Best Practice #

Docker Compose adalah tool yang powerful, tapi tanpa konvensi yang baik, file docker-compose.yml bisa jadi berantakan dan sulit di-maintain. Artikel ini membahas best practice yang akan membuat file Compose-mu bersih, aman, dan production-ready.

1. Gunakan File yang Ringkas dan Modular #

File Compose yang besar dan kompleks sulit di-maintain. Pecah dengan beberapa strategi.

Pisahkan per Environment #

# docker-compose.yml — base
services:
  api:
    build: ./api
    image: myapp/api:${VERSION}
  
  db:
    image: postgres:16-alpine

# docker-compose.override.yml — development (auto-loaded)
services:
  api:
    command: npm run dev
    volumes:
      - ./api/src:/app/src
  
  db:
    ports:
      - "5432:5432"

# docker-compose.prod.yml — production
services:
  api:
    command: node dist/server.js
    environment:
      - NODE_ENV=production
    deploy:
      replicas: 2

Pakai sesuai environment:

# Development
docker compose up

# Production (no override, only prod)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Modular dengan Include (Docker Compose V2.20+) #

# docker-compose.yml
include:
  - path: ./compose/api.yml
  - path: ./compose/db.yml
  - path: ./compose/redis.yml

Pecah per concern (database, cache, aplikasi) ke file terpisah. Include menggabungkan mereka saat runtime.

2. Pakai Image Tag yang Stabil #

Selalu pin versi image. Hindari latest yang bisa berubah tanpa kamu sadari.

# BAGUS — pin versi
image: postgres:16-alpine
image: redis:7.2-alpine
image: nginx:1.25-alpine

# BURUK — bisa berubah sewaktu-waktu
image: postgres
image: postgres:latest
image: nginx

# BURUK — terlalu longgar
image: postgres:16  # dapat minor/patch update

Pattern terbaik:

  • Major.minor.patch + variant tag: postgres:16.2.1-alpine
  • Atau major.minor + variant: postgres:16.2-alpine

3. Hindari Hardcoded Secret #

Secret (password, API key, certificate) tidak boleh di file YAML yang di-commit ke Git.

# BURUK
services:
  db:
    environment:
      - POSTGRES_PASSWORD=supersecret  # TER-ekspos di Git!

# BAGUS — via .env
services:
  db:
    environment:
      - POSTGRES_PASSWORD=${DB_PASSWORD}  # dari .env, di-gitignore

# BAGUS — via secrets
services:
  api:
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

File .gitignore:

.env
.env.local
.env.*.local
secrets/

4. Healthcheck untuk Service yang Punya Dependent #

Healthcheck adalah satu-satunya cara reliable untuk depends_on: service_healthy.

services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  api:
    depends_on:
      db:
        condition: service_healthy  # tanpa healthcheck, ini gak ada artinya

5. Named Volume untuk Data Persisten #

Selalu pakai named volume untuk data yang harus persistent.

# BAGUS
volumes:
  db-data:

services:
  db:
    volumes:
      - db-data:/var/lib/postgresql/data

# HINDARI untuk production
services:
  db:
    volumes:
      - /var/lib/postgresql/data:/var/lib/postgresql/data

Named volume portable, di-manage Docker, support backup. Bind mount ke absolute path rapuh.

6. Resource Limits #

Set limit agar satu service tidak menghabiskan semua resource.

services:
  api:
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 512M

deploy.resources didukung di Compose V2 dan Docker Swarm. Untuk Docker daemon biasa, mem_limit dan cpus di top-level service adalah alternatif.

7. Restart Policy #

Set restart policy agar service auto-restart saat crash.

services:
  api:
    restart: unless-stopped  # selalu restart kecuali manual stop
  
  worker:
    restart: on-failure      # restart hanya saat crash

unless-stopped paling aman untuk production. no (default) untuk development.

8. Network Isolation #

Pisahkan service ke network sesuai perannya.

services:
  nginx:
    networks:
      - frontend
  
  api:
    networks:
      - frontend
      - backend
  
  db:
    networks:
      - backend  # tidak di-expose ke host

networks:
  frontend:
  backend:

Database tidak di network publik, mengurangi attack surface.

9. Hindari Container Berjalan sebagai Root #

Default banyak image jalan sebagai root. Override dengan user:.

services:
  api:
    user: "1000:1000"  # UID:GID non-root

Atau di Dockerfile:

RUN useradd -m -u 1000 app
USER app
Container root ≠ host root. Namespace隔离 membuat root di container tidak punya akses langsung ke host. Tapi defense in depth mengharuskan non-root di container juga — kalau ada container escape, attacker tidak langsung dapat root.

10. Logging Configuration #

Configure logging driver untuk kontrol ukuran dan rotasi log.

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Default driver json-file bisa memenuhi disk kalau tidak di-limit. Set max-size dan max-file untuk rotasi otomatis.

