โ† Semua pembelajaran / Go Nol โ†’ Enterprise
Fase 10 ยท Deploy di AWS

Image Docker: multi-stage & distroless

Binari Go statis membuat image container jadi sangat kecil. Yang menentukan hasilnya bukan trik, melainkan urutan langkah build supaya cache lapisan bekerja, dan basis image yang dipilih.

Sumber asli docs.docker.com Resmi Rangkuman ~8 menit baca

Intisari

  • Dua tahap: golang untuk membangun, scratch atau distroless untuk menjalankan.
  • Salin go.mod dan go.sum lebih dulu, unduh dependensi, baru salin kode โ€” supaya cache lapisan tidak batal setiap perubahan kode.
  • CGO_ENABLED=0 menghasilkan binari statis. Tanpa itu, scratch gagal jalan dengan pesan yang menyesatkan.
  • Image scratch tetap butuh sertifikat CA dan zoneinfo, atau HTTPS dan zona waktu rusak.
  • Jalankan sebagai pengguna bukan root, dan jangan pernah memberi tag latest pada image produksi.

Dockerfile yang dipakai

FROM golang:1.26-bookworm AS build

WORKDIR /src

# 1. Dependensi lebih dulu โ€” lapisan ini hanya batal saat go.mod/go.sum berubah.
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

# 2. Baru kode. Perubahan kode tidak mengulang unduhan dependensi.
COPY . .

# 3. Build statis, tanpa simbol debug.
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 GOOS=linux \
    go build -trimpath \
      -ldflags="-s -w -X main.versi=${VERSI} -X main.commit=${COMMIT}" \
      -o /out/server ./cmd/server

# โ”€โ”€ Tahap jalan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=build /out/server /server

USER nonroot:nonroot
EXPOSE 8080

ENTRYPOINT ["/server"]
BarisKenapa penting
COPY go.mod go.sum terpisahCache lapisan: satu perubahan kode tidak mengulang go mod download
--mount=type=cacheCache modul dan build bertahan antar build โ€” memangkas waktu CI drastis
CGO_ENABLED=0Binari benar-benar statis; tanpa ini scratch gagal
-trimpathBuang jalur absolut dari binari โ€” build reproducible, tidak bocor struktur folder
-ldflags="-s -w"Buang tabel simbol; binari ~25% lebih kecil
distroless:nonrootTanpa shell, tanpa manajer paket, bukan root

Memilih basis image

BasisUkuranIsiUntuk
scratch~0 MBBenar-benar kosongPaling kecil; kamu urus CA dan zoneinfo sendiri
distroless/static~2 MBCA, zoneinfo, /etc/passwdRekomendasi
alpine~8 MBmusl + BusyBoxKalau butuh shell untuk debug
debian-slim~80 MBglibc lengkapKalau perlu cgo

distroless/static mengalahkan scratch untuk hampir semua kasus. Ia sudah membawa tiga hal yang selalu dibutuhkan aplikasi web: sertifikat CA (tanpa itu setiap panggilan HTTPS gagal dengan x509: certificate signed by unknown authority), basis data zona waktu (tanpa itu time.LoadLocation("Asia/Jakarta") gagal), dan entri pengguna non-root. Tambahannya cuma 2 MB.

# Kalau tetap memakai scratch, ini yang harus disalin sendiri:
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /etc/passwd /etc/passwd
COPY --from=build /out/server /server
USER 65534
ENTRYPOINT ["/server"]

Alternatif untuk zona waktu: impor _ "time/tzdata" di main.go. Ia menyematkan seluruh basis data zona waktu ke dalam binari (~450 KB), sehingga image tidak perlu menyediakannya sama sekali.

Kesalahan yang khas

GejalaPenyebab
exec format error / no such file or directoryBinari dinamis di scratch โ€” lupa CGO_ENABLED=0
x509: certificate signed by unknown authoritySertifikat CA tidak ikut
unknown time zone Asia/Jakartazoneinfo tidak ikut
Build lambat setiap kaliCOPY . . sebelum go mod download
Image ratusan MBTidak memakai multi-stage; folder build ikut terbawa
Task gagal start di FargateArsitektur salah โ€” Fargate ARM64 butuh GOARCH=arm64

Multi-arsitektur

# Fargate Graviton (ARM64) sekitar 20% lebih murah untuk kinerja setara.
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t 123456789.dkr.ecr.ap-southeast-1.amazonaws.com/toko:v1.4.2 \
  --push .
# Di Dockerfile, terima argumen platform dari buildx
FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS build
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/server ./cmd/server

Karena Go melakukan kompilasi silang secara asli, membangun image ARM64 dari runner amd64 tidak butuh emulasi QEMU โ€” hanya menyetel GOARCH. Ini berbeda jauh dari bahasa yang perlu mengompilasi ekstensi native, dan membuat pindah ke Graviton jadi keputusan yang murah.

.dockerignore

.git
.github
*_test.go
testdata/
bin/
tmp/
deploy/
README.md
.env
.env.*

Tanpa .dockerignore, seluruh folder .git ikut ke konteks build โ€” memperlambat build, membesarkan konteks, dan (kalau Dockerfile-mu melakukan COPY . . ke tahap akhir) berpotensi membawa rahasia yang pernah ter-commit ke dalam image. Baris .env di atas bukan formalitas.

Memeriksa hasilnya

docker images toko:v1.4.2                      # ukuran
docker history toko:v1.4.2                     # lapisan mana yang besar
docker run --rm toko:v1.4.2 --versi            # binarinya jalan?
trivy image toko:v1.4.2                        # pindai kerentanan
docker sbom toko:v1.4.2                        # daftar isi
REPOSITORY   TAG       SIZE
toko         v1.4.2    17.2MB      โ† distroless + binari Go

Bandingkan:
  golang:1.26 sebagai basis runtime   ~900 MB
  node:22-alpine + aplikasi           ~180 MB
  php:8.4-fpm + aplikasi              ~500 MB

Ukuran kecil bukan sekadar estetika. Ia berarti pull lebih cepat saat scale-out (Fargate mengunduh image setiap task baru), biaya penyimpanan ECR lebih rendah, dan permukaan serangan jauh lebih sempit โ€” tidak ada shell, curl, atau manajer paket yang bisa dipakai penyerang yang berhasil mengeksekusi kode.

Latihan: bangun aplikasimu dengan Dockerfile di atas dan catat ukurannya. Lalu hapus CGO_ENABLED=0 dan coba jalankan di scratch โ€” baca pesan errornya. Terakhir, jalankan docker run --rm -it --entrypoint sh toko:v1.4.2 pada image distroless dan lihat bahwa memang tidak ada shell untuk dimasuki.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.