Dockerfile Gin untuk Local Development dan Stage Lainnya
Docker sering langsung diasosiasikan dengan production, padahal kebutuhan setiap stage dalam lifecycle aplikasi Golang + Gin berbeda jauh. Local development butuh iterasi cepat, testing butuh environment yang konsisten, sementara production butuh image yang kecil dan aman. Kesalahan yang sering terjadi adalah memakai satu Dockerfile yang sama untuk semua stage tersebut — akibatnya local development jadi lambat karena ukuran image yang tidak perlu, image production jadi besar karena membawa tool development, dan workflow tim jadi tidak efisien. Artikel ini membahas cara menyusun Dockerfile yang tepat untuk setiap stage, mulai dari local dev hingga production dengan multi-stage build.
Prinsip Dasar Dockerfile untuk Gin
Secara fundamental, Dockerfile untuk aplikasi Gin melakukan empat hal: menentukan base image Go, mengunduh dependency, build binary, lalu menjalankan aplikasi. Berikut contoh paling dasar yang mencakup keempatnya:
FROM golang:1.21
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o app
EXPOSE 8080
CMD ["./app"]
Dockerfile ini valid dan bisa langsung dijalankan, tapi tidak ideal jika dipakai untuk semua kebutuhan. Base image golang:1.21 membawa seluruh toolchain Go (compiler, source code standar library, dan tool pendukung) yang ukurannya bisa mencapai ratusan megabyte — sesuatu yang berguna saat development tapi sia-sia saat runtime di production. Masalahnya bukan pada kode Dockerfile itu sendiri, tapi pada asumsi bahwa satu konfigurasi bisa memuaskan semua kebutuhan sekaligus.
Setiap stage dalam lifecycle aplikasi punya prioritas yang berbeda, dan prioritas itu sering saling bertentangan. Local development ingin iterasi secepat mungkin, sementara production ingin image sekecil dan seaman mungkin — dua tujuan yang kalau dipaksa dicapai dengan satu Dockerfile, hasilnya kompromi yang buruk untuk keduanya. Tabel berikut merangkum perbedaan prioritas tersebut sebelum masuk ke detail masing-masing stage:
| Stage | Prioritas Utama | Hal yang Dihindari |
|---|---|---|
| Local Dev | Iterasi cepat, hot reload | Rebuild image setiap perubahan kode |
| Testing | Konsistensi environment | Server yang ikut berjalan saat test |
| Staging | Mendekati kondisi production | Konfigurasi yang menyimpang jauh dari prod |
| Production | Ukuran kecil, keamanan | Tool development yang tidak perlu |
Bagian berikutnya membahas bagaimana Dockerfile dasar di atas berkembang menjadi versi yang sesuai untuk masing-masing stage tersebut, dimulai dari local development.
Versi Go pada contoh (1.21) mengikuti artikel asli — sesuaikan dengan versi yang dipakai di proyekmu. Prinsip yang dibahas di artikel ini berlaku untuk versi Go manapun, termasuk versi-versi terbaru.Dockerfile untuk Local Development
Tujuan Local Development
Pada tahap local development, fokus utama bukan ukuran image atau keamanan runtime, melainkan kecepatan iterasi. Developer ingin perubahan kode langsung terlihat efeknya tanpa harus rebuild image setiap kali menyimpan file. Tiga kebutuhan utamanya:
- Perubahan kode cepat terlihat
- Tidak perlu rebuild image setiap save
- Mendukung hot reload
Pendekatan yang Tepat
Untuk mencapai kecepatan iterasi tersebut, kombinasikan tiga pendekatan: volume mount agar perubahan file di host langsung terbaca container, tool hot reload seperti air yang otomatis me-restart aplikasi saat file berubah, dan image yang tidak perlu dioptimasi ukurannya karena hanya dipakai secara lokal.
Contoh Dockerfile Local Dev
FROM golang:1.21
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go install github.com/air-verse/air@latest
CMD ["air"]
Dockerfile ini sengaja sederhana dan tidak mengejar ukuran image kecil. air akan memonitor perubahan file Go di dalam container — kombinasikan dengan volume mount (dibahas lebih lanjut di bagian Docker Compose) supaya perubahan kode di editor langsung terdeteksi tanpa rebuild image sama sekali.
Cara kerja air cukup sederhana: ia membaca konfigurasi (biasanya dari file .air.toml), memantau ekstensi file yang ditentukan (default-nya .go), lalu menjalankan ulang go build dan restart proses setiap ada perubahan terdeteksi. Karena proses build-nya berjalan di dalam container, kamu tidak perlu menginstal Go di mesin host sama sekali — semua toolchain ada di dalam container, host hanya menyediakan editor dan source code.
# .air.toml — konfigurasi minimal untuk hot reload
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/main ."
bin = "tmp/main"
include_ext = ["go"]
exclude_dir = ["tmp", "vendor"]
[log]
time = false
Hot reload hanya berguna kalau source code-nya benar-benar ter-mount dari host. Tanpa volume mount,COPY . .di Dockerfile cuma menyalin snapshot kode saat build —airakan jalan, tapi tidak akan mendeteksi perubahan apa pun.
Satu hal lain yang sering terlewat di stage ini adalah .dockerignore. Tanpa file ini, instruksi COPY . . akan menyalin semua isi direktori project ke dalam image — termasuk folder .git, file .env lokal, dan binary hasil build sebelumnya. Selain memperlambat proses build karena ukuran context yang besar, ini juga berisiko membawa data sensitif ke dalam image.
# .dockerignore
.git
.env
tmp/
*.log
Dockerfile untuk Testing Stage
Untuk testing — baik unit test maupun integration test — kebutuhannya berbeda lagi. Di sini yang diinginkan adalah environment yang konsisten antar developer dan CI, test yang berjalan otomatis saat container start, dan tidak ada server yang dijalankan sama sekali karena tujuannya bukan melayani request.
Contoh Dockerfile Testing
FROM golang:1.21
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
CMD ["go", "test", "./..."]
Strukturnya hampir identik dengan Dockerfile local dev — bedanya hanya pada CMD. Alih-alih menjalankan air atau binary aplikasi, container langsung menjalankan go test ./... yang mengeksekusi seluruh test suite di project lalu exit. Dockerfile semacam ini biasanya tidak dijalankan manual oleh developer, tapi dipanggil otomatis di CI pipeline setiap ada push atau pull request.
Alasan kenapa test dijalankan di dalam container, bukan langsung di mesin CI runner, adalah konsistensi. Versi Go di laptop developer A bisa berbeda dengan laptop developer B, dan keduanya bisa berbeda lagi dengan versi yang terinstal di CI runner. Test yang lolos di satu environment tapi gagal di environment lain — biasa disebut “works on my machine” — adalah sumber frustrasi klasik yang bisa dihindari dengan menjalankan test di dalam image yang versi Go-nya sudah dikunci secara eksplisit.
# Variasi: testing dengan coverage report
FROM golang:1.21
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
CMD ["go", "test", "-v", "-cover", "./..."]
Flag -cover di atas menambahkan informasi code coverage ke output test, dan -v menampilkan detail setiap test case yang dijalankan — keduanya opsional, tapi sering dipakai di CI pipeline untuk memantau kualitas test secara berkelanjutan.
Dockerfile untuk Production (Multi-Stage Build)
Kenapa Perlu Multi-Stage Build?
Kalau Dockerfile development langsung dipakai di production, tiga masalah muncul sekaligus. Image jadi besar karena masih membawa seluruh toolchain Go. Banyak tool seperti air ikut terbawa padahal tidak pernah dipakai saat runtime. Surface attack juga jadi lebih luas — semakin banyak binary dan tool yang ada di dalam image, semakin banyak potensi celah keamanan yang harus dijaga.
Solusi untuk ketiga masalah ini adalah multi-stage build: proses build dan proses runtime dipisah jadi dua stage berbeda, dan hanya hasil akhir (binary) yang dibawa ke image final.
flowchart TD
A[Stage: builder<br/>golang:1.21-alpine] --> B[go mod download]
B --> C[go build -o app]
C --> D[Binary app dihasilkan]
D -.->|COPY --from=builder| E[Stage: runtime<br/>alpine:3.19]
E --> F[Image final: hanya binary + alpine]Contoh Dockerfile Production
# Stage build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app
# Stage runtime
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app/app .
EXPOSE 8080
CMD ["./app"]
Stage pertama (builder) menggunakan golang:1.21-alpine — varian Alpine dari image Go yang lebih ringan dibanding image default — untuk mengompilasi binary. CGO_ENABLED=0 memastikan binary dikompilasi secara statis tanpa dependency ke C library sistem, sehingga bisa langsung dijalankan di base image minimal seperti Alpine tanpa error “shared library not found”.
Stage kedua (runtime) mulai dari image alpine:3.19 yang bersih, lalu hanya menyalin satu file: binary hasil build dari stage builder melalui instruksi COPY --from=builder. Tidak ada source code, tidak ada Go toolchain, tidak ada dependency manager — hanya binary yang siap dijalankan.
Hasil dari pendekatan ini:
- Image jauh lebih kecil dibanding Dockerfile single-stage
- Tidak ada compiler Go yang terbawa ke runtime
- Lebih aman karena surface attack lebih kecil, dan lebih cepat saat di-pull atau di-deploy
CGO_ENABLED=0 di sini bukan opsional. Tanpa flag ini, binary biasanya tetap ter-link secara dinamis ke glibc, dan akan gagal dijalankan di Alpine yang memakai musl libc — errornya biasanya muncul sebagai “no such file or directory” yang membingungkan karena binary-nya sebenarnya ada, hanya saja dynamic linker-nya tidak ditemukan. Mengompilasi secara statis (CGO_ENABLED=0) menghilangkan dependency tersebut sepenuhnya.
Sebagai alternatif dari alpine:3.19, base image scratch atau gcr.io/distroless/static juga bisa dipakai untuk stage runtime. Keduanya bahkan lebih minimal dari Alpine — scratch benar-benar kosong tanpa shell atau package manager sama sekali. Trade-off-nya adalah debugging jadi lebih sulit karena tidak ada shell untuk masuk ke dalam container (docker exec tidak akan berfungsi tanpa shell), sehingga Alpine sering jadi pilihan tengah yang lebih praktis: cukup kecil, tapi masih punya sh untuk keperluan debugging darurat.
Dockerfile untuk Staging
Staging sering diperlakukan seolah-olah sama dengan testing atau bahkan local dev, padahal tujuannya berbeda. Staging ada untuk memvalidasi bahwa aplikasi berjalan benar dalam kondisi yang semirip mungkin dengan production — termasuk image yang dipakai, ukuran resource, dan konfigurasi environment variable. Kalau staging memakai Dockerfile yang berbeda jauh dari production, bug yang muncul di production bisa jadi tidak pernah muncul di staging, dan staging kehilangan fungsinya sebagai tahap validasi terakhir sebelum rilis.
Pendekatan paling aman untuk staging adalah memakai Dockerfile multi-stage yang sama dengan production, dengan perbedaan hanya pada nilai environment variable atau image tag yang dipakai saat deploy:
# Dockerfile.staging — identik dengan production,
# perbedaan ada di environment variable saat runtime
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app/app .
ENV APP_ENV=staging
EXPOSE 8080
CMD ["./app"]
Satu-satunya tambahan dari versi production adalah ENV APP_ENV=staging, yang dibaca aplikasi untuk menentukan konfigurasi mana yang dipakai — misalnya koneksi ke database staging, bukan database production. Dengan menjaga struktur Dockerfile tetap identik antara staging dan production, kamu memastikan bahwa apa yang divalidasi di staging benar-benar merepresentasikan apa yang akan terjadi setelah rilis ke production.
Menggabungkan dengan Docker Compose
Di level project, setiap stage biasanya dipasangkan dengan file Compose yang berbeda, supaya konfigurasi tiap environment tetap terpisah dan tidak saling bercampur:
docker-compose.dev.ymldocker-compose.test.ymldocker-compose.prod.yml
Berikut contoh konfigurasi untuk local dev, yang memanfaatkan volume mount agar hot reload dari bagian sebelumnya benar-benar berfungsi:
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app
ports:
- "8080:8080"
Baris volumes: - .:/app inilah yang menghubungkan kode di host dengan kode di dalam container secara real-time. Begitu file di host berubah, air di dalam container langsung mendeteksinya dan me-restart aplikasi. Pendekatan ini menjaga separation of concern antar environment — konfigurasi dev tidak akan pernah bocor ke prod, dan sebaliknya.
Untuk staging dan production, file Compose-nya jauh lebih sederhana karena tidak ada volume mount yang perlu dijaga — image sudah berisi binary final, jadi tidak ada alasan untuk mount source code dari host:
# docker-compose.prod.yml
services:
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- APP_ENV=production
restart: unless-stopped
Perhatikan tidak ada bagian volumes di konfigurasi production — ini disengaja. Production harus menjalankan persis apa yang sudah di-build ke dalam image, tanpa kemungkinan file di host secara tidak sengaja menimpa isi container. restart: unless-stopped juga jadi tambahan penting di production, supaya container otomatis kembali jalan kalau crash atau host di-restart, sesuatu yang biasanya tidak diperlukan saat local dev.
Kesalahan Umum yang Harus Dihindari
✗ Satu Dockerfile dipakai untuk semua stage (dev, test, staging, production)
✗ Mengoptimasi Dockerfile dev seperti production (multi-stage, minim layer)
✗ Menggunakan tag "latest" sebagai base image
✗ Menjalankan tool hot reload (seperti air) di production
Keempat kesalahan ini punya akar yang sama: mencampur kebutuhan stage yang berbeda ke dalam satu konfigurasi. Berikut konteks tambahan untuk masing-masing:
Satu Dockerfile untuk semua stage biasanya berawal dari niat baik — “biar simpel, satu Dockerfile saja cukup”. Hasilnya, Dockerfile itu jadi kompromi yang tidak optimal untuk stage manapun: terlalu berat untuk local dev, terlalu riskan untuk production.
Mengoptimasi Dockerfile dev seperti production adalah kebalikannya — developer menerapkan multi-stage build bahkan untuk local dev, padahal multi-stage build berarti setiap perubahan kode butuh rebuild penuh dari awal. Ini justru menghilangkan keuntungan hot reload yang seharusnya didapat di stage ini.
Tag latest terlihat praktis karena selalu mengambil versi terbaru, tapi ini justru membuat build tidak reproducible. Image yang berhasil di-build hari ini bisa gagal di-build besok kalau maintainer base image merilis versi baru yang mengubah behaviour secara tidak terduga. Selalu kunci versi secara eksplisit, misalnya golang:1.21-alpine bukan golang:latest.
Hot reload di production seperti air menambahkan proses watcher yang terus berjalan di background, memonitor filesystem untuk perubahan yang di production tidak akan pernah terjadi — source code di image production bersifat immutable. Proses watcher ini hanya membuang resource CPU dan memori tanpa manfaat apa pun.
Ringkasan
- Local dev memprioritaskan kecepatan — pakai volume mount dan tool hot reload seperti
air, jangan optimasi ukuran image.- Testing memprioritaskan konsistensi — jalankan
go test ./...di dalam container yang environment-nya sama dengan CI.- Staging sebaiknya mendekati production — gunakan multi-stage build supaya perilaku staging mencerminkan production.
- Production memprioritaskan ukuran dan keamanan — multi-stage build dengan base image minimal seperti Alpine, dan
CGO_ENABLED=0untuk binary statis.- Pisahkan Dockerfile per stage (
Dockerfile.dev,Dockerfile.test, dst.) dan pasangkan masing-masing dengan file Docker Compose yang sesuai.- Hindari base image bertag
latestagar build tetap reproducible.- Jangan bawa tool development (seperti
air) ke image production — itu memperbesar attack surface tanpa manfaat di runtime.- Selalu pakai
.dockerignoreagarCOPY . .tidak ikut menyalin.git,.env, atau file sensitif lainnya ke dalam image.