Untuk production, pertimbangkan external logging driver:

services:
  api:
    logging:
      driver: fluentd
      options:
        fluentd-address: localhost:24224
        tag: myapp.api

Kirim log ke centralized logging (ELK, Loki, CloudWatch) untuk retention dan searchability.

11. Image yang Efisien #

Pilih base image yang kecil dan aman.

# BAGUS — alpine variant
image: postgres:16-alpine
image: node:20-alpine
image: python:3.12-slim

# KURANG BAGUS — full image
image: postgres:16
image: node:20
image: python:3.12

# BURUK — sangat besar
image: ubuntu:22.04  # 70MB+ untuk OS

Image kecil = download cepat, scan cepat, attack surface kecil.

Untuk production dengan security tinggi, pertimbangkan distroless:

image: gcr.io/distroless/nodejs20-debian12
image: gcr.io/distroless/python3-debian12

Distroless punya hanya runtime + dependencies, tanpa shell, package manager, atau utilitas lain.

12. Validasi Compose File #

Selalu validasi sebelum deploy.

# Tampilkan parsed config
docker compose config

# Strict validation (no output kalau OK)
docker compose config --quiet

Integrasikan di CI/CD:

# .github/workflows/ci.yml
- name: Validate docker-compose
  run: docker compose config --quiet

13. Naming Convention yang Konsisten #

# Service names
services:
  web:           # atau frontend
  api:           # atau backend
  db:            # atau database, postgres
  cache:         # atau redis
  worker:        # atau job-processor

# Volume names
volumes:
  db-data:       # untuk data
  cache-data:    # untuk cache state
  uploads:       # untuk user uploads
  shared-config: # untuk konfigurasi bersama

# Network names
networks:
  frontend:       # untuk service yang di-expose ke user
  backend:        # untuk service internal
  management:     # untuk monitoring, debugging

Hindari nama generik seperti data, app, container1.

14. Dokumentasi dengan Komentar #

Komentar menjelaskan WHY, bukan WHAT.

services:
  api:
    # Pool size di-tune untuk 4-core CPU.
    # Terlalu tinggi akan context-switch overhead.
    deploy:
      resources:
        limits:
          cpus: "1.0"
    
    # Heap 1GB cukup untuk workload normal.
    # Untuk > 10K concurrent request, naikkan ke 2GB.
    memory: 1G

15. Struktur Direktori #

project/
├── docker-compose.yml
├── docker-compose.override.yml
├── docker-compose.prod.yml
├── .env
├── .env.example
├── .dockerignore
├── api/
│   ├── Dockerfile
│   ├── src/
│   └── package.json
├── web/
│   ├── Dockerfile
│   └── src/
├── db/
│   └── init/
│       └── 01-schema.sql
└── secrets/  # di-gitignore
    └── db_password.txt

Struktur jelas = mudah di-onboard developer baru.

Anti-Pattern yang Harus Dihindari #

1. Image latest #

# BURUK
image: nginx:latest

Bisa berubah sewaktu-waktu, menyebabkan surprise bug.

2. Hardcoded Secret #

# BURUK
environment:
  - API_KEY=sk-1234567890

Akan ter-commit ke Git history. Pakai .env atau secrets:.

3. Bind Mount untuk Data Production #

# BURUK untuk production
volumes:
  - /var/lib/postgresql/data:/var/lib/postgresql/data

Tidak portable, rapuh. Pakai named volume.

4. Tidak Ada Healthcheck #

# KURANG BAGUS
services:
  api:
    depends_on:
      - db

depends_on saja unreliable tanpa healthcheck. Service dependent bisa start sebelum dependency siap.

5. Port Berlebihan di Expose #

# BURUK — semua port di-expose ke host
services:
  db:
    ports:
      - "5432:5432"
  cache:
    ports:
      - "6379:6379"
  
# BAGUS — port internal tidak di-expose
services:
  db:
    expose:
      - "5432"  # hanya accessible di internal network

Hanya expose service yang memang perlu di-reach dari host.

6. Container Berjalan sebagai Root #

Default kebanyakan image. Override dengan user: "1000:1000".

7. File YAML Tidak Valid #

# SALAH — indentasi tidak konsisten
services:
  api:
  image: nginx
    ports:
    - "80:80"

Indentasi 2 spasi, konsisten.

8. Nama Service Generik #

# BURUK
services:
  app1:
  app2:
  service:

# BAGUS
services:
  web:
  api:
  worker:

Checklist Review #

Sebelum file Compose dianggap production-ready, jalankan checklist ini.

Front matter & structure:

  • File ter-version control (Git)
  • .env di-.gitignore, .env.example di-commit
  • Override file untuk dev/prod terpisah
  • README dengan quickstart

Images:

  • Semua image pakai tag stabil (bukan latest)
  • Image sekecil mungkin (alpine/slim/distroless)
  • Dockerfile di repo, multi-stage build

