Struktur Dockerfile #

Dockerfile terlihat seperti deretan perintah sederhana, dan memang begitulah adanya. Tapi di balik kesederhanaannya, ada struktur yang menentukan apakah image yang dihasilkan kecil dan aman, atau besar dan rapuh. Setiap instruksi punya peran spesifik, dan urutan penulisannya bukan kebetulan — ia adalah build graph yang menentukan performa, ukuran, dan maintainability image.

Memahami struktur Dockerfile bukan hanya soal bisa menulis image. Ia adalah soal memahami bagaimana setiap instruksi berinteraksi dengan sistem layer, bagaimana cache bekerja, dan bagaimana keputusan kecil di Dockerfile bisa berdampak besar di production. Backend engineer, DevOps, dan platform engineer yang paham struktur Dockerfile bisa membangun image yang reproducible, kecil, dan aman — tanpa perlu trial and error yang mahal.

Artikel ini membahas setiap instruksi penting yang umum dipakai, peran spesifiknya, perbedaan instruksi yang sering tertukar (CMD vs ENTRYPOINT, COPY vs ADD, ARG vs ENV), dan praktik menyusun Dockerfile yang bersih.

Anatomi Umum Dockerfile #

Sebuah Dockerfile pada dasarnya adalah daftar instruksi berurutan yang dieksekusi oleh Docker daemon saat docker build berjalan. Setiap instruksi menghasilkan satu layer di image. Layer bersifat read-only, dan layer baru di atasnya bisa me-replace file dari layer di bawahnya (tapi tidak menghapus layer itu sendiri — semua layer tetap ada sampai image di-prune).

flowchart TD
    A[FROM: Base Image] --> B[ARG: Build-time Variable]
    B --> C[ENV: Runtime Variable]
    C --> D[WORKDIR: Direktori Kerja]
    D --> E[COPY/ADD: Salin File]
    E --> F[RUN: Eksekusi Perintah]
    F --> G[EXPOSE: Dokumentasi Port]
    G --> H[USER: User Runtime]
    H --> I[ENTRYPOINT/CMD: Startup]

Urutan di atas adalah urutan logis yang umum. Tidak semua instruksi wajib, dan beberapa bisa diulang (terutama FROM di multi-stage build). Yang wajib adalah FROM (kecuali Dockerfile kosong, yang tidak masuk akal) dan minimal satu CMD atau ENTRYPOINT agar container tahu apa yang harus dijalankan.

Hal yang perlu kamu pahami sejak awal: setiap baris Dockerfile adalah satu layer. Jika kamu menulis 20 baris RUN, hasilnya adalah 20 layer. Jika kamu menggabungkan 20 perintah dalam satu RUN dengan &&, hasilnya adalah 1 layer. Layer yang lebih banyak = image yang lebih besar dan waktu pull yang lebih lama. Tapi layer yang terlalu agresif digabung = cache yang kurang granular. Ada trade-off yang harus dipahami.


FROM — Base Image #

Fungsi #

FROM menentukan base image yang menjadi pondasi image yang kamu bangun. Ia wajib menjadi instruksi pertama yang non-komentar di Dockerfile (kecuali ada ARG khusus yang dipakai di FROM).

FROM node:20-alpine

node:20-alpine artinya: ambil image node versi 20 dengan varian alpine (Linux minimalis). Docker menarik image ini dari registry, dan semua instruksi setelahnya dijalankan di atas filesystem image ini.

Aturan Penting #

Tag eksplisit, bukan latest. Selalu tentukan tag versi pada base image. FROM node:latest adalah anti-pattern: kamu tidak pernah tahu versi persis yang akan kamu dapat, dan build yang “berhasil” hari ini bisa berbeda besok.

// ✗ Anti-pattern: tidak deterministik
FROM node:latest

// ✓ Benar: pin versi dan varian
FROM node:20.11-alpine

Varian base image sangat berpengaruh. Image resmi biasanya punya beberapa varian:

Varian Ukuran Catatan
-alpine Kecil musl libc, kadang ada masalah kompatibilitas
-slim Sedang Debian minimal, glibc
default Besar OS lengkap dengan banyak utilitas
-distroless Sangat kecil Tanpa shell, tanpa package manager

Multi-stage build menggunakan FROM lebih dari sekali. Ini adalah pola yang sangat umum dan akan dibahas lebih detail nanti, tapi intinya: FROM kedua biasanya adalah base image yang lebih kecil untuk runtime.

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

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

