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.
Intisari
- Dua tahap:
golanguntuk membangun,scratchataudistrolessuntuk menjalankan. - Salin
go.moddango.sumlebih dulu, unduh dependensi, baru salin kode โ supaya cache lapisan tidak batal setiap perubahan kode. CGO_ENABLED=0menghasilkan binari statis. Tanpa itu,scratchgagal jalan dengan pesan yang menyesatkan.- Image
scratchtetap butuh sertifikat CA dan zoneinfo, atau HTTPS dan zona waktu rusak. - Jalankan sebagai pengguna bukan root, dan jangan pernah memberi tag
latestpada 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"]
| Baris | Kenapa penting |
|---|---|
COPY go.mod go.sum terpisah | Cache lapisan: satu perubahan kode tidak mengulang go mod download |
--mount=type=cache | Cache modul dan build bertahan antar build โ memangkas waktu CI drastis |
CGO_ENABLED=0 | Binari benar-benar statis; tanpa ini scratch gagal |
-trimpath | Buang jalur absolut dari binari โ build reproducible, tidak bocor struktur folder |
-ldflags="-s -w" | Buang tabel simbol; binari ~25% lebih kecil |
distroless:nonroot | Tanpa shell, tanpa manajer paket, bukan root |
Memilih basis image
| Basis | Ukuran | Isi | Untuk |
|---|---|---|---|
scratch | ~0 MB | Benar-benar kosong | Paling kecil; kamu urus CA dan zoneinfo sendiri |
distroless/static | ~2 MB | CA, zoneinfo, /etc/passwd | Rekomendasi |
alpine | ~8 MB | musl + BusyBox | Kalau butuh shell untuk debug |
debian-slim | ~80 MB | glibc lengkap | Kalau 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
| Gejala | Penyebab |
|---|---|
exec format error / no such file or directory | Binari dinamis di scratch โ lupa CGO_ENABLED=0 |
x509: certificate signed by unknown authority | Sertifikat CA tidak ikut |
unknown time zone Asia/Jakarta | zoneinfo tidak ikut |
| Build lambat setiap kali | COPY . . sebelum go mod download |
| Image ratusan MB | Tidak memakai multi-stage; folder build ikut terbawa |
| Task gagal start di Fargate | Arsitektur 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.