Production Grade #

Dockerfile yang production grade bukan sekadar file yang bisa menghasilkan image yang bisa dijalankan. Ia adalah fondasi keamanan, performa, stabilitas, dan efisiensi operasional aplikasi di environment nyata — CI/CD pipeline, container registry, orchestrator (Kubernetes, ECS, Nomad), hingga observability dan incident response. Dockerfile yang ditulis asal mungkin bekerja di laptop, tapi di production ia akan jadi sumber incident yang tidak perlu.

Artikel ini tidak terikat pada bahasa pemrograman tertentu. Setiap bahasa (Go, Java, Rust, Node.js, Python, Ruby, PHP) punya karakteristik dan tooling masing-masing, dan kamu sudah punya artikel terpisah untuk masing-masing. Fokus artikel ini adalah prinsip universal yang harus dipahami sebelum menyentuh Dockerfile bahasa apapun: apa saja yang harus ada, kenapa penting, dan apa konsekuensinya jika diabaikan.

Apa yang Dimaksud “Production Grade”? #

Production grade Dockerfile adalah Dockerfile yang memenuhi lima kriteria secara bersamaan.

Aman secara default. Image tidak menyimpan rahasia, tidak berjalan sebagai root, menggunakan base image terpercaya, dan meminimalkan attack surface. Security bukan afterthought — ia adalah prasyarat.

Efisien secara biaya. Image sekecil mungkin, layer sekecil mungkin, cache build seoptimal mungkin. Setiap MB tambahan = biaya registry, bandwidth, dan cold start yang lebih tinggi.

Deterministik. Build yang sama, dengan input yang sama, harus menghasilkan image yang sama. Tidak ada “di mesin saya beda”, tidak ada dependency pada latest tanpa kontrol.

Mudah dioperasikan. Image punya healthcheck, logging ke STDOUT, signal handling yang benar, dan konfigurasi lewat environment variable. Operator tidak perlu SSH ke container untuk debugging.

Siap di-orkestrasi. Image bisa dijalankan di Kubernetes atau orchestrator lain tanpa modifikasi. Ia stateless, immutable, dan mengikuti契约 operasional platform.

Production grade bukan soal menulis Dockerfile yang “cukup jalan”. Ia soal menulis Dockerfile yang aman, stabil, dan murah dijalankan dalam jangka panjang — bukan hanya di deployment pertama, tapi di tahun kedua, ketiga, dan seterusnya.


Image Kecil: Fitur, Bukan Optimasi #

Ukuran image sering dianggap sebagai masalah “optimasi” yang bisa dilakukan nanti. Padahal image kecil adalah fitur fundamental yang memengaruhi banyak hal lain.

Waktu build CI. CI pipeline yang membangun image 1 GB akan lebih lambat dari yang membangun image 100 MB. Ini adalah biaya berulang setiap kali ada commit.

Waktu pull. Di Kubernetes atau cluster manapun, node baru harus menarik image dari registry. Image besar = autoscaling lambat = incident response lambat.

Biaya registry dan bandwidth. Image besar = biaya penyimpanan dan transfer yang lebih tinggi. Ini kecil per image, tapi signifikan dalam skala production.

Cold start. Untuk serverless container (AWS Fargate, Cloud Run, Lambda container) atau autoscaling agresif, image besar = cold start yang lambat = user experience buruk.

Attack surface. Image besar biasanya membawa lebih banyak package, library, dan tool. Setiap package tambahan adalah potensi vulnerability. Image kecil = lebih sedikit CVE yang harus ditrack.

flowchart LR
    A[Image Besar] --> B[Pull Lambat]
    A --> C[Cold Start Lambat]
    A --> D[Attack Surface Luas]
    A --> E[Biaya Registry Naik]
    A --> F[Cache Build Kurang Efisien]
    B & C & D & E & F --> G[Operasional Mahal]
Penting: Image besar bukan sekadar “boros biaya” — ia juga memperbesar blast radius saat terjadi vulnerability. Jika base image yang kamu pakai punya CVE, image kecil dengan audit dependency yang ketat akan lebih cepat di-patch daripada image besar yang masih punya banyak package “warisan” yang tidak dipakai.

Prinsip yang harus dipegang: setiap MB tambahan adalah risiko, dan setiap dependency yang tidak dipakai adalah bug yang belum ditemukan.


Multi-Stage Build: Standar Wajib #

Multi-stage build bukan trik optimasi — ia adalah pemisahan concern yang menjadi standar wajib di Dockerfile production.