Penting untuk memberikan nama pada stage build (AS builder) agar bisa diacu oleh stage lain dengan COPY --from=builder.


ARG — Build-time Variable #

Fungsi #

ARG mendefinisikan variable yang hanya tersedia saat proses build — di dalam FROM, RUN, LABEL, dan instruksi build-time lainnya. Variable ini tidak tersedia di container runtime.

ARG GO_VERSION=1.22
FROM golang:${GO_VERSION}-alpine

Nilai GO_VERSION bisa di-override saat build dengan flag --build-arg:

docker build --build-arg GO_VERSION=1.21 -t myapp .

Karakteristik #

  • Scope: hanya di-instruksi setelah deklarasi ARG, sampai akhir build (atau FROM baru).
  • Bisa di-override: lewat --build-arg atau nilai default setelah =.
  • Tidak aman untuk secret: nilai ARG akan terlihat di docker history setelah image dibuild.
// ANTI-PATTERN: secret di ARG
ARG DB_PASSWORD=supersecret
RUN echo "password=$DB_PASSWORD" >> /app/config

// BENAR: secret di-mount saat runtime atau lewat secret manager
ARG DB_PASSWORD  # nilai default kosong

Argumen ARG sangat berguna untuk parameter build yang boleh berubah-ubah antar build, seperti versi runtime, target platform, atau flag build. Tapi ia bukan tempat untuk menyimpan konfigurasi runtime atau credential.


ENV — Runtime Environment Variable #

Fungsi #

ENV mendefinisikan environment variable yang tersedia saat container berjalan. Variable ini juga tersedia saat build (di RUN setelah deklarasi ENV), tapi tujuan utamanya adalah untuk runtime.

ENV NODE_ENV=production
ENV APP_PORT=8080

Nilai NODE_ENV=production akan tersedia di dalam container, dan aplikasi Node.js akan otomatis membaca variabel ini untuk optimasi (misal: menghilangkan source map, mengaktifkan minification).

Perbedaan ARG vs ENV #

Aspek ARG ENV
Tersedia saat build Ya Ya
Tersedia saat runtime Tidak Ya
Di-override lewat --build-arg --env atau -e
Aman untuk secret Tidak Tidak
Tersimpan di image Tidak (default) Ya

ARG adalah variabel build, ENV adalah variabel runtime. Mereka sering dipakai bersamaan, tapi untuk tujuan yang berbeda.

ARG GO_VERSION=1.22
FROM golang:${GO_VERSION}-alpine

ENV APP_ENV=production \
    APP_PORT=8080

Pada contoh di atas, GO_VERSION hanya relevan saat build (memilih base image), sedangkan APP_ENV dan APP_PORT akan tetap ada di container yang berjalan.

Catatan tentang Secret #

Baik ARG maupun ENV tidak boleh dipakai untuk menyimpan credential. Keduanya akan tercatat di history image dan bisa di-inspect dengan docker history atau docker inspect. Untuk secret, gunakan:

  • Docker secrets (pada Docker Swarm)
  • Kubernetes secrets (di-mount sebagai volume atau env)
  • External secret manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager)
  • BuildKit secret mounts (--mount=type=secret) untuk build-time secret

WORKDIR — Direktori Kerja #

Fungsi #

WORKDIR menentukan direktori kerja untuk instruksi-instruksi setelahnya. Ia membuat path relatif menjadi eksplisit dan menghindari penggunaan cd yang污染 layer.

WORKDIR /app
COPY . .

Setelah WORKDIR /app, semua path relatif akan berdasarkan /app. COPY . . artinya: salin isi build context ke /app di image.

Praktik yang Baik #

Selalu gunakan WORKDIR, jangan cd di RUN.

// ✗ Anti-pattern: cd membuat layer baru
RUN cd /app && npm install

// ✓ Benar: WORKDIR lebih bersih dan reusable
WORKDIR /app
RUN npm install

RUN cd /app && npm install bekerja, tapi cd membuat layer tambahan dan membuat instruksi sulit dibaca. WORKDIR lebih eksplisit dan bisa diacu oleh banyak instruksi.

Bisa dipanggil berulang. Jika kamu perlu berpindah direktori, panggil WORKDIR lagi. Tidak ada efek samping.

WORKDIR /app
COPY package*.json ./
RUN npm install

WORKDIR /app/src
COPY . .
RUN npm run build

WORKDIR akan membuat direktori secara otomatis jika belum ada.


COPY — Menyalin File dari Build Context #

Fungsi #

COPY menyalin file dan direktori dari build context (lokasi docker build dijalankan) ke image.

