Go dirancang sejak awal sebagai bahasa yang menghasilkan binary tunggal hasil kompilasi, berbeda dari Node.js atau Python yang butuh runtime dan seluruh dependency ikut disalin ke dalam image. Karakteristik ini membuat aplikasi Go jadi kandidat paling ideal untuk image container yang benar-benar minimal, bahkan bisa berjalan tanpa sistem operasi lengkap sama sekali di dalamnya. Bab ini membahas studi kasus penuh containerizing aplikasi Go: menyusun Dockerfile multi-stage yang menghasilkan binary statis, memilih base image paling minimal seperti scratch atau distroless, menekan ukuran image lewat beberapa teknik optimasi, sampai menyiapkan live reload untuk kebutuhan development sehari-hari lewat Air atau CompileDaemon.
31.1 Multi-Stage Build untuk Binary Go Minimal
Aplikasi Go dikompilasi menjadi satu file binary yang sudah menyertakan seluruh dependency-nya, sehingga image final sebenarnya tidak butuh Go toolchain, source code, maupun go.mod sama sekali untuk bisa berjalan. Multi-stage build jadi pola yang paling pas dimanfaatkan di sini: tahap pertama memakai image Go lengkap untuk meng-compile source code, tahap kedua cukup menyalin hasil binary-nya saja ke image yang jauh lebih kecil.
31.1.1 Struktur Project Go untuk Container
Project Go yang lazim dipakai untuk web service biasanya punya struktur sederhana dengan go.mod di root project sebagai penanda module, ditambah file main.go sebagai entry point.
myapp/
├── go.mod
├── go.sum
├── main.go
├── internal/
│ ├── handler/
│ └── config/
├── .dockerignore
└── DockerfileFile go.sum menyimpan checksum tiap dependency untuk memastikan versi yang di-download saat build persis sama dengan yang dipakai saat development, sehingga file ini wajib ikut disalin ke dalam image bersama go.mod. Folder internal dipakai Go untuk membatasi package di dalamnya supaya cuma bisa diimpor dari dalam module yang sama, praktik umum untuk memisahkan kode aplikasi dari public API kalau project juga diekspos sebagai library.
31.1.2 Dockerfile Multi-Stage Dasar
Tahap builder memakai image resmi golang yang sudah menyertakan compiler dan toolchain lengkap, sementara tahap final cukup memakai scratch, image kosong tanpa isi apa pun yang disediakan Docker.
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./main.go
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]Instruksi COPY --from=builder cuma menyalin satu file binary /app/server ke image final, meninggalkan seluruh source code, Go toolchain, dan file sementara hasil build di tahap builder yang tidak ikut terbawa ke image production. Hasilnya, image final bisa berukuran hanya beberapa megabyte, jauh lebih kecil dibanding image aplikasi Node.js atau Python yang tetap butuh runtime lengkap di tahap production.
31.1.3 Caching go.mod dan go.sum untuk Build Lebih Cepat
Urutan instruksi COPY pada Dockerfile di atas sengaja dipisah: go.mod dan go.sum disalin dan di-download dependency-nya lebih dulu, baru kemudian seluruh source code disalin lewat COPY . .. Pola ini memanfaatkan Docker layer caching supaya go mod download cuma dijalankan ulang kalau memang ada perubahan pada dependency, bukan setiap kali ada perubahan kode aplikasi.
Di lapangan, project yang menyalin seluruh source code lebih dulu sebelum menjalankan go mod download sering mengeluh build Docker terasa lambat, padahal penyebabnya cuma urutan instruksi yang keliru, bukan soal jaringan atau ukuran dependency itu sendiri. Verifikasi urutan cache bekerja dengan menjalankan build dua kali berturut-turut tanpa mengubah dependency.
docker build -t myapp .
docker build -t myapp .Build kedua semestinya menampilkan CACHED pada langkah RUN go mod download kalau go.mod dan go.sum tidak berubah sejak build sebelumnya.
31.2 Static Compilation dan Distroless/Scratch Base Image
Dockerfile pada sub-bab sebelumnya sudah memakai CGO_ENABLED=0 supaya binary yang dihasilkan benar-benar statis, syarat mutlak supaya binary tersebut bisa berjalan di image sekosong scratch. Bagian ini membahas lebih dalam kenapa opsi ini penting, serta pilihan base image minimal yang tersedia selain scratch.
31.2.1 CGO_ENABLED dan Static Linking
Secara default, Go toolchain mengaktifkan cgo, mekanisme yang memungkinkan kode Go memanggil pustaka C, terutama dipakai secara implisit oleh package net dan os/user untuk resolusi DNS dan pencarian user di sistem lewat pustaka C standar. Kalau cgo aktif, binary hasil go build jadi dynamically linked terhadap pustaka C sistem seperti glibc, sehingga binary tersebut cuma bisa berjalan di sistem yang punya pustaka yang sama persis, dan pasti gagal dijalankan di image scratch yang sama sekali tidak punya pustaka apa pun.
Menyetel CGO_ENABLED=0 memaksa Go toolchain memakai implementasi Go murni untuk resolusi DNS dan fungsi terkait, menghasilkan binary yang statically linked dan tidak bergantung pustaka sistem apa pun.
CGO_ENABLED=0 GOOS=linux go build -o /app/server ./main.goVariabel GOOS=linux memastikan binary dikompilasi untuk sistem Linux, penting terutama kalau proses build dijalankan dari mesin macOS atau Windows tapi image container yang dituju berbasis Linux. Verifikasi binary benar-benar statis lewat perintah file setelah build selesai.
file /app/serverOutput yang menampilkan statically linked menandakan binary sudah siap dijalankan di base image minimal seperti scratch. Kalau output masih menampilkan dynamically linked, periksa ulang apakah CGO_ENABLED=0 benar-benar diterapkan pada proses build, atau apakah ada dependency pihak ketiga yang memaksa cgo tetap aktif.
31.2.2 Base Image Scratch: Kelebihan dan Keterbatasan
scratch adalah image paling minimal yang disediakan Docker, benar-benar kosong tanpa shell, tanpa package manager, tanpa certificate authority, dan tanpa data timezone. Cocok untuk binary Go statis yang tidak butuh apa pun selain dirinya sendiri, tapi keterbatasannya perlu diperhatikan Developer sebelum dipakai di production.
Aplikasi yang melakukan request HTTPS keluar, misalnya memanggil API pihak ketiga, butuh sertifikat root CA untuk memverifikasi certificate server tujuan. Karena scratch tidak menyertakan sertifikat apa pun, sertifikat tersebut perlu disalin manual dari tahap builder.
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./main.go
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]Tanpa baris COPY sertifikat ini, panggilan HTTPS dari dalam container akan gagal dengan error semacam x509: certificate signed by unknown authority, meskipun binary aplikasinya sendiri berjalan normal. Kalau aplikasi juga bergantung pada zona waktu lokal lewat package time/tzdata, data timezone perlu disalin serupa dari /usr/share/zoneinfo di image builder, atau lebih sederhana dengan meng-import package time/tzdata langsung di kode Go supaya data timezone ikut ter-embed ke dalam binary itu sendiri.
Keterbatasan lain yang sering jadi jebakan Sysadmin/DevOps Engineer saat troubleshooting: scratch tidak punya shell sama sekali, sehingga perintah docker exec -it <container> sh yang lazim dipakai untuk masuk ke dalam container pasti gagal dengan pesan exec: "sh": executable file not found. Debugging container berbasis scratch mengandalkan docker logs dan tool eksternal seperti docker debug atau ephemeral container lewat kubectl debug di lingkungan Kubernetes, bukan masuk langsung ke dalam container yang bermasalah.
31.2.3 Distroless sebagai Alternatif Scratch
Distroless adalah kumpulan image minimal yang dirawat proyek gcr.io/distroless, menyediakan lapisan tipis di atas scratch yang sudah menyertakan sertifikat root CA, data timezone, serta konfigurasi user dan group dasar, tanpa menyertakan shell, package manager, atau tool sistem lain yang tidak dibutuhkan aplikasi saat runtime.
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./main.go
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]Varian static-debian12 dipakai untuk binary yang benar-benar statis seperti hasil CGO_ENABLED=0, sudah menyertakan sertifikat root CA dan data timezone tanpa perlu disalin manual seperti pada scratch. Tag nonroot menyediakan user nonroot dengan UID 65532 yang sudah dikonfigurasi sejak dari image dasarnya, sehingga instruksi USER nonroot:nonroot cukup merujuk user tersebut tanpa perlu membuat user baru lewat useradd yang toh tidak tersedia karena image ini tidak punya shell maupun package manager.
Kalau aplikasi masih butuh cgo aktif karena dependency tertentu, misalnya driver database yang mensyaratkan pustaka C, distroless menyediakan varian gcr.io/distroless/base-debian12 yang menyertakan glibc dan pustaka C dasar, berbeda dengan varian static yang murni untuk binary statis tanpa dependency pustaka C sama sekali.
31.3 Optimasi Ukuran Image untuk Go App
Base image minimal seperti scratch atau distroless sudah jadi fondasi image kecil, tapi ukuran binary Go itu sendiri juga bisa ditekan lebih lanjut lewat beberapa opsi compiler dan strategi build, sekaligus mempercepat proses build lewat pemanfaatan cache yang lebih optimal.
31.3.1 Strip Debug Symbols dengan ldflags
Binary Go secara default menyertakan informasi debug symbol dan path source code yang berguna untuk profiling dan debugging, tapi menambah ukuran binary tanpa manfaat kalau memang tidak dipakai di production. Opsi -ldflags="-w -s" membuang informasi tersebut saat proses linking.
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./main.goFlag -w membuang tabel informasi DWARF yang dipakai debugger seperti delve untuk memetakan baris kode ke instruksi binary, sementara flag -s membuang tabel simbol yang dipakai untuk stack trace yang lebih deskriptif. Konsekuensinya, binary hasil build dengan kedua flag ini jadi lebih sulit di-debug langsung lewat delve kalau ada masalah di production, sehingga sebagian tim memilih tetap menyertakan debug symbol pada build khusus staging atau debugging, dan cuma memakai -w -s untuk image yang benar-benar dituju ke production.
31.3.2 Perbandingan Ukuran Antar Base Image
Perbedaan ukuran akhir image antar pilihan base image cukup signifikan untuk aplikasi Go yang sama persis. Tabel berikut merangkum karakteristik masing-masing pilihan yang sudah dibahas.
| Base Image | Shell | Root CA | Cocok untuk |
|---|---|---|---|
golang:1.23 | Ada | Ada | Tahap builder, bukan production |
alpine | Ada (ash) | Perlu ca-certificates | Butuh debugging shell atau cgo dengan musl |
gcr.io/distroless/static | Tidak ada | Sudah tersedia | Binary statis (CGO_ENABLED=0) |
scratch | Tidak ada | Perlu disalin manual | Binary statis, kontrol penuh atas isi image |
Base image alpine kadang jadi pilihan populer karena ukurannya kecil dan tetap menyediakan shell untuk keperluan debugging, tapi perlu diperhatikan bahwa alpine memakai musl libc, bukan glibc. Kalau binary Go dikompilasi dengan cgo aktif di lingkungan glibc lalu dijalankan di atas alpine yang berbasis musl, binary tersebut bisa gagal berjalan karena perbedaan implementasi pustaka C, jebakan yang sering ditemui Developer yang baru pindah dari base image debian atau ubuntu ke alpine tanpa menyesuaikan ulang proses build cgo-nya.
31.3.3 BuildKit Cache Mount untuk Build Go
BuildKit, builder default Docker sejak beberapa versi terakhir, mendukung cache mount lewat opsi --mount=type=cache yang memungkinkan direktori cache Go module dan build cache tetap tersimpan antar proses build, meskipun layer Docker sendiri di-invalidate.
# syntax=docker/dockerfile:1
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./main.go
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]Direktori /go/pkg/mod menyimpan hasil download module Go, sedangkan /root/.cache/go-build menyimpan hasil kompilasi package yang bisa dipakai ulang oleh go build di build berikutnya. Baris komentar # syntax=docker/dockerfile:1 di baris pertama wajib disertakan supaya Docker memakai sintaks Dockerfile terbaru yang mendukung opsi --mount pada instruksi RUN, sesuai dokumentasi resmi BuildKit.
Beda dengan cache layer biasa yang bergantung urutan COPY, cache mount tetap dipakai ulang meskipun seluruh source code berubah total, karena cache tersebut disimpan terpisah dari layer image dan tidak ikut menjadi bagian dari image final. Di lapangan, teknik ini terasa manfaatnya pada project Go besar dengan banyak dependency, di mana build tanpa cache mount bisa makan waktu signifikan cuma untuk mengompilasi ulang package yang sebenarnya tidak berubah sama sekali.
31.4 Live Reload untuk Development (Air/CompileDaemon)
Sifat Go sebagai compiled language berarti setiap perubahan kode butuh proses compile ulang sebelum bisa dijalankan, berbeda dengan Node.js atau Python yang bisa langsung menjalankan ulang interpreter-nya. Tanpa bantuan tool tambahan, workflow development di dalam container jadi merepotkan karena Developer harus menghentikan container, menjalankan ulang build, lalu menjalankan container lagi setiap kali ada perubahan baris kode.
31.4.1 Setup Air untuk Live Reload
Air adalah tool live reload populer di ekosistem Go yang memantau perubahan file source code, lalu otomatis menjalankan ulang proses go build dan menjalankan binary hasilnya begitu ada perubahan terdeteksi. Buat Dockerfile khusus development yang menginstal Air di dalam image, terpisah dari Dockerfile production yang sudah dibahas sebelumnya.
FROM golang:1.23
WORKDIR /app
RUN go install github.com/air-verse/air@latest
COPY go.mod go.sum ./
RUN go mod download
COPY . .
EXPOSE 8080
CMD ["air", "-c", ".air.toml"]Air butuh file konfigurasi .air.toml di root project yang menentukan perintah build dan pola file yang dipantau.
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/main ./main.go"
bin = "./tmp/main"
include_ext = ["go"]
exclude_dir = ["tmp", "vendor"]
delay = 1000
[log]
time = falseJalankan air init di dalam project untuk menghasilkan template .air.toml secara otomatis, lalu sesuaikan bagian cmd dan bin sesuai struktur project. Supaya perubahan kode di host langsung terdeteksi Air di dalam container, definisikan service development di compose.yaml dengan bind mount ke source code.
services:
app-dev:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "8080:8080"
volumes:
- .:/app
- go-mod-cache:/go/pkg/mod
volumes:
go-mod-cache:Bind mount .:/app membuat perubahan file di host langsung tercermin di dalam container tanpa perlu rebuild image, sementara named volume go-mod-cache menjaga module cache Go tetap ada meski container dibuat ulang, supaya go mod download tidak perlu mengunduh ulang seluruh dependency setiap kali container development dinyalakan dari awal.
31.4.2 Alternatif CompileDaemon
CompileDaemon adalah pilihan lain yang lebih sederhana dibanding Air, tidak butuh file konfigurasi terpisah karena seluruh opsi ditentukan lewat flag saat dijalankan.
RUN go install github.com/githubnemo/CompileDaemon@latest
CMD CompileDaemon -polling -log-prefix=false \
-build="go build -o /tmp/server ./main.go" \
-command="/tmp/server"Opsi -polling penting disertakan saat CompileDaemon dijalankan di dalam container Linux dengan source code yang di-mount dari host macOS atau Windows lewat Docker Desktop, karena mekanisme filesystem event native kadang tidak diteruskan dengan benar lewat lapisan virtualisasi Docker Desktop, sehingga CompileDaemon perlu memeriksa perubahan file secara berkala alih-alih menunggu notifikasi event. Air punya opsi serupa lewat poll = true di dalam blok [build] pada .air.toml kalau menghadapi masalah yang sama.
31.4.3 Troubleshooting Live Reload di Dalam Container
Kalau Air atau CompileDaemon terlihat berjalan tapi tidak bereaksi terhadap perubahan file, periksa lebih dulu apakah bind mount di compose.yaml benar-benar mengarah ke folder project yang sedang diedit, bukan tersalin dari proses COPY di image yang jadinya tidak ter-sync dengan perubahan di host.
docker compose exec app-dev ls -la /appPerintah di atas memverifikasi isi folder /app di dalam container sudah sesuai dengan isi folder project di host. Kalau isinya sudah sesuai tapi reload tetap tidak terjadi, aktifkan opsi polling seperti dijelaskan sebelumnya, karena ini jadi penyebab paling umum ditemui Developer yang menjalankan Docker Desktop di macOS atau Windows dengan WSL2.
Satu hal yang perlu diingat soal trustworthiness workflow ini: Dockerfile development dengan Air atau CompileDaemon tidak boleh dipakai untuk image production, karena keduanya menyertakan Go toolchain lengkap dan tool tambahan yang menambah ukuran image serta attack surface tanpa manfaat di production. Pisahkan selalu Dockerfile development dan Dockerfile production seperti pola yang sudah dibahas di sub-bab multi-stage build dan distroless sebelumnya, supaya image yang benar-benar di-deploy tetap minimal dan aman.

