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)
-
.envdi-.gitignore,.env.exampledi-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
-
.dockerignoreexclude 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:
- File ringkas dan modular — pisahkan per environment.
- Image tag stabil — pin versi.
- Hindari hardcoded secret — pakai
.envatausecrets:. - Healthcheck untuk dependent.
- Named volume untuk data.
- Resource limits untuk setiap service.
- Restart policy untuk resilience.
- Network isolation sesuai peran.
- Non-root user untuk security.
- Logging configuration untuk kontrol ukuran.
- Image efisien — alpine/slim.
- Validasi sebelum deploy.
- Naming convention konsisten.
- Dokumentasi yang jelas.
- 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
.envatausecrets:. File.envdi-.gitignore.- Healthcheck untuk service dependent — tanpa healthcheck,
depends_on: service_healthytidak 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 configsebelum 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