COPY package.json package-lock.json ./
COPY . .

Bentuk paling umum:

  • COPY <src> <dest> — menyalin satu atau lebih file/direktori.
  • COPY --from=<stage> <src> <dest> — menyalin dari stage build lain (multi-stage).

Praktik yang Baik #

Copy dependency definition dulu, source code belakangan. Ini adalah aturan paling penting untuk cache.

// ✗ Anti-pattern: source code di atas, cache npm install selalu invalid
COPY . .
RUN npm install

// ✓ Benar: dependency dulu, source belakangan
COPY package*.json ./
RUN npm install
COPY . .

Gunakan .dockerignore. File ini menentukan apa yang tidak ikut terkirim ke Docker daemon. Tanpa .dockerignore, semua file di build context akan dikirim — termasuk node_modules lokal, .git, file rahasia, dan lain-lain.

# .dockerignore
node_modules
.git
.env
*.log
dist
build
.DS_Store

.dockerignore sama pentingnya dengan .gitignore. Ia mempercepat build, mengurangi ukuran context, dan mencegah kebocoran data sensitif.


ADD — COPY Plus Fitur Tambahan #

Fungsi Tambahan #

ADD punya dua kemampuan ekstra dibanding COPY:

  1. Ekstrak arsip tar otomatis — jika <src> adalah file .tar lokal, Docker akan mengekstraknya.
  2. Download dari URLADD bisa mengambil file dari URL.
ADD archive.tar.gz /app/
ADD https://example.com/file.txt /app/

Rekomendasi #

Gunakan COPY secara default, kecuali butuh fitur ADD.

Alasannya:

  • ADD dari URL tidak direkomendasikan — tidak ada validasi checksum, dan dependency ke URL eksternal membuat build tidak deterministik.
  • Ekstrak tar otomatis berguna, tapi bisa diganti dengan RUN tar -xzf archive.tar.gz -C /app/ yang lebih eksplisit.
  • COPY lebih jelas dibaca — COPY hanya menyalin, ADD punya dua perilaku.

Prinsip umumnya: gunakan instruksi yang paling sesuai dengan kebutuhanmu, dan hindari fitur yang tidak kamu pakai.


RUN — Eksekusi Perintah Saat Build #

Fungsi #

RUN menjalankan perintah di dalam image saat build. Hasilnya menjadi bagian dari image. Ini adalah instruksi utama untuk meng-install dependency, menjalankan setup, dan memodifikasi image.

RUN apt-get update && apt-get install -y curl

Bentuk Sintaks #

Ada dua bentuk: shell form dan exec form.

# Shell form — dijalankan dengan /bin/sh -c
RUN apt-get update && apt-get install -y curl

# Exec form — langsung dieksekusi, tanpa shell
RUN ["apt-get", "update", "&&", "apt-get", "install", "-y", "curl"]

Shell form lebih umum dan lebih mudah dibaca. Exec form berguna saat kamu perlu menghindari shell, misalnya untuk menghindari masalah signal handling.

Praktik yang Baik #

Gabungkan perintah yang berkaitan, dan hapus cache di layer yang sama.

// ✗ Anti-pattern: cache tidak dibersihkan, layer membengkak
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