Konsepnya: pisahkan build environment dari runtime environment. Stage pertama punya compiler, package manager, dan tool build. Stage kedua (yang menjadi image akhir) hanya punya artefak yang dibutuhkan untuk runtime.

# Stage 1: build dengan compiler
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o app

# Stage 2: runtime tanpa compiler
FROM gcr.io/distroless/base-debian12
COPY --from=builder /app/app /app
CMD ["/app"]

Mengapa wajib? Karena tanpa multi-stage, image runtime akan membawa compiler, source code, dan build cache. Ini menambah ukuran image, menambah attack surface, dan membuat image lebih lambat untuk di-deploy.

Rule of thumb: Jika Dockerfile kamu hanya punya satu stage, itu sinyal merah. Bukan berarti selalu salah, tapi hampir selalu ada ruang untuk perbaikan.

Manfaat multi-stage build:

  • Image runtime jauh lebih kecil — compiler dan tool build tidak ikut.
  • Tidak ada compiler di production — mengurangi risiko supply chain attack.
  • Boundary yang jelas — perbedaan antara “dibutuhkan untuk build” dan “dibutuhkan untuk runtime” terlihat eksplisit di Dockerfile.
  • Cross-compilation lebih mudah — stage build bisa target platform berbeda dari stage runtime.

Non-Root User: Default, Bukan Opsional #

Secara default, container berjalan sebagai root (UID 0). Ini berbahaya. Jika attacker berhasil melakukan container escape, mereka mendapat akses root di host. Semakin tinggi privilege di container, semakin besar dampaknya.

// ✗ Anti-pattern: container jalan sebagai root
FROM alpine
COPY app /app
CMD ["/app"]

// ✓ Benar: container jalan sebagai user non-root
FROM alpine
RUN adduser -D -u 1001 appuser
COPY --chown=appuser:appuser app /app
USER appuser
CMD ["/app"]

Manfaat non-root user:

  • Mengurangi dampak container escape — attacker tidak langsung mendapat root host.
  • Compliance — banyak standar keamanan (PCI-DSS, SOC2) mengharuskan non-root container.
  • Least privilege — container hanya punya permission yang dibutuhkan.
  • Volume mount lebih aman — file di-mount dengan permission user container, bukan root.

Best practice:

  • Buat user eksplisit dengan UID tetap (>= 1000) untuk konsistensi.
  • Gunakan --chown di COPY agar file langsung dimiliki user yang benar.
  • Untuk distroless image, gunakan user nonroot:nonroot yang sudah tersedia.
  • Hindari UID random — bisa menyebabkan masalah permission pada volume yang di-share.

Base Image Minimal dan Terpercaya #

Base image menentukan fondasi image kamu. Ia menentukan security posture, ukuran, dan dependency implisit yang akan kamu bawa.

Prinsip pemilihan:

  • Gunakan image resmi dari publisher terpercaya. Docker Official Image, Distroless, atau image dari CNCF project.
  • Hindari image random dari publisher yang tidak jelas, apalagi image “all-in-one” yang membawa banyak hal.
  • Pilih varian yang sesuai — alpine, slim, atau distroless, tergantung kebutuhan.
  • Pin versi spesifikgolang:1.22.5-alpine3.19, bukan golang:latest.
flowchart TD
    A[Butuh shell untuk debugging?] -->|Ya| B[Alpine / Slim]
    A -->|Tidak| C[Distroless / Scratch]
    B --> D[Trade-off: Ukuran vs Observability]
    C --> E[Trade-off: Keamanan vs Kemudahan Debug]
    D --> F[Default untuk Aplikasi Umum]
    E --> G[Default untuk High-Security / Production Mature]

