Best Practice #
Local development dengan Docker Compose adalah titik pertemuan antara produktivitas developer dan konsistensi environment. Bedanya dengan production: di local development, developer experience (DX) adalah fitur utama — bukan ukuran image, security hardening, atau zero-downtime deployment. Artikel ini merangkum best practice yang berlaku lintas bahasa dan framework (Go, Java, Node.js, Python, Rust, Angular, React, Vue, dan lainnya), dan akan membuat setup local development-mu konsisten, cepat, dan menyenangkan untuk digunakan.
Sepuluh prinsip di bawah ini disusun berdasarkan prioritas: mulai dari hal yang paling sering di-skip (override file, healthcheck), naik ke keamanan dan operasional (network isolation, secret, logging). Terapkan secara bertahap — bahkan satu atau dua prinsip saja sudah membuat workflow-mu terasa lebih solid.
1. Selalu Gunakan Override File untuk Development #
Salah satu keputusan paling fundamental di local development adalah tidak mencampur konfigurasi development dengan konfigurasi production. Override file adalah mekanisme resmi Docker Compose untuk memisahkan keduanya tanpa duplikasi.
# docker-compose.yml — base, sama untuk semua environment
services:
api:
build:
context: ./api
dockerfile: Dockerfile
image: myapp/api:${VERSION:-latest}
environment:
- DATABASE_URL=postgres://app:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
networks:
- backend
db:
image: postgres:16.2-alpine
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
networks:
backend:
volumes:
db-data:
# docker-compose.override.yml — OTOMATIS di-merge saat `docker compose up`
services:
api:
command: npm run dev
volumes:
- ./api/src:/app/src # bind mount source code
ports:
- "3000:3000" # expose ke host
environment:
- NODE_ENV=development
- DEBUG=*
db:
ports:
- "5432:5432" # expose hanya untuk debug
# Development: override otomatis ter-load
docker compose up
# Production: TIDAK load override
docker compose -f docker-compose.yml up -d
# ANTI-PATTERN: satu file untuk semua environment
services:
api:
command: npm run dev # HARUS di-override untuk production
volumes:
- ./api/src:/app/src # bind mount HARUS tidak ada di production
ports:
- "3000:3000" # port mapping harus beda di production
Tanpa override file, kamu akan sering menambahkan # if development atau mengubah-ubah command manual. Itu rawan lupa, sulit di-review, dan bikin rekan tim frustasi. Override file juga memberikan satu sumber kebenaran untuk service yang sama — base file mendefinisikan “service ini seperti apa”, override file mendefinisikan “untuk development, bagian mana yang berbeda”.
Override file hanya untuk development. Untuk environment lain (staging, production, preview), pakai file eksplisit:docker-compose -f docker-compose.yml -f docker-compose.prod.yml up. Jangan pakaidocker-compose.override.ymluntuk production — itu invitation for disaster.
2. Pakai Healthcheck untuk Service Dependent #
depends_on hanya mengatur urutan start, bukan kesiapan service. Database bisa sudah running tapi belum menerima koneksi — aplikasi akan crash dengan error connection refused. Solusinya: healthcheck.
services:
db:
image: postgres:16.2-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
cache:
image: redis:7.2-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
api:
depends_on:
db:
condition: service_healthy # tunggu sampai healthcheck db "healthy"
cache:
condition: service_healthy
# ANTI-PATTERN: depends_on tanpa healthcheck
services:
api:
depends_on:
- db
- cache
# ini HANYA memastikan db/cache start pertama, bukan "siap"
# ANTI-PATTERN: workaround dengan sleep di entrypoint
#!/bin/sh
sleep 30 # JANGAN. Tidak reliable, race condition, lambat
node server.js
# Cek status healthcheck
docker compose ps
# STATUS: Up 2 minutes (healthy) ← yang kita mau
# STATUS: Up 2 minutes (health: starting) ← tunggu
Healthcheck di local development sama pentingnya dengan production. Tanpa itu, kamu akan sering melihat error startup yang sebenarnya tidak perlu — error yang akan hilang setelah beberapa detik. Tapi selama error itu muncul, developer akan kehilangan waktu untuk men-debug hal yang sebenarnya bukan bug.
Pilih perintah healthcheck yang ringan dan cepat.pg_isreadylebih cepat daripsql -c "SELECT 1". Untuk HTTP service,wget -q --spiderataucurl -flebih cepat dari full HTTP request. Healthcheck akan jalan tiap beberapa detik — optimize for speed.
3. Bind Mount Source Code untuk Hot Reload #
Salah satu kekuatan terbesar Docker Compose di local development adalah bind mount untuk source code. Dengan bind mount, edit file di host langsung terlihat di container — tanpa rebuild, tanpa restart.
# BENAR: bind mount untuk source code
services:
api:
build:
context: ./api
dockerfile: Dockerfile.dev # Dockerfile khusus development
command: npm run dev # jalankan dalam mode watch/hot reload
volumes:
- ./api/src:/app/src # source code di-bind dari host
- /app/node_modules # anonymous volume, JANGAN di-overwrite
environment:
- NODE_ENV=development
# ANTI-PATTERN: copy source code di Dockerfile, rebuild setiap edit
services:
api:
build:
context: ./api
command: node server.js
# Tidak ada bind mount → edit di host tidak terlihat di container
# BENAR: pisahkan dependency cache dari source code
services:
api:
volumes:
- ./api/src:/app/src # source code bind mount
- node_modules:/app/node_modules # dependency di named volume
- /app/dist # build output anonymous
# Verifikasi bind mount aktif
docker compose exec api ls -la /app/src
# Harus muncul file dari host
Trik penting: anonymous volume (path host kosong) untuk menimpa direktori yang ada di image. Contoh di Node.js: saat npm install membuat node_modules di image, bind mount ./src:/app/src akan membuat direktori src baru — tapi node_modules masih utuh. Kalau kamu bind mount seluruh project (./api:/app), node_modules akan hilang karena host tidak punya folder itu. Solusinya: bind mount sub-folder spesifik + anonymous volume untuk hal-hal yang tidak boleh di-overwrite.
Pattern ini juga berlaku untuk bahasa lain:
# Java/Maven: cache local repository
services:
api:
volumes:
- ./api/src:/app/src
- maven-repo:/root/.m2/repository # named volume
- /app/target # anonymous, build output
# Rust/Cargo: cache registry + target
services:
api:
volumes:
- ./api/src:/app/src
- cargo-registry:/usr/local/cargo/registry
- cargo-target:/app/target
# Python: cache pip + venv
services:
api:
volumes:
- ./api/app:/app
- pip-cache:/root/.cache/pip
- /app/.venv # kalau pakai venv
4. Named Volume untuk Data Persisten #
Bedakan dua jenis volume: bind mount untuk source code (lihat prinsip #3), named volume untuk data yang harus bertahan antar container restart.
# BENAR: named volume untuk data persisten
services:
db:
image: postgres:16.2-alpine
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data: # Docker yang manage, portable, support backup
# KURANG BAGUS untuk development: bind mount ke absolute path
services:
db:
volumes:
- /Users/you/data/postgres:/var/lib/postgresql/data
# Rapuh: pindah mesin, pindah OS, hilang
# Inspect volume
docker volume inspect <project>_db-data
# Lokasi: /var/lib/docker/volumes/<project>_db-data/_data
# Tidak tergantung absolute path host
# Backup named volume
docker run --rm \\
-v <project>_db-data:/source:ro \\
-v $(pwd):/backup \\
alpine tar -czf /backup/db-data.tar.gz -C /source .
# Restore
docker run --rm \\
-v <project>_db-data:/target \\
-v $(pwd):/backup \\
alpine tar -xzf /backup/db-data.tar.gz -C /target
# Reset volume kalau development database rusak
docker compose down -v # HAPUS volume, BUKAN container
docker compose up
Jangan bind-mount /var/lib/postgresql/data ke host untuk development. Setiap kali host reboot, path bisa berubah (terutama di macOS/Windows dengan Docker Desktop yang menyembunyikan VM di balik layer). Named volume yang di-manage Docker stabil di semua environment.
Panduan praktis: bind mount untuk source code, named volume untuk data, anonymous volume untuk cache yang akan di-rebuild. Campur ketiganya dengan sadar — masing-masing punya kegunaan. Untuk data production, pertimbangkan backup strategy (lihat contoh di atas) — data development juga bisa berharga (schema migration test, fixture data untuk testing).
5. Non-Root User untuk Container Security #
Container yang jalan sebagai root adalah default banyak image resmi. Ini bukan masalah besar di local development karena namespace isolasi membuat root di container tidak punya akses langsung ke host, tapi defense in depth mengajarkan kita untuk tidak mengandalkan satu lapis keamanan.
# BENAR: container jalan sebagai user non-root
services:
api:
user: \"1000:1000\" # UID:GID
# BENAR: di Dockerfile, switch ke user non-root
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --chown=node:node . .
USER node # Built-in user di image node:alpine
# ANTI-PATTERN: container jalan sebagai root
services:
api:
# Tidak ada `user:` → jalan sebagai root (UID 0)
# Cek user yang sedang jalan di container
docker compose exec api whoami
# Harus: node atau appuser, BUKAN root
# Cek UID dari proses utama
docker compose exec api stat -c '%u' /proc/1/
# Harus: 1000 (atau nilai lain non-zero)
Kalau aplikasimu menulis file ke bind mount (misalnya source code yang di-mount), running sebagai root akan membuat file di host jadi milik root. Ini akan menyebabkan permission error di IDE/editor kamu. Solusi: set UID yang sama dengan user host-mu (1000:1000 untuk user biasa di Linux/macOS).
Untuk local development, fokus pada matching UID dengan host agar tidak ada permission drama. Untuk production, fokus pada defense in depth agar container escape tidak langsung jadi host root. Keduanya saling melengkapi — praktik yang baik di development akan lebih mudah di-promote ke production.
# Cari tahu UID user host kamu
id -u
# Output: 1000 (atau angka lain)
# Pakai nilai ini di compose: user: \"1000:1000\"
# Multi-service dengan UID konsisten
services:
api:
user: \"1000:1000\"
worker:
user: \"1000:1000\"
scheduler:
user: \"1000:1000\"
6. Resource Limits untuk Setiap Service #
Resource limits mencegah satu service menghabiskan semua memory/CPU laptop-mu. Tanpa limits, satu runaway service bisa membuat seluruh Docker Desktop hang dan kamu harus reboot host.
# BENAR: set resource limits
services:
api:
deploy:
resources:
limits:
cpus: \"1.0\"
memory: 1G
reservations:
cpus: \"0.25\"
memory: 256M
# Sintaks alternatif (top-level, lebih sederhana)
services:
api:
mem_limit: 1g
mem_reservation: 256m
cpus: \"1.0\"
# ANTI-PATTERN: tanpa limits
services:
api:
# Tidak ada batasan → bisa habiskan semua memory/CPU
# Monitor penggunaan resource
docker stats
# CONTAINER CPU % MEM USAGE / LIMIT MEM %
# myapp-api 0.5% 245MiB / 1GiB 24%
# myapp-db 2.1% 380MiB / 2GiB 19%
Di local development, batasi sesuai dengan resource laptop-mu. Kalau laptop punya 16GB RAM dan 4 core, bagi rata: API 1GB/1 core, DB 2GB/2 core, cache 256MB/0.5 core, total 3.25GB / 3.5 core — masih sisa banyak untuk IDE, browser, dan OS. Jangan copy-paste limit dari tutorial online tanpa melihat konteks hardware.
Resource limits juga penting untuk testing failure scenario. Dengan limit memory 512MB, kamu bisa lihat apakah aplikasi-mu gracefully handle OOM (out of memory) — atau crash. Tanpa limit, kamu tidak akan pernah tahu sampai production. Ini adalah bentuk chaos engineering ringan yang bisa kamu lakukan setiap hari.
# Guideline alokasi resource untuk development
services:
api: # memory: 512M-1G, cpu: 0.5-1
web: # memory: 256M-512M, cpu: 0.25-0.5
worker: # memory: 512M-1G, cpu: 0.5-1
db: # memory: 1G-2G, cpu: 1-2
cache: # memory: 128M-256M, cpu: 0.25-0.5
queue: # memory: 256M-512M, cpu: 0.25-0.5
7. Network Isolation per Peran #
Network isolation mungkin terdengar seperti konsep production, tapi di local development juga relevan — terutama untuk mencegah accidental exposure dan mempercepat cold start.
# BENAR: pisahkan network sesuai peran
services:
web:
networks:
- frontend
api:
networks:
- frontend # reachable dari web
- backend # reachable dari db, cache
db:
networks:
- backend # TIDAK di frontend → tidak reachable dari web
# Tidak ada `ports:` → tidak expose ke host
cache:
networks:
- backend
networks:
frontend:
backend:
# ANTI-PATTERN: satu network untuk semua + expose semua
services:
db:
networks:
- default
ports:
- \"5432:5432\" # semua orang bisa akses DB dari host
cache:
networks:
- default
ports:
- \"6379:6379\" # dan Redis juga
flowchart LR
subgraph HOST["Host (laptop kamu)"]
DEV[Developer / IDE]
end
subgraph FRONTEND["Network: frontend"]
WEB[web :3000]
API[api :8080]
end
subgraph BACKEND["Network: backend (internal)"]
DB[db :5432]
CACHE[cache :6379]
QUEUE[queue :5672]
end
DEV -->|localhost:3000| WEB
DEV -->|localhost:8080| API
API --> DB
API --> CACHE
API --> QUEUE
WEB -.tidak bisa.-> DB
style BACKEND fill:#f5f5f5,stroke:#333
style FRONTEND fill:#e3f2fd,stroke:#1976d2
# Test isolation: coba akses db dari container yang tidak seharusnya
docker compose exec web ping db
# Wajib gagal — web tidak punya akses ke network backend
Pemisahan network juga memberikan performance benefit. Network bridge di Docker punya overhead kecil tapi nyata. Kalau service yang sering berkomunikasi (api ↔ db) di network yang sama, latency-nya lebih rendah dibanding yang harus hop lewat banyak network. Lebih penting lagi, ini adalah simulasi production — di production, frontend tidak akan pernah bisa akses database langsung. Kalau setup development-mu meniru pola itu, kamu akan lebih percaya bahwa kode-mu benar-benar aman di production.
8. Secret Management — Jangan Hardcode #
Secret (password, API key, token) tidak boleh ada di file docker-compose.yml yang di-commit ke Git. Kalau ter-commit, sudah terlambat — Git history akan mengingatnya selamanya.
# BENAR: secret via .env file
services:
db:
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
# .env (di-gitignore)
DB_PASSWORD=local-dev-password
REDIS_PASSWORD=local-dev-redis
API_KEY=sk-test-local-only
# .env.example (di-commit, tanpa nilai)
DB_PASSWORD=changeme
REDIS_PASSWORD=changeme
API_KEY=your-key-here
# .gitignore
.env
.env.local
.env.*.local
# BENAR: secret via Docker secrets (lebih aman)
services:
api:
secrets:
- db_password
- api_key
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
# JANGAN: hardcode di compose file
services:
db:
environment:
- POSTGRES_PASSWORD=supersecret123 # ada di Git selamanya
# Cek apakah ada secret di Git history
git log -p | grep -iE 'password|secret|key|token' | head -20
# Kalau ada → rotate secret SEKARANG, lalu bersihkan history
Kalau secret production pernah ter-commit ke Git, rotate secret itu. Membersihkan Git history tidak menghapus data dari clone orang lain. Pakai tool seperti git-filter-repo atau BFG Repo-Cleaner, tapi tetap anggap secret itu compromised.
Untuk local development, .env sudah cukup. Docker secrets overkill. Yang penting: pisahkan secret dari kode dan .gitignore .env.
# Pakai env_file untuk grouping (lebih bersih dari pada environment list)
services:
api:
env_file:
- .env
- .env.local # optional, override untuk environment spesifik
environment:
- APP_ENV=local
9. Image Tag Stabil — Hindari latest
#
Tag latest adalah default yang berbahaya — image di balik tag itu bisa berubah sewaktu-waktu tanpa kamu sadari. Hari ini aplikasimu jalan, besok tiba-tiba broken karena upstream release versi mayor.
# BENAR: pin ke versi spesifik
services:
db:
image: postgres:16.2-alpine # major.minor.patch + variant
cache:
image: redis:7.2-alpine
api:
image: myapp/api:1.4.2-alpine
# ANTI-PATTERN: pakai latest
services:
db:
image: postgres:latest # bisa berubah kapan saja
cache:
image: redis # default-nya latest, sama berbahayanya
# KURANG BAGUS: terlalu longgar
services:
db:
image: postgres:16 # dapat minor/patch update otomatis
# Cek tag image yang tersedia
docker search postgres --limit 5
# Lihat semua tag di Docker Hub: https://hub.docker.com/_/postgres/tags
# BENAR: Dockerfile juga pin base image
FROM node:20.11-alpine # BUKAN node:latest atau node:20
Pattern terbaik untuk tag:
- Production:
image:postgres:16.2.1-alpine(full version)- Staging:
image:postgres:16.2-alpine(minor pinned)- Local dev:
image:postgres:16-alpine(major pinned) — masih oke, asalkan kamu aware- Hindari:
latest, tanpa tag, atau hanya major (postgres:16dapat minor update)
Prinsip ini berlaku dua arah: baik image bahasa/tool (Node, Python, Go) maupun image aplikasi (yang kamu build sendiri). Untuk aplikasi internal, tag dengan versi aplikasi (misalnya 1.4.2) plus label environment (misalnya 1.4.2-staging).
# Lihat tag dan digest image (untuk reproducibility penuh)
docker pull postgres:16.2-alpine
docker inspect postgres:16.2-alpine | jq '.[0].RepoDigests'
# Pin ke digest untuk full reproducibility
# image: postgres@sha256:abc123...
10. Logging Configuration untuk Rotasi Log #
Container default-nya pakai json-file driver yang tidak ada rotasi otomatis. Log bisa memenuhi disk dalam hitungan hari, terutama di development dengan log yang verbose.
# BENAR: set logging limit
services:
api:
logging:
driver: json-file
options:
max-size: \"10m\" # max 10MB per file
max-file: \"3\" # simpan max 3 file (30MB total)
# ANTI-PATTERN: tanpa logging config
services:
api:
# Default: unlimited json-file
# Bisa penuhi disk, susah di-trace, susah di-rotate
# Lihat log dengan limit
docker compose logs --tail=100 api
# Follow log real-time
docker compose logs -f api
# Lihat ukuran log saat ini
ls -lah ~/.docker/containers/*/\\(api\\)*.log
# KONSEKUENSI: log tanpa limit memenuhi disk
# /var/lib/docker/containers/<id>/<id>-json.log bisa sampai GB
# Disk penuh → Docker tidak bisa start container baru
# Harus manual: docker system prune --volumes
# Untuk development, output ke STDOUT saja — biarkan `docker compose logs` yang handle
services:
api:
# Di dalam container, app log ke STDOUT/STDERR, bukan ke file
# Docker compose logs akan capture dan tampilkan
Untuk local development, logging ke STDOUT adalah pola yang paling clean. App-mu log ke console, Docker capture, dan docker compose logs menampilkan. Tidak perlu setup logrotate, file rotation, atau external aggregator. Kalau butuh persistensi log (jarang di development), pakai volume mount ke direktori log host.
Anti-Pattern yang Harus Dihindari #
Berikut adalah anti-pattern paling umum di local development yang harus kamu waspadai.
1. Pakai latest untuk Semua Image
#
# ✗ JANGAN
services:
db:
image: postgres
cache:
image: redis
# ✓ BENAR: pin ke versi spesifik
services:
db:
image: postgres:16.2-alpine
cache:
image: redis:7.2-alpine
Bisa berubah sewaktu-waktu. Build hari ini bisa berbeda dari build besok. Debugging jadi nightmare karena kamu tidak yakin image mana yang sedang jalan.
2. Hardcode Secret di Compose File #
# ✗ JANGAN
services:
db:
environment:
- POSTGRES_PASSWORD=supersecret
# ✓ BENAR: pakai .env (di-gitignore)
services:
db:
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
Secret di file yang di-commit ke Git akan tersimpan selamanya di history. Bahkan setelah kamu hapus di commit berikutnya, history masih ingat.
3. Tidak Pakai Healthcheck #
# ✗ JANGAN
services:
api:
depends_on:
- db
- cache
# depends_on TIDAK menunggu service siap, hanya start order
# ✓ BENAR: tambah healthcheck + condition: service_healthy
services:
db:
healthcheck:
test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]
interval: 10s
timeout: 5s
retries: 5
api:
depends_on:
db:
condition: service_healthy
depends_on saja unreliable. Service dependent bisa start sebelum dependency benar-benar siap — error connection refused di awal startup.
4. Bind Mount untuk Data Persisten #
# ✗ JANGAN untuk development
services:
db:
volumes:
- /var/lib/postgresql/data:/var/lib/postgresql/data
# ✓ BENAR: pakai named volume
services:
db:
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Bind mount ke absolute path rapuh — pindah mesin, pindah OS, hilang. Named volume yang di-manage Docker stabil di semua environment.
5. Container Jalan sebagai Root #
# ✗ JANGAN
services:
api:
# default: jalan sebagai root (UID 0)
# ✓ BENAR: explicit non-root user
services:
api:
user: \"1000:1000\"
Default banyak image jalan sebagai root. Override dengan user: di compose atau USER di Dockerfile. Di local development, ini juga mencegah permission drama di bind mount.
6. Pakai sleep untuk Tunggu Dependency
#
# ✗ JANGAN
#!/bin/sh
sleep 30 # magic number, race condition, lambat
node server.js
# ✓ BENAR: retry logic di aplikasi, atau pakai healthcheck + depends_on
Sleep di entrypoint adalah band-aid yang tidak reliable. Pakai healthcheck + condition: service_healthy — atau retry logic di aplikasi (yang juga production-ready).
7. Expose Semua Port ke Host #
# ✗ JANGAN
services:
db:
ports:
- \"5432:5432\" # tidak perlu diakses dari host
cache:
ports:
- \"6379:6379\"
# ✓ BENAR: hanya expose yang memang perlu
services:
db:
expose:
- \"5432\" # hanya accessible di internal network
Setiap port yang di-expose ke host adalah attack surface. Hanya expose service yang memang perlu di-reach dari host (API, frontend). Database, cache, message broker — biarkan internal.
8. Logging Tanpa Limit #
# ✗ JANGAN
services:
api:
# default: json-file tanpa limit
# ✓ BENAR: set max-size + max-file
services:
api:
logging:
driver: json-file
options:
max-size: \"10m\"
max-file: \"3\"
Log tanpa limit memenuhi disk dalam hitungan hari (terutama di development dengan debug verbose). Set limit atau output ke STDOUT.
Checklist Review Local Development #
Sebelum file Compose-mu dianggap siap untuk local development, jalankan checklist ini.
DOCKERFILE:
□ Base image di-pin ke versi spesifik (bukan :latest)
□ Multi-stage build untuk production
□ Dockerfile.dev terpisah dari Dockerfile production
□ Non-root user di-set di production stage
□ .dockerignore exclude file yang tidak perlu (.git, node_modules, dll)
COMPOSE:
□ Override file untuk development (docker-compose.override.yml)
□ Service names deskriptif (api, db, cache, queue, web)
□ env_file untuk grouping environment variables
□ .env di-gitignore, .env.example di-commit
□ Naming convention konsisten
□ Komentar menjelaskan WHY (bukan WHAT)
□ docker compose config valid (tidak ada warning)
HEALTHCHECK:
□ Healthcheck untuk service yang punya dependent (db, cache, queue)
□ Perintah healthcheck ringan dan cepat
□ interval/timeout/retries di-tune dengan wajar
□ depends_on pakai condition: service_healthy
□ Tidak ada sleep di entrypoint untuk tunggu dependency
VOLUME:
□ Bind mount untuk source code (hot reload)
□ Named volume untuk data persisten (db, cache state)
□ Anonymous volume untuk cache yang akan di-rebuild (node_modules, target)
□ Tidak ada bind mount ke absolute path host untuk data
□ Backup strategy untuk data penting (opsional di dev)
NETWORK:
□ Network isolation sesuai peran (frontend/backend)
□ Database di network internal (tidak reachable dari web)
□ Hanya expose port yang memang perlu ke host
□ Inter-service communication pakai service name (bukan IP)
SECURITY:
□ Tidak ada hardcoded secret di compose file
□ Container jalan sebagai non-root user
□ Image base image sekecil mungkin (alpine/slim)
□ Resource limits di-set untuk setiap service
□ Logging configuration dengan max-size dan max-file
Ringkasan #
- Override file — pisahkan konfigurasi development dengan base.
docker-compose.override.ymlauto-load saatdocker compose up.- Healthcheck —
depends_onhanya start order, bukan readiness. Tambah healthcheck +condition: service_healthyuntuk service dependent.- Bind mount untuk source code (hot reload), named volume untuk data persisten, anonymous volume untuk cache yang akan di-rebuild.
- Named volume untuk database, cache state, upload files. Jangan bind-mount ke absolute path host.
- Non-root user —
user: \"1000:1000\"di compose atauUSERdi Dockerfile. Match UID dengan host untuk hindari permission drama.- Resource limits —
deploy.resources.limitsatau top-levelmem_limit/cpus. Alokasi sesuai resource laptop-mu.- Network isolation — pisahkan frontend/backend. Database di network internal, tidak di-expose ke host.
- Secret management — pakai
.env(di-gitignore) atausecrets:. JANGAN hardcode di compose file yang di-commit.- Image tag stabil — pin ke
major.minor.patch + variant. Hindarilatestdan major-only tag.- Logging configuration — set
max-size+max-fileuntuk rotasi otomatis. Output ke STDOUT, biarkandocker compose logsyang handle.- Checklist — Dockerfile, Compose, Healthcheck, Volume, Network, Security. Run review sebelum setiap commit ke main branch.
- Anti-pattern:
latesttag, hardcoded secret, no healthcheck, bind mount untuk data, root user, sleep di entrypoint, expose semua port, log tanpa limit.- Local development ≠ production — fokus pada DX dan iteration speed, bukan image size atau security hardening maksimal.
- Iterasi bertahap — terapkan satu atau dua prinsip dulu, rasakan improvement-nya, lalu tambah yang lain.
- Best practice adalah consistency — pilih pattern dan terapkan di semua project. Mixing pattern bikin onboarding developer baru jadi sulit.
← Sebelumnya: Angular