// ✓ Benar: satu layer, cache dibersihkan di tempat yang sama
RUN apt-get update && apt-get install -y \
    curl \
    ca-certificates \
  && rm -rf /var/lib/apt/lists/*

Prinsip ini berlaku untuk semua package manager: apt, apk, yum, pip, npm. Cache harus dihapus dalam RUN yang sama dengan instalasi, karena layer berikutnya tidak bisa mengecilkan layer sebelumnya.

Setiap RUN = satu layer. Menggabungkan RUN = mengurangi jumlah layer = image lebih kecil. Tapi menggabungkan terlalu banyak = cache yang kurang granular. Trade-off ini harus dipahami.


EXPOSE — Dokumentasi Port #

Fungsi #

EXPOSE mendokumentasikan port yang digunakan aplikasi di dalam container. Ini hanya dokumentasiEXPOSE tidak membuka port ke host.

EXPOSE 8080

Untuk membuka port ke host, kamu tetap butuh flag -p di docker run atau konfigurasi ports di Docker Compose.

docker run -p 8080:8080 myapp

Mengapa Tetap Penting #

Walaupun EXPOSE tidak membuka port, ia punya beberapa fungsi penting:

  • Dokumentasi — pembaca Dockerfile langsung tahu port mana yang digunakan aplikasi.
  • Docker ComposeEXPOSE adalah hint untuk automatic port mapping.
  • Orchestrator — beberapa orchestrator (terutama yang lebih tua) membaca EXPOSE untuk konfigurasi internal.
  • Tooling — beberapa tool (seperti docker run -P) me-random-map semua EXPOSE ke host port.

USER — User Runtime #

Fungsi #

USER menentukan user yang digunakan untuk instruksi selanjutnya dan saat container berjalan. Default-nya adalah root (UID 0), yang merupakan risiko keamanan.

RUN adduser -D -u 1001 appuser
USER appuser

Mengapa Penting #

Menjalankan container sebagai root berbahaya. Jika attacker berhasil keluar dari container (container escape), mereka mendapat akses root di host. Semakin tinggi privilege di container, semakin besar dampaknya.

Prinsip least privilege: container seharusnya hanya punya permission yang dibutuhkan untuk menjalankan tugasnya, tidak lebih.

Pola Umum #

Buat user eksplisit dengan UID tetap. UID tetap penting untuk konsistensi permission di volume mount.

RUN addgroup -g 1001 -S appgroup \
 && adduser -u 1001 -S appuser -G appgroup
USER appuser

Distroless image sudah punya user nonroot. Jika kamu menggunakan gcr.io/distroless/base-debian12, cukup USER nonroot:nonroot.

FROM gcr.io/distroless/base-debian12
USER nonroot:nonroot
CMD ["/app"]

VOLUME — Deklarasi Mount Point #

Fungsi #

VOLUME mendeklarasikan mount point untuk data persisten di luar filesystem container. Ini memberitahu Docker bahwa direktori tersebut akan menyimpan data yang harus bertahan di luar lifecycle container.

VOLUME ["/data", "/logs"]

Karakteristik #

  • Data di dalam VOLUME tidak ikut di-commit saat image baru dibuild dari container.
  • Data di-mount ke host atau ke volume named secara default.
  • Deklarasi, bukan inisialisasiVOLUME hanya mendeklarasikan, data harus diisi saat runtime.

Dalam praktik, VOLUME di Dockerfile jarang dipakai. Lebih umum untuk mendeklarasikan volume di docker run -v atau docker-compose.yml. Tapi VOLUME berguna sebagai dokumentasi bahwa direktori tersebut butuh data persisten.


HEALTHCHECK — Kontrak Operasional #

Fungsi #

HEALTHCHECK menentukan cara Docker mengecek apakah container benar-benar sehat dan siap melayani request. Ini adalah instruksi yang sangat penting untuk environment terorkestrasi (Kubernetes, ECS, Swarm).

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

Parameter:

  • --interval — jarak antar cek (default 30s).
  • --timeout — batas waktu tiap cek (default 30s).
  • --start-period — waktu tunggu setelah container start (default 0s).
  • --retries — jumlah gagal berturut-turut sebelum status unhealthy (default 3).

Penting untuk Distroless #

Jika base image tidak punya curl atau wget (misal distroless), HEALTHCHECK tetap bisa jalan — tapi kamu harus pikirkan bagaimana cara mengeceknya. Alternatif:

  • Endpoint HTTP yang dicek dari dalam aplikasi (misal: aplikasi sendiri menulis health status ke file).
  • Pakai base image yang punya shell untuk healthcheck (tapi ini menambah ukuran image).
  • Cek dari luar dengan sidecar container (lebih kompleks, tapi benar-benar terpisah).
Catatan untuk distroless: Distroless image sengaja menghapus shell dan utility umum seperti curl. Jika kamu pakai HEALTHCHECK CMD curl ... di distroless, Docker akan error. Solusinya: gunakan endpoint yang dicek via TCP, atau pisahkan healthcheck logic ke sidecar.

ENTRYPOINT vs CMD #

Dua instruksi ini sering tertukar, padahal perannya berbeda. Keduanya menentukan apa yang dijalankan saat container start.

CMD #

CMD adalah default command yang bisa dengan mudah di-override saat docker run.

CMD ["npm", "start"]

Override di runtime:

docker run myapp npm test
# `npm test` menggantikan `npm start`

ENTRYPOINT #

ENTRYPOINT adalah executable utama yang lebih sulit di-override. Dia jadi program yang dijalankan di dalam container.

ENTRYPOINT ["python", "app.py"]

Override di runtime butuh flag --entrypoint:

docker run --entrypoint python myapp app.py --version

Kombinasi ENTRYPOINT + CMD #

Pola paling umum adalah menggabungkan keduanya: ENTRYPOINT sebagai executable utama, CMD sebagai default argument.

ENTRYPOINT ["python"]
CMD ["app.py"]

Saat dijalankan tanpa argument, container akan menjalankan python app.py. Saat dijalankan dengan argument, CMD di-override tapi ENTRYPOINT tetap.

docker run myapp            # → python app.py
docker run myapp test.py    # → python test.py

Perbandingan #

Aspek CMD ENTRYPOINT
Override Mudah Perlu --entrypoint
Bisa lebih dari satu Tidak (yang terakhir menang) Tidak (yang terakhir menang)
Cocok untuk Default command dengan fleksibilitas Binary utama yang konsisten
Pakai bentuk Exec form (["..."]) Exec form (["..."])

Hindari shell form untuk CMD dan ENTRYPOINT. Shell form menjalankan perintah via /bin/sh -c, yang makan signal dan menyebabkan container tidak bisa di-shutdown dengan benar. Selalu gunakan exec form:

// ✗ Anti-pattern: shell form, signal tidak lewat
CMD npm start

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

Praktik Menyusun Dockerfile #

Sekarang setelah kamu tahu setiap instruksi, mari kita lihat urutan yang baik dalam menulis Dockerfile.

flowchart TD
    A[1. ARG untuk parameter build] --> B[2. FROM base image]
    B --> C[3. LABEL untuk metadata]
    C --> D[4. ENV untuk runtime]
    D --> E[5. WORKDIR]
    E --> F[6. Install OS dependency]
    F --> G[7. Copy dependency definition]
    G --> H[8. Install app dependency]
    H --> I[9. Copy source code]
    I --> J[10. Multi-stage copy artifact]
    J --> K[11. EXPOSE]
    K --> L[12. USER non-root]
    L --> M[13. ENTRYPOINT/CMD]

Prinsip di balik urutan ini:

  1. Yang jarang berubah di atas — base image, OS dependency.
  2. Yang sering berubah di bawah — source code aplikasi.
  3. Konfigurasi runtime di tengah — env variable, workdir.
  4. Yang menentukan startup di akhir — entrypoint, cmd, user.

Urutan ini memaksimalkan cache hit dan meminimalkan waktu build.


Contoh Dockerfile Lengkap #

Untuk melihat semua instruksi di atas dalam konteks, perhatikan Dockerfile multi-stage untuk aplikasi Go:

# ==== Build stage ====
ARG GO_VERSION=1.22
FROM golang:${GO_VERSION}-alpine AS builder

LABEL stage=builder

WORKDIR /app

RUN apk add --no-cache git

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux \
    go build -ldflags="-s -w" -o app

# ==== Runtime stage ====
FROM gcr.io/distroless/base-debian12

WORKDIR /app
COPY --from=builder /app/app /app/app

USER nonroot:nonroot

EXPOSE 8080
ENTRYPOINT ["/app/app"]

Perhatikan:

  • ARG di luar FROM pertama dipakai untuk parameter versi Go.
  • LABEL menambahkan metadata.
  • Build stage punya compiler dan git; runtime stage tidak.
  • --from=builder menyalin artifact dari stage sebelumnya.
  • Runtime image (distroless) tidak punya shell, tidak punya package manager.
  • USER nonroot:nonroot memastikan container tidak jalan sebagai root.
  • ENTRYPOINT dalam exec form, dengan path absolut.

Ringkasan #

  • Struktur Dockerfile adalah urutan instruksi yang dieksekusi top-to-bottom. Setiap instruksi menghasilkan satu layer.
  • FROM wajib dan menentukan base image. Selalu pin versi, hindari latest. Multi-stage build menggunakan FROM lebih dari sekali.
  • ARG untuk parameter build, ENV untuk variable runtime. Keduanya tidak aman untuk secret.
  • WORKDIR lebih baik dari cd di RUN. COPY untuk salin file, ADD hanya jika butuh ekstrak tar.
  • RUN menggabungkan perintah terkait dan hapus cache di layer yang sama. Setiap RUN = satu layer.
  • EXPOSE hanya dokumentasi port. USER wajib non-root di production. HEALTHCHECK untuk orchestrator.
  • ENTRYPOINT adalah executable utama, CMD adalah default command. Gunakan exec form untuk keduanya agar signal handling benar.
  • Urutan yang baik = yang jarang berubah di atas, yang sering berubah di bawah. Ini memaksimalkan cache hit.

← Sebelumnya: Apa itu Dockerfile?   Berikutnya: Production Grade →

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