Distroless image (gcr.io/distroless/*) adalah pilihan terbaik untuk production. Image ini hanya membawa runtime yang dibutuhkan aplikasi (misal: glibc, CA certs, Java JRE) tanpa shell, tanpa package manager, tanpa utilitas OS. Attack surface minimal, ukuran kecil, dan kamu dipaksa untuk mengandalkan logging/observability yang proper.

Trade-off distroless: Tidak bisa docker exec -it container sh untuk debugging interaktif. Kamu harus mengandalkan log, metrics, dan tracing. Ini fitur, bukan bug — ia memaksa disiplin observability.


Deterministic dan Reproducible Build #

Production Dockerfile harus deterministik: input yang sama menghasilkan output yang sama. Tidak boleh ada “tapi kemarin build-nya jalan”.

Anti-pattern:

# Tag tanpa versi: bisa berubah sewaktu-waktu
FROM node:latest
FROM ubuntu:latest
FROM python:3

# Install tanpa pin versi
RUN apt-get install -y curl
RUN pip install flask

Best practice:

# Tag eksplisit dengan patch version
FROM node:20.11.1-alpine
FROM ubuntu:24.04
FROM python:3.12.3-slim

# Install dengan versi eksplisit
RUN apt-get install -y curl=7.81.0-1ubuntu1
RUN pip install flask==3.0.3

Mengapa penting?

  • Security audit — kamu bisa cek image hasil build, identifikasi CVE berdasarkan versi dependency yang tepat.
  • Rollback — versi yang sama di-build ulang akan menghasilkan image yang sama persis.
  • Compliance — beberapa standar (FedRAMP, ISO 27001) mengharuskan reproducibility.
  • Debugging — incident di production bisa di-reproduce di environment development.

Tools pendukung:

  • pip freeze / pip-compile untuk Python.
  • npm ci (bukan npm install) untuk Node.js.
  • go.sum untuk Go.
  • Cargo.lock untuk Rust.
  • composer.lock untuk PHP.
  • Gemfile.lock untuk Ruby.
Catatan: Lock file saja tidak cukup. Kamu juga harus memastikan base image punya tag yang eksplisit. Image yang dibuild dari FROM node:20 dan FROM node:20.11.1 akan berbeda ketika node:20 di-resolve ke versi patch yang lebih baru.

Layering yang Disengaja #

Setiap instruksi Dockerfile menghasilkan satu layer. Layer yang tidak disengaja akan menumpuk dan membuat image membengkak.

Prinsipnya:

  • Yang jarang berubah di atas — base image, OS dependency.
  • Dependency aplikasi di tengah — package manager install.
  • Source code di bawah — kode aplikasi yang paling sering berubah.
// ✓ Urutan yang memaksimalkan cache
FROM node:20-alpine          # Layer 1: base image (jarang berubah)
WORKDIR /app                  # Layer 2: struktur (jarang berubah)
RUN apk add --no-cache curl  # Layer 3: OS dep (jarang berubah)
COPY package*.json ./         # Layer 4: dependency file (jarang berubah)
RUN npm install               # Layer 5: dependency (jarang berubah)
COPY . .                      # Layer 6: source code (sering berubah)
CMD ["npm", "start"]          # Layer 7: entrypoint (jarang berubah)

Dengan urutan ini, ketika kamu mengubah source code (Layer 6), Docker hanya perlu rebuild Layer 6 dan seterusnya. Layer 1-5 di-reuse dari cache. Build menjadi jauh lebih cepat.

Prinsip Dockerfile sebagai build graph: Dockerfile bukan script linear yang dieksekusi berurutan tanpa peduli output. Ia adalah dependency graph yang bisa dioptimasi. Setiap instruksi adalah node, dan Docker menentukan node mana yang bisa di-skip dari cache.


Konfigurasi via Environment, Bukan Hardcode #

Production container harus stateless dan portable antar environment. Cara mencapainya: konfigurasi lewat environment variable, bukan hardcode di image.

// ✗ Anti-pattern: konfigurasi hardcode
ENV DB_HOST=production-db.example.com
ENV LOG_LEVEL=info

// ✓ Benar: default value, override saat runtime
ENV DB_HOST=localhost
ENV LOG_LEVEL=info

Override saat runtime:

docker run -e DB_HOST=staging-db -e LOG_LEVEL=debug myapp

Prinsip: Image yang berbeda untuk dev, staging, dan production adalah bau arsitektur. Satu image harus bisa jalan di semua environment dengan konfigurasi yang berbeda.

Yang harus ada di env variable:

  • Database host, port, credential.
  • API endpoint.
  • Log level.
  • Feature flag.
  • Cache TTL.
  • Listening port.

Yang TIDAK boleh ada di image:

  • Credential (password, API key, token).
  • Environment-specific URL.
  • Debug flag yang selalu aktif.
  • File konfigurasi yang berbeda per environment.

Build-time vs runtime secret:

  • Build-time secret — gunakan BuildKit secret mount (--mount=type=secret). Tersedia saat build, tidak tersimpan di image.
  • Runtime secret — mount dari orchestrator (Kubernetes secret, Docker secret, Vault) atau env variable dari secret manager.

Logging ke STDOUT/STDERR #

Container production tidak boleh menulis log ke file lokal. Kenapa?

  • File log di container akan hilang saat container di-restart (immutable filesystem).
  • File log di container sulit diakses dari luar.
  • Orchestrator sudah punya mekanisme untuk menangkap log dari STDOUT/STDERR.
// ✗ Anti-pattern: log ke file
CMD ["./app", "--log-file=/var/log/app.log"]

// ✓ Benar: log ke STDOUT/STDERR (default)
CMD ["./app"]

Aplikasi yang ditulis dengan benar akan menulis log ke STDOUT/STDERR secara default. Beberapa framework butuh konfigurasi eksplisit:

  • Java — set logback ke STDOUT.
  • Python — gunakan logging dengan StreamHandler(sys.stdout).
  • Node.jsconsole.log dan console.error sudah ke STDOUT/STDERR.
  • Go — log ke os.Stdout dan os.Stderr.

Format log yang baik:

  • Plain text untuk development (mudah dibaca manusia).
  • JSON structured log untuk production (mudah di-parse oleh log aggregator).
Tips: Gunakan 12-factor app methodology untuk logging. Aplikasi tidak boleh mengelola log file sendiri — biarkan orchestrator yang mengumpulkan log dari STDOUT/STDERR.

Healthcheck: Kontrak Operasional #

HEALTHCHECK adalah cara image berkomunikasi dengan orchestrator tentang kondisi dirinya. Tanpa healthcheck, orchestrator hanya tahu container “running” (proses ada), bukan “healthy” (siap melayani request).

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Prinsip healthcheck yang baik:

  • Cepat — healthcheck bisa berjalan setiap 10-30 detik, harus selesai dalam 1-3 detik.
  • Merepresentasikan kesiapan — cek bahwa aplikasi benar-benar siap melayani request, bukan hanya “proses hidup”.
  • Tidak berat — jangan query database besar atau hit endpoint yang mahal.
  • Deterministik — output harus konsisten: healthy atau unhealthy, tidak pernah flaky.

Untuk distroless image (tanpa curl/wget):

  • Buat healthcheck yang dicek dari dalam aplikasi.
  • Atau gunakan TCP-only check (cek apakah port bisa di-connect).
  • Atau gunakan orchestrator-native probe (Kubernetes liveness/readiness probe yang terpisah dari Docker HEALTHCHECK).

Tiga kondisi yang harus dibedakan:

  • Liveness — apakah container perlu di-restart? Cek apakah proses masih berfungsi.
  • Readiness — apakah container siap menerima traffic? Cek apakah dependency sudah siap.
  • Startup — apakah aplikasi sudah selesai inisialisasi? Cek apakah first-start sudah selesai.

HEALTHCHECK di Dockerfile biasanya untuk liveness/readiness sederhana. Untuk kontrol yang lebih granular, gunakan probe orchestrator.


Graceful Shutdown dan Signal Handling #

Production container harus bisa di-shutdown dengan rapi saat menerima SIGTERM (signal default dari orchestrator saat scaling down atau restart). Shutdown yang tidak rapi menyebabkan:

  • Request yang sedang diproses terputus tiba-tiba.
  • Koneksi database yang tidak ditutup.
  • Transaksi yang setengah-setengah.
  • File lock yang tidak dilepas.
// ✗ Anti-pattern: shell form makan signal
CMD npm start
# npm jadi child process dari sh, SIGTERM tidak sampai ke npm

// ✓ Benar: exec form, signal lewat ke proses utama
CMD ["npm", "start"]

Prinsip signal handling:

  • Exec form di CMD/ENTRYPOINT — pastikan proses utama adalah PID 1, agar signal sampai langsung.
  • Trap signal di aplikasi — aplikasi harus handle SIGTERM dengan benar (close connection, flush log, exit dengan code 0).
  • Grace period — orchestrator biasanya memberi waktu 30 detik sebelum SIGKILL. Aplikasi harus selesai shutdown dalam waktu itu.

Implementasi umum:

  • Node.js — handle SIGTERM dengan process.on('SIGTERM', ...).
  • Go — default sudah benar, tapi pastikan defer untuk cleanup.
  • Java — tambahkan JVM shutdown hook.
  • Python — handle SIGTERM dengan signal.signal(signal.SIGTERM, ...).
  • Rust — handle SIGTERM dengan signal handler crate.

HEALTHCHECK yang baik juga membantu — saat shutdown, orchestrator biasanya mengirim SIGTERM dulu, menunggu, lalu SIGKILL. Aplikasi harus berhenti menerima request baru (readiness fail), lalu shutdown dengan rapi.


Security Scanning dan Vulnerability Awareness #

Production image harus di-scan secara rutin untuk vulnerability. Image yang tidak pernah di-scan adalah image yang menunggu untuk dieksploitasi.

Tool yang umum dipakai:

  • Trivy — open source, support banyak format image.
  • Snyk — komersial, dengan database CVE yang luas.
  • Docker Scout — built-in di Docker Hub dan Docker Desktop.
  • Grype — open source dari Anchore.
  • Clair — open source dari Red Hat.

Integrasi ke CI/CD:

# Contoh GitHub Actions
- name: Build image
  run: docker build -t myapp:${{ github.sha }} .

- name: Scan image
  run: trivy image --severity HIGH,CRITICAL myapp:${{ github.sha }}

- name: Fail if critical
  run: trivy image --exit-code 1 --severity CRITICAL myapp:${{ github.sha }}

Apa yang di-scan:

  • OS package — CVE di apt, apk, yum packages.
  • Application dependency — CVE di pip, npm, gem, composer packages.
  • Base image — pastikan base image yang kamu pilih juga up-to-date.

Pola pikir: Image bukan hanya hasil build, ia adalah artefak deployable yang punya security posture sendiri. Ia harus ditrack, di-scan, dan di-patch seperti artefact software lainnya.


Image yang Benar untuk Orchestrator #

Orchestrator (Kubernetes, ECS, Nomad) punya konvensi yang harus diikuti agar image bisa berjalan dengan baik.

Pattern yang diharapkan:

  • Stateless — tidak ada state lokal, semua konfigurasi lewat env.
  • Idempotent start — container yang di-restart harus langsung berfungsi, tanpa setup manual.
  • Horizontal scalable — N container harus bisa jalan tanpa konflik.
  • Graceful shutdown — handle SIGTERM dengan benar.
  • Log ke STDOUT — agar log bisa dikumpulkan orchestrator.
  • Healthcheck — agar orchestrator tahu kapan container siap.

Anti-pattern:

  • Menjalankan banyak proses dalam satu container — orchestrator mengelola proses, bukan service. Satu container = satu proses utama.
  • Menggunakan supervisor — process manager tradisional tidak cocok untuk container. Orchestrator sudah jadi process manager.
  • Treat container seperti VM — install banyak service, simpan state di filesystem. Ini model lama yang harus ditinggalkan.

Prinsip: Container adalah unit aplikasi, bukan unit server. Orchestrator mengelola lifecycle-nya. Image harus didesain untuk model ini.


Anti-Pattern yang Harus Dihindari #

Beberapa kesalahan yang paling umum di Dockerfile production:

Anti-Pattern Dampak Solusi
Base image latest Build tidak deterministik Pin tag eksplisit
Secret di image Credential bocor Inject saat runtime
Container jalan sebagai root Risiko privilege escalation User non-root
Satu stage build Image bawa compiler Multi-stage build
Tidak ada .dockerignore Context membengkak Filter dengan .dockerignore
Log ke file Log hilang saat restart Log ke STDOUT
Shell form di CMD/ENTRYPOINT Signal handling rusak Exec form
Healthcheck tidak ada Orchestrator buta HEALTHCHECK di image atau probe di orchestrator
Tag tanpa strategi Sulit rollback Semantic version atau commit hash
Tidak ada scan CVE tidak terdeteksi Scan di CI/CD

Kesamaan dari semua anti-pattern: bekerja di awal, menyakitkan di belakang. Semua terlihat “cukup jalan” sampai production, sampai incident, sampai security audit.


Ringkasan #

  • Production grade bukan soal syntax — ia soal cara berpikir: aman, efisien, deterministik, mudah dioperasikan, dan siap di-orkestrasi.
  • Image kecil adalah fitur, bukan optimasi. Setiap MB tambahan = biaya, waktu, dan attack surface.
  • Multi-stage build adalah standar wajib untuk memisahkan build environment dari runtime environment.
  • Non-root user harus jadi default, bukan opsional. Buat user eksplisit dengan UID tetap.
  • Base image harus minimal dan terpercaya. Distroless adalah pilihan terbaik untuk production mature.
  • Deterministic build wajib: pin versi base image, lock dependency, hindari latest.
  • Konfigurasi via environment, bukan hardcode. Credential tidak boleh di image.
  • Log ke STDOUT/STDERR, bukan file. Biarkan orchestrator yang mengumpulkan log.
  • Healthcheck adalah kontrak operasional dengan orchestrator. Tanpa healthcheck, orchestrator buta.
  • Signal handling harus benar: exec form di CMD/ENTRYPOINT, handle SIGTERM di aplikasi.
  • Scan vulnerability di CI/CD untuk setiap image yang akan di-deploy.

← Sebelumnya: Struktur Dockerfile   Berikutnya: Ukuran Image →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact