
Hari 16: Docker Image 8MB, Distroless Bikin Aman
Fase 2 dimulai! Bikin Docker image SecureBank API cuma 8MB pakai multi-stage build dan distroless. Build di alpine, run tanpa OS — attack surface minimal.
Fase 2 Dimulai: Dari Kode ke Container
Fase 1 kemarin fokusnya ke kode — pipeline CI/CD, SAST, SCA, secret scanning, input validation, JWT auth, threat modeling. Semua di level source code.
Fase 2 sekarang fokusnya ke infrastruktur — container, Terraform, DAST. Dan langkah pertama: bungkus aplikasi ke dalam Docker image yang sekecil dan seaman mungkin.
Kenapa Multi-stage Build?
Bayangkan dua skenario:
Skenario 1: Single-stage build
FROM golang:1.26
COPY . .
RUN go build -o /securebank ./cmd/api
ENTRYPOINT ["/securebank"]
Image size: ~800MB. Kenapa? Karena image bawa seluruh Go toolchain, Alpine package manager, git, dan ratusan library yang gak dibutuhin di runtime. Attacker yang masuk ke container ini bisa apk add, curl, wget, dan sh — semua tersedia.
Skenario 2: Multi-stage build
FROM golang:1.26-alpine AS builder # ~300MB (buang setelah build)
RUN go build -ldflags="-w -s" -o /securebank ./cmd/api
FROM gcr.io/distroless/static-debian12:nonroot # ~2MB
COPY --from=builder /securebank /securebank
ENTRYPOINT ["/securebank"]
Image size: 7.97MB. Hanya binary + CA certs. Tidak ada shell, tidak ada package manager, tidak ada utilitas apapun. Attacker yang masuk gak bisa apa-apa karena... memang gak ada apa-apanya.
Apa yang Dibuat Hari Ini
Dockerfile Multi-stage
Stage 1 (builder):
- Base image:
golang:1.26-alpine(~300MB) - Install
gitdanca-certificates - Download Go modules (
go mod download) - Build binary dengan
CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s"
Stage 2 (runtime):
- Base image:
gcr.io/distroless/static-debian12:nonroot(~2MB) - Copy binary dari builder stage
- Copy CA certificates (buat HTTPS request ke external API kalau perlu)
USER nonroot:nonroot— container jalan sebagai user biasa, bukan rootEXPOSE 8080ENTRYPOINT ["/securebank"]
.dockerignore
Datangnya kecil tapi penting — .dockerignore mencegah file gak perlu masuk ke Docker build context:
.git
.github
docs
progress
security
*.md
*.test
*.out
coverage.out
.env
.env.*
Tanpa ini, seluruh repo (termasuk .git, report JSON, dll) bakal di-copy ke builder stage dan memperlambat build.
Hasil: 7.97MB
$ docker images securebank:v1
REPOSITORY TAG IMAGE ID SIZE
securebank v1 4f8e6cb6a41e 7.97MB
Target < 15MB, hasil 7.97MB. Distroless + stripped binary = ultra-compact.
Container Test
$ docker run -d -p 8080:8080 -e JWT_SECRET=test-secret securebank:v1
$ curl http://localhost:8080/health
{"status":"healthy"}
Aplikasi berjalan normal di container. /health endpoint merespons karena memang dirancang public (tanpa auth).
Beberapa Detail yang Penting
CGO_ENABLED=0 itu wajib untuk distroless
Distroless static-debian12 gak punya glibc. Kalau binary di-link secara dinamis ke glibc (default behaviour Go kalau CGO aktif), binary bakal crash di runtime. CGO_ENABLED=0 bikin binary statically-linked — semua library sudah di-include di dalam binary.
-ldflags="-w -s" bikin binary lebih kecil
-w= strip DWARF debug info (gak perlu di production)-s= strip symbol table (nama fungsi, variabel, dll)
Hasilnya binary Go yang tadinya ~12MB jadi ~7MB. Bonus: attacker gak bisa objdump atau strings untuk melihat detail internal binary.
Distroless gak punya shell
Ini feature, bukan bug. Tidak ada sh, bash, apk, apt, curl, wget, atau utilitas apapun. Keuntungannya:
- Attack surface minimal — gak ada yang bisa di-exploit
- Image size kecil — gak ada layer yang gak perlu
- Gak bisa di-debug langsung — kalau mau debug, harus pakai
docker cpatau ephemeral debug container
Kalau butuh debug, bisa pakai:
kubectl debug -it securebank-pod --image=busybox --target=securebank
USER nonroot:nonroot
Container jalan sebagai user nonroot (UID 65534), bukan root. Ini berarti kalau attacker berhasil masuk, mereka gak punya akses root di container. Ini gak sempurna (container escape masih mungkin), tapi menambahkan satu lapis pertahanan lagi.
Lesson Learned
1. Multi-stage build itu fundamental untuk container security. Build stage bawa toolchain yang besar (300MB+), tapi hanya binary final yang masuk ke runtime stage. Attack surface drastis berkurang — dari OS lengkap ke binary saja.
2. Image size kecil = attack surface kecil. 800MB single-stage vs 8MB multi-stage. Lebih sedikit code di production = lebih sedikit celah. Ini prinsip yang sama kayak "minimal installation" di server biasa.
3. Distroless itu security by default. Gak ada shell = gak bisa di-exploit dengan shell-based attack. Attacker masuk ke container dan... gak bisa apa-apa. Tapi ini juga artinya debugging lebih susah — harus pakai debug container terpisah.
4. CGO_ENABLED=0 bukan cuma opsi, itu kebutuhan. Tanpa flag ini, binary bond to glibc dan gak bakal jalan di distroless. Ini mistake yang sering terjadi pertama kali build Go image untuk distroless.
5. .dockerignore itu seperti .gitignore tapi untuk container. File yang gak perlu (.git, docs, test files) gak usah masuk ke build context. Ini bikin build lebih cepat dan image lebih kecil.
Kesimpulan
Hari ini SecureBank API resmi jadi container. Dari binary Go yang jalan di laptop ke Docker image 7.97MB yang jalan di distroless. Build di alpine (300MB+), run di distroless (2MB base). Container jalan sebagai nonroot, gak ada shell, gak ada utilitas.
Ini baru permulaan Fase 2. Besok: scan image ini dengan Trivy dan bandingkan hasilnya dengan image "naif" yang pakai alpine base. Spoiler: perbedaannya bakal signifikan.
Diskusi & Komentar
Hari 15: Fase 1 Selesai, 4 Quality Gates, 0 CVE
Next ArticleHari 17: Distroless 8MB vs Alpine 352MB, Sama-Sama 0 CVE
Artikel Terkait
Hari 5: Trivy SCA Scan Nemukan 4 CVE di Golang API
Hari kelima 60 hari DevSecOps! Scan dependensi Go pakai Trivy dan nemukan 4 CVE termasuk 1 CRITICAL — termasuk library deprecated jwt-go.
Hari 8: Semgrep SAST Scan Temukan Kode Tidak Aman
Hari kedelapan 60 hari DevSecOps! Install Semgrep, buat kode insecure (MD5), dan scan ketemu 2 finding — MD5 weak hash dan HTTP server tanpa TLS.
Hari 10: MD5 ke Bcrypt, Pipeline Hijau Lagi
Hari kesepuluh 60 hari DevSecOps! Fix SAST findings — ganti MD5 ke bcrypt, hapus custom rule HTTP TLS, dan pipeline CI kembali hijau setelah 4 job semua pass.