Dockerfile & Compose
Referensi lengkap instruksi Dockerfile, pola multi-stage build, dan konfigurasi docker-compose.yml / podman-compose. Berlaku untuk Docker maupun Podman.
00
FROM — Base Image
Setiap Dockerfile wajib dimulai dengan FROM. Pilih base image sekecil mungkin.
Bentuk dasar
dockerfile
FROM node:22-alpine # image:tag FROM node:22-alpine AS builder # beri nama stage (multi-stage) FROM scratch # image kosong — untuk binary statis FROM ubuntu:24.04
Rekomendasi base image
Gunakan tag spesifik (
:22-alpine) bukan :latest agar build reproducible. Alpine ~5 MB, Distroless ~20 MB, Debian-slim ~70 MB.contoh
# Ringan + shell tersedia FROM node:22-alpine FROM python:3.12-alpine FROM golang:1.22-alpine # Tanpa shell, tanpa paket OS — attack surface minimal FROM gcr.io/distroless/nodejs22-debian12:nonroot FROM gcr.io/distroless/python3-debian12:nonroot FROM gcr.io/distroless/static-debian12:nonroot # untuk Go binary statis
01
Instruksi Utama
Instruksi yang membentuk layer dan metadata image.
WORKDIR
WORKDIR membuat direktori jika belum ada. Selalu gunakan path absolut — hindari cd di dalam RUN.dockerfile
WORKDIR /app
COPY vs ADD
Gunakan
COPY untuk file lokal. ADD hanya untuk kasus khusus: auto-extract tar atau URL (jarang dibutuhkan).dockerfile
COPY package.json package-lock.json ./ # file spesifik COPY src/ ./src/ # direktori COPY --chown=node:node . . # salin dengan ownership COPY --from=builder /app/dist ./dist # copy antar stage ADD archive.tar.gz /opt/ # extract otomatis ADD https://example.com/file /opt/file # dari URL (tidak disarankan)
RUN
dockerfile
# Shell form — dijalankan via /bin/sh -c RUN apt-get update && apt-get install -y curl # Exec form — tanpa shell, tidak ada wildcard/pipe RUN ["npm", "ci"] # Gabungkan perintah dalam satu layer untuk menjaga image tetap kecil RUN apt-get update && apt-get install -y --no-install-recommends curl git && rm -rf /var/lib/apt/lists/*
ENV dan ARG
ARG hanya tersedia saat build. ENV tersimpan di image dan tersedia saat runtime. Jangan simpan secret di keduanya — gunakan secret mount.dockerfile
ARG NODE_VERSION=22 # nilai default, bisa di-override saat build ARG BUILD_DATE # tanpa default — wajib di-supply saat build ENV NODE_ENV=production # ENV tersimpan di image dan tersedia saat runtime # beberapa ENV bisa dalam satu instruksi: ENV PORT=3000 HOSTNAME=0.0.0.0
EXPOSE
EXPOSE bersifat dokumentasi saja — tidak membuka port ke host. Port forwarding tetap dikonfigurasi di docker run -p atau compose.dockerfile
EXPOSE 3000 # TCP (default) EXPOSE 53/udp # UDP
CMD dan ENTRYPOINT
ENTRYPOINT = perintah utama yang tidak bisa diganti (kecuali --entrypoint). CMD = argumen default — bisa di-override. Gunakan exec form
["..."] agar proses menerima sinyal OS dengan benar.dockerfile
# Exec form — WAJIB untuk produksi (sinyal PID 1 langsung ke proses) CMD ["node", "server.js"] ENTRYPOINT ["node", "server.js"] # Kombinasi: ENTRYPOINT = binary, CMD = argumen default ENTRYPOINT ["nginx"] CMD ["-g", "daemon off;"] # Shell form — hindari di produksi (proses berjalan di bawah /bin/sh) CMD node server.js
USER
Selalu jalankan proses sebagai non-root di produksi. Buat user di layer awal, baru switch di layer akhir.
dockerfile
# Buat user non-root RUN addgroup --system --gid 1001 appgroup && adduser --system --uid 1001 appuser --ingroup appgroup USER appuser # gunakan nama USER 1001 # atau UID langsung (lebih eksplisit di distroless)
HEALTHCHECK
dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD curl -f http://localhost:3000/health || exit 1 HEALTHCHECK NONE # nonaktifkan healthcheck dari base image
Volume dan metadata lainnya
dockerfile
VOLUME /data # deklarasi mount point (opsional, dokumentasi) LABEL org.opencontainers.image.source="https://github.com/org/repo" LABEL org.opencontainers.image.version="1.0.0" LABEL maintainer="team@example.com" STOPSIGNAL SIGTERM # sinyal untuk menghentikan container (default SIGTERM)
02
Multi-Stage Build
Pisahkan environment build dan runtime untuk menghasilkan image sekecil mungkin.
Pola umum: deps → builder → runner
dockerfile
# Stage 1: install dependensi (bisa di-cache) FROM node:22-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci # Stage 2: build FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build # Stage 3: runtime — hanya artefak yang dibutuhkan FROM gcr.io/distroless/nodejs22-debian12:nonroot AS runner WORKDIR /app COPY --from=builder --chown=nonroot:nonroot /app/dist ./dist EXPOSE 3000 CMD ["dist/server.js"]
Build stage tertentu (untuk dev/test)
shell
# Berhenti di stage builder, tidak lanjut ke runner docker build --target builder -t myapp:dev . # Podman sama persis podman build --target builder -t myapp:dev .
Contoh: Go binary statis
dockerfile
FROM golang:1.22-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/server ./cmd/server FROM gcr.io/distroless/static-debian12:nonroot AS runner COPY --from=builder /app/server /server EXPOSE 8080 ENTRYPOINT ["/server"]
03
Best Practices
.dockerignore
Tanpa
.dockerignore, seluruh node_modules, .git, dan file sensitif masuk ke build context dan memperlambat build..dockerignore
.git .gitignore node_modules npm-debug.log .env .env.* *.test.ts coverage/ .next/cache dist/ Dockerfile* docker-compose* README.md
Cache layer dengan benar
Docker build cache bekerja secara berurutan — jika satu layer berubah, semua layer di bawahnya dijalankan ulang. Salin file yang jarang berubah lebih dulu.
dockerfile
# SALAH — salin semua file dulu, baru install (setiap kode berubah = npm ci ulang) COPY . . RUN npm ci # BENAR — salin manifest dulu, install, baru salin kode COPY package.json package-lock.json ./ RUN npm ci COPY . .
Secret saat build (tidak masuk image)
Jangan hardcode token atau password di Dockerfile. Gunakan
--secret agar tidak tersimpan di layer manapun.dockerfile
# Dockerfile RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
shell
# Build command docker build --secret id=npmrc,src=$HOME/.npmrc . podman build --secret id=npmrc,src=$HOME/.npmrc .
Build cache eksternal (CI)
shell
# Export cache ke registry docker build --cache-from type=registry,ref=registry.example.com/myapp:cache --cache-to type=registry,ref=registry.example.com/myapp:cache,mode=max -t myapp:latest .
04
Compose — Struktur File
Anatomi docker-compose.yml. Berlaku untuk Docker Compose v2 dan podman-compose.
Skeleton lengkap
yaml
services:
app: # nama service (juga jadi hostname di dalam network)
image: myapp:latest # pakai image yang sudah ada
# atau:
build:
context: . # direktori build context
dockerfile: deploy/Dockerfile # path Dockerfile (opsional)
args:
NODE_ENV: production
container_name: myapp # nama container eksplisit (opsional)
restart: unless-stopped # always | on-failure | no | unless-stopped
ports:
- "127.0.0.1:3000:3000" # host:container — bind ke localhost saja
environment:
NODE_ENV: production
DATABASE_URL: postgres://user:pass@db:5432/mydb
env_file:
- .env # load dari file
volumes:
- appdata:/data # named volume
- ./config:/app/config:ro # bind mount, read-only
networks:
- appnet
depends_on:
db:
condition: service_healthy # tunggu healthcheck OK
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- appnet
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata: # named volume — dikelola oleh Docker/Podman
appdata:
networks:
appnet:
driver: bridgeGunakan network external (infra shared)
Untuk stack yang bergabung ke network yang sudah ada (seperti
rattletrap-network), gunakan external: true.yaml
services:
app:
image: myapp:latest
networks:
- rattletrap-network
networks:
rattletrap-network:
external: true # network sudah dibuat sebelumnya, tidak dibuat ulangProfiles — service opsional
yaml
services:
app:
image: myapp:latest
debug-tools:
image: nicolaka/netshoot
profiles:
- debug # hanya aktif jika profile debug di-enableshell
docker compose --profile debug up -d podman compose --profile debug up -d
05
Compose — Opsi Service Penting
restart policy
yaml
restart: no # default — tidak restart otomatis restart: always # selalu restart (termasuk saat daemon reboot) restart: on-failure # restart hanya jika exit code != 0 restart: unless-stopped # restart kecuali dihentikan manual (rekomendasi produksi)
Port — bind ke localhost saja
Selalu prefix dengan
127.0.0.1: di produksi agar port tidak terbuka ke publik. Biarkan NGINX yang menjadi pintu masuk.yaml
ports: - "127.0.0.1:8080:80" # aman — hanya loopback - "8080:80" # BAHAYA di server publik — terbuka ke semua interface
depends_on dengan kondisi
yaml
depends_on:
db:
condition: service_healthy # tunggu healthcheck OK
redis:
condition: service_started # cukup tunggu container running (default)
migrations:
condition: service_completed_successfully # tunggu container exit 0Volume — named vs bind mount vs tmpfs
yaml
volumes:
- pgdata:/var/lib/postgresql/data # named volume (rekomen untuk data DB)
- ./nginx.conf:/etc/nginx/nginx.conf:ro # bind mount read-only
- type: tmpfs # RAM disk
target: /tmp
tmpfs:
size: 128mLogging driver
yaml
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"06
Compose — Perintah
Ganti docker dengan podman untuk stack Podman.
Lifecycle
shell
docker compose up -d # start semua service, background docker compose up -d --build # build ulang image dulu docker compose up -d --force-recreate # paksa buat ulang container docker compose down # stop dan hapus container + network docker compose down -v # + hapus named volume docker compose restart api # restart service tertentu
Monitoring
shell
docker compose ps # status semua service docker compose logs -f # ikuti semua log docker compose logs -f --tail 50 api # 50 baris terakhir, service api docker compose top # proses di tiap container
Eksekusi dan debug
shell
docker compose exec api bash # shell ke container service api docker compose exec db psql -U user mydb # langsung jalankan perintah docker compose run --rm api npm test # jalankan perintah sekali, lalu hapus container
Config dan validasi
shell
docker compose config # tampilkan config setelah variable substitution docker compose config --services # daftar nama service docker compose convert # (podman) konversi ke format lain
Update dan redeploy
shell
# Pull image terbaru, rebuild, dan recreate container docker compose pull docker compose up -d --build --force-recreate # Bersihkan image lama docker image prune -f
07
Contoh Dockerfile per Stack
Template siap pakai, production-ready. Copy, sesuaikan nama image dan port, deploy.
Next.js (App Router — standalone)
Wajib tambahkan
output: 'standalone' di next.config.mjs agar .next/standalone terbentuk. Runner memakai distroless — tanpa shell, tanpa root.nextjs
FROM node:22-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build FROM gcr.io/distroless/nodejs22-debian12:nonroot AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 COPY --from=builder --chown=nonroot:nonroot /app/.next/standalone ./ COPY --from=builder --chown=nonroot:nonroot /app/.next/static ./.next/static EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME=0.0.0.0 CMD ["server.js"]
next.config.mjs
const nextConfig = {
output: 'standalone',
};
export default nextConfig;Node.js / Express API
node
FROM node:22-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nodeapp COPY --from=deps --chown=nodeapp:nodejs /app/node_modules ./node_modules COPY --chown=nodeapp:nodejs . . USER nodeapp EXPOSE 3000 CMD ["node", "src/index.js"]
Java Spring Boot — Maven
Layered jar (
-Dspring-boot.repackage.layered=true) memaksimalkan cache Docker — layer dependensi jarang berubah dan di-cache terpisah dari kode aplikasi.java
FROM maven:3.9-eclipse-temurin-21 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -q COPY src ./src RUN mvn package -DskipTests -q # Ekstrak layered jar FROM eclipse-temurin:21-jre-alpine AS layers WORKDIR /app COPY --from=builder /app/target/*.jar app.jar RUN java -Djarmode=layertools -jar app.jar extract FROM eclipse-temurin:21-jre-alpine AS runner WORKDIR /app RUN addgroup --system --gid 1001 spring && adduser --system --uid 1001 spring --ingroup spring COPY --from=layers --chown=spring:spring /app/dependencies/ ./ COPY --from=layers --chown=spring:spring /app/spring-boot-loader/ ./ COPY --from=layers --chown=spring:spring /app/snapshot-dependencies/ ./ COPY --from=layers --chown=spring:spring /app/application/ ./ USER spring EXPOSE 8080 ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Java Spring Boot — Gradle
gradle
FROM gradle:8-jdk21-alpine AS builder WORKDIR /app COPY build.gradle settings.gradle ./ COPY gradle ./gradle RUN gradle dependencies --no-daemon -q COPY src ./src RUN gradle bootJar --no-daemon -q FROM eclipse-temurin:21-jre-alpine AS runner WORKDIR /app RUN addgroup --system --gid 1001 spring && adduser --system --uid 1001 spring --ingroup spring COPY --from=builder --chown=spring:spring /app/build/libs/*.jar app.jar USER spring EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
Python — FastAPI (dengan uv)
uv adalah package manager Python modern yang 10–100x lebih cepat dari pip. Install dependensi dengan --no-cache untuk menjaga image tetap kecil.python
FROM python:3.12-slim AS builder WORKDIR /app RUN pip install uv --no-cache-dir COPY requirements.txt . RUN uv pip install --system --no-cache -r requirements.txt FROM python:3.12-slim AS runner WORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 RUN addgroup --system --gid 1001 appgroup && adduser --system --uid 1001 appuser --ingroup appgroup COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages COPY --from=builder /usr/local/bin /usr/local/bin COPY --chown=appuser:appgroup . . USER appuser EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Python — Flask / generic (pip)
python
FROM python:3.12-alpine AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt FROM python:3.12-alpine AS runner WORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 RUN addgroup --system --gid 1001 appgroup && adduser --system --uid 1001 appuser --ingroup appgroup COPY --from=builder /install /usr/local COPY --chown=appuser:appgroup . . USER appuser EXPOSE 5000 CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"]
Go — binary statis (distroless)
CGO_ENABLED=0 menghasilkan binary statis yang tidak bergantung pada library C — bisa berjalan di distroless/static yang hampir kosong.go
FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/server FROM gcr.io/distroless/static-debian12:nonroot AS runner COPY --from=builder /app/server /server EXPOSE 8080 ENTRYPOINT ["/server"]
React / Vue / Angular — SPA (nginx)
Ganti
npm run build dan direktori output (dist atau build) sesuai framework. Sesuaikan juga konfigurasi nginx untuk SPA routing.react
FROM node:22-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build # output ke /app/dist atau /app/build FROM nginx:1.27-alpine AS runner RUN rm /etc/nginx/conf.d/default.conf COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/app.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
nginx.conf
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
# SPA fallback — semua route diarahkan ke index.html
location / {
try_files $uri $uri/ /index.html;
}
# Cache asset statis
location ~* .(js|css|png|jpg|svg|ico|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
gzip on;
gzip_types text/plain text/css application/javascript application/json;
}Rust — binary statis (distroless)
rust
FROM rust:1.78-alpine AS builder
WORKDIR /app
RUN apk add --no-cache musl-dev
COPY Cargo.toml Cargo.lock ./
# Pre-build dependensi untuk cache
RUN mkdir src && echo "fn main(){}" > src/main.rs && cargo build --release && rm -rf src
COPY src ./src
RUN touch src/main.rs && cargo build --release
FROM gcr.io/distroless/static-debian12:nonroot AS runner
COPY --from=builder /app/target/release/server /server
EXPOSE 8080
ENTRYPOINT ["/server"].dockerignore universal
Simpan di root project bersama Dockerfile. Sesuaikan sesuai stack — hapus baris yang tidak relevan.
.dockerignore
# Version control .git .gitignore # Dependencies (di-install ulang saat build) node_modules vendor __pycache__ .venv venv target/ # Rust/Java build output # Build output lokal dist build .next out *.class *.jar !app.jar # kecualikan hanya jika perlu copy secara eksplisit # Test dan coverage *.test.ts *.spec.ts coverage htmlcov .pytest_cache # Environment & secret .env .env.* *.pem *.key # Docker dan CI Dockerfile* docker-compose* .github .gitlab-ci.yml # IDE dan OS .idea .vscode *.DS_Store *.swp