Security:

  • Tidak ada hardcoded secret
  • Container tidak jalan sebagai root
  • Healthcheck untuk semua service yang punya dependent
  • Network isolation sesuai peran
  • Resource limits di-set

Data:

  • Named volume untuk data persisten
  • Backup strategy didefinisikan
  • .dockerignore exclude file yang tidak perlu

Operational:

  • Restart policy di-set
  • Logging configuration
  • Validasi dengan docker compose config
  • Test di environment yang sama dengan production

Maintenance:

  • Naming convention konsisten
  • Komentar menjelaskan WHY
  • Tidak ada dead config (commented-out service, unused env)

Contoh File Production-Ready #

# docker-compose.yml (base untuk semua environment)
services:
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      web:
        condition: service_healthy
    networks:
      - frontend
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://localhost/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    user: "101:101"

  web:
    build:
      context: ./web
      dockerfile: Dockerfile
    image: myapp/web:${VERSION:-latest}
    environment:
      - NODE_ENV=production
      - API_URL=http://api:8080
    depends_on:
      api:
        condition: service_healthy
    networks:
      - frontend
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 1G
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    image: myapp/api:${VERSION:-latest}
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://app:${DB_PASSWORD}@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    env_file:
      - .env.production
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2G
    user: "1000:1000"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  db:
    image: postgres:16.2-alpine
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=myapp
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    user: "999:999"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  cache:
    image: redis:7.2-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - cache-data:/data
    networks:
      - backend
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    user: "999:999"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

volumes:
  db-data:
  cache-data:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # tidak expose ke host

File ini mengimplementasikan semua best practice yang dibahas:

  • Image tag stabil
  • Healthcheck di setiap service
  • Network isolation
  • Resource limits
  • Restart policy
  • Logging configuration
  • Non-root user
  • Secret via env_file
  • Named volume

Migrasi dari Best Practice Lama #

Untuk project yang sudah ada, migrasi bertahap:

Step 1: Image tag stabil Ganti latest ke specific version. Test, deploy, monitor.

Step 2: Healthcheck Tambahkan healthcheck ke service yang punya dependent. Enable condition: service_healthy.

Step 3: Resource limits Start dengan conservative limits. Tune berdasarkan monitoring.

Step 4: Network isolation Pisahkan frontend/backend network. Test semua flow masih bekerja.

Step 5: Logging configuration Tambahkan logging.options. Monitor disk usage.

Step 6: Non-root user Paling invasive — beberapa image mungkin broken. Test thoroughly.

Step 7: Secret management Migrasi dari hardcoded ke .env atau secrets:. Rotate password.

Migrrasi tidak harus sekaligus. Prioritaskan berdasarkan risiko dan impact.

Summary #

Best practice Docker Compose:

  1. File ringkas dan modular — pisahkan per environment.
  2. Image tag stabil — pin versi.
  3. Hindari hardcoded secret — pakai .env atau secrets:.
  4. Healthcheck untuk dependent.
  5. Named volume untuk data.
  6. Resource limits untuk setiap service.
  7. Restart policy untuk resilience.
  8. Network isolation sesuai peran.
  9. Non-root user untuk security.
  10. Logging configuration untuk kontrol ukuran.
  11. Image efisien — alpine/slim.
  12. Validasi sebelum deploy.
  13. Naming convention konsisten.
  14. Dokumentasi yang jelas.
  15. Checklist sebelum production.

Ringkasan #

  • Gunakan file yang ringkas dan modular — pisahkan per environment dengan override file, atau modular dengan include.
  • Image tag stabil — selalu pin versi, hindari latest.
  • Hindari hardcoded secret — pakai .env atau secrets:. File .env di-.gitignore.
  • Healthcheck untuk service dependent — tanpa healthcheck, depends_on: service_healthy tidak reliable.
  • Named volume untuk data persisten, bind mount hanya untuk development source code.
  • Resource limits agar satu service tidak mengganggu yang lain.
  • Restart policy untuk auto-recovery.
  • Network isolation sesuai peran (frontend, backend, management).
  • Non-root user untuk defense in depth.
  • Logging configuration untuk kontrol ukuran dan rotasi log.
  • Image efisien — alpine/slim/distroless untuk ukuran kecil dan attack surface kecil.
  • Validasi dengan docker compose config sebelum deploy.
  • Naming convention konsisten — service, volume, network punya pola jelas.
  • Dokumentasi — komentar menjelaskan WHY, README punya quickstart.
  • Checklist review sebelum production: struktur, security, data, operational, maintenance.
  • Anti-pattern: image latest, hardcoded secret, bind mount untuk production data, no healthcheck, port berlebihan, root user.
  • Best practice adalah consistency — pilih pattern dan terapkan di semua file Compose di project.

← Sebelumnya: Local Development   Berikutnya: Local Development → Spring Boot

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