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

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

dockerfile
WORKDIR /app

COPY vs ADD

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

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

dockerfile
EXPOSE 3000          # TCP (default)
EXPOSE 53/udp        # UDP

CMD dan ENTRYPOINT

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

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

.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

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)

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: bridge

Gunakan network external (infra shared)

yaml
services:
  app:
    image: myapp:latest
    networks:
      - rattletrap-network

networks:
  rattletrap-network:
    external: true    # network sudah dibuat sebelumnya, tidak dibuat ulang

Profiles — service opsional

yaml
services:
  app:
    image: myapp:latest

  debug-tools:
    image: nicolaka/netshoot
    profiles:
      - debug              # hanya aktif jika profile debug di-enable
shell
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

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 0

Volume — 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: 128m

Logging 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)

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

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)

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)

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)

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

.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