Saya pernah bergabung di proyek yang punya docker-compose.yml sepanjang 400 baris. Tidak ada komentar. Tidak ada urutan yang masuk akal. Port-nya hardcoded semua. Setiap kali onboarding developer baru, butuh setengah hari hanya untuk membuat environment-nya jalan.
Itu bukan masalah Docker-nya. Itu masalah tidak ada yang pernah duduk serius memikirkan struktur dari docker compose local development setup mereka.
Artikel ini bukan tutorial "apa itu Docker Compose" — kalau kamu belum tahu itu, baca dulu dokumentasi resminya. Di sini saya mau berbagi pendekatan konkret yang saya pakai untuk proyek solo maupun tim kecil: file yang bisa dibaca manusia, environment yang reproducible, dan workflow yang tidak membuatmu ingin lempar laptop.
Kenapa Setup Default Docker Compose Itu Masalah
Docker Compose versi terbaru (v2, yang sudah bundled di Docker Desktop 4.x ke atas) sudah jauh lebih baik dari v1. Tapi mayoritas tutorial di internet masih mengajarkan pola lama: satu file docker-compose.yml raksasa, semua config di situ, depends_on yang tidak benar-benar menunggu service siap, dan secret yang di-hardcode langsung di YAML.
Masalah nyatanya:
- Port collision — developer A pakai port 5432 untuk Postgres, developer B juga. Konflik.
- State yang tidak konsisten — volume tidak di-prune, container lama masih jalan, data stale.
- Tidak ada pemisahan antara dev dan CI — config yang sama dipakai di dua konteks berbeda, padahal kebutuhannya beda.
- Secret bocor —
POSTGRES_PASSWORD=password123langsung di file yang masuk ke Git.
Semua ini bisa diatasi dengan struktur yang lebih disiplin.
Struktur File yang Saya Pakai
Ini layout direktori yang saya pakai untuk proyek backend Node.js + Postgres + Redis:
project-root/
├── docker-compose.yml # base config, shared services
├── docker-compose.override.yml # dev-specific overrides (gitignored atau tidak)
├── docker-compose.ci.yml # untuk CI pipeline
├── .env.example # template, masuk Git
├── .env # actual secrets, TIDAK masuk Git
└── services/
├── app/
│ └── Dockerfile
└── worker/
└── Dockerfile
Konsepnya: docker-compose.yml adalah definisi apa yang ada. docker-compose.override.yml adalah bagaimana kita menjalankannya di lokal. Docker Compose secara otomatis merge dua file ini kalau kamu jalankan docker compose up tanpa flag tambahan.
Untuk CI, kamu eksplisit:
docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d
Anatomy docker-compose.yml yang Bersih
Ini contoh docker-compose.yml base yang saya anggap wajar:
# docker-compose.yml
version: "3.9"
services:
app:
build:
context: ./services/app
dockerfile: Dockerfile
environment:
NODE_ENV: ${NODE_ENV:-development}
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
networks:
- backend
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- backend
cache:
image: redis:7-alpine
volumes:
- redis_data:/data
networks:
- backend
volumes:
postgres_data:
redis_data:
networks:
backend:
driver: bridge
Beberapa hal yang sengaja saya lakukan:
- Tidak ada port yang di-expose di base file. Port mapping masuk ke override file. Ini mencegah konflik kalau ada yang jalankan beberapa proyek sekaligus.
healthcheckdi Postgres.depends_ontanpacondition: service_healthyitu bohong — container bisa "started" tapi Postgres belum siap terima koneksi.- Semua credential dari environment variable. Tidak ada yang hardcoded.
- Network eksplisit. Bukan karena wajib, tapi karena lebih mudah di-debug kalau ada masalah network isolation.
Override File untuk Development Lokal
# docker-compose.override.yml
services:
app:
volumes:
- ./services/app:/app # hot reload
- /app/node_modules # jangan overwrite node_modules dari container
ports:
- "3000:3000"
command: npm run dev # nodemon atau tsx watch
environment:
DEBUG: "app:*"
db:
ports:
- "5432:5432" # expose ke host untuk akses dari TablePlus/DBeaver
cache:
ports:
- "6379:6379"
File ini boleh masuk Git atau tidak, tergantung tim. Kalau semua developer pakai setup yang sama, masukkan. Kalau ada yang pakai port berbeda atau butuh config personal, tambahkan ke .gitignore dan sediakan .override.example.yml.
Trik volume - /app/node_modules itu penting. Tanpa itu, volume mount dari host akan menimpa node_modules yang ada di dalam container — dan kalau host kamu macOS atau Windows, binary native module-nya bisa salah arsitektur.
File .env yang Benar
.env.example yang masuk Git:
# .env.example
NODE_ENV=development
POSTGRES_USER=appuser
POSTGRES_PASSWORD=changeme
POSTGRES_DB=appdb
.env yang tidak masuk Git (tambahkan ke .gitignore):
# .env — JANGAN COMMIT
NODE_ENV=development
POSTGRES_USER=appuser
POSTGRES_PASSWORD=s3cur3p4ss!
POSTGRES_DB=appdb
Docker Compose otomatis membaca .env dari direktori yang sama dengan file compose-nya. Tidak perlu --env-file flag kecuali kamu mau file-nya di lokasi lain.
Satu hal yang sering diabaikan: jangan pakai nama variable yang bentrok dengan variable sistem. USER, HOME, PATH — itu sudah ada di environment host kamu. Kalau kamu set USER=postgres di .env, hal aneh bisa terjadi.
Workflow Sehari-hari yang Masuk Akal
Ini command yang saya pakai hampir setiap hari, bukan yang ada di tutorial:
# Mulai semua service, rebuild kalau ada perubahan Dockerfile
docker compose up --build -d
# Lihat log app saja, bukan semua service
docker compose logs -f app
# Masuk ke container database untuk debugging
docker compose exec db psql -U appuser -d appdb
# Reset database — hapus volume, recreate
docker compose down -v && docker compose up -d
# Lihat resource usage per container
docker stats $(docker compose ps -q)
Satu kebiasaan yang menghemat banyak waktu: jangan pernah jalankan docker compose down tanpa tahu konsekuensinya. down tanpa flag akan menghapus container dan network tapi bukan volume. down -v menghapus volume juga — data Postgres kamu hilang. Saya sudah pernah kena ini lebih dari sekali.
Menangani Service yang Butuh Waktu Lama Startup
depends_on dengan condition: service_healthy itu bagus, tapi tidak semua image punya healthcheck bawaan. Contoh untuk service yang tidak punya healthcheck:
services:
app:
depends_on:
migration:
condition: service_completed_successfully
migration:
image: your-app-image
command: npm run migrate
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
Pola ini menjalankan migration sebagai service terpisah yang harus selesai (service_completed_successfully) sebelum app utama start. Lebih bersih dari script entrypoint yang penuh sleep 5 && npm run migrate.
condition: service_completed_successfully baru tersedia di Docker Compose v2.1 ke atas. Kalau tim kamu masih pakai v1, ini tidak akan jalan.
Perbandingan Pendekatan: Satu File vs Multi-File
| Aspek | Satu File | Multi-File (base + override) |
|---|---|---|
| Kesederhanaan awal | ✅ Mudah | ❌ Perlu setup lebih |
| Skalabilitas | ❌ Cepat jadi berantakan | ✅ Terstruktur |
| Reuse untuk CI | ❌ Perlu duplikasi atau if-else | ✅ File CI terpisah |
| Onboarding developer baru | ❌ Harus baca semua | ✅ Override jelas tujuannya |
| Debugging config | ❌ Semua campur | ✅ Jelas mana yang dev-only |
Untuk proyek solo yang sederhana (satu app, satu database), satu file masih oke. Begitu ada lebih dari 3 service atau butuh CI yang berbeda, multi-file jauh lebih masuk akal.
Integrasi dengan Makefile
Ini opsional tapi saya selalu pakai. Makefile sebagai interface yang konsisten:
.PHONY: up down logs shell db-shell reset
up:
docker compose up --build -d
down:
docker compose down
logs:
docker compose logs -f app
shell:
docker compose exec app sh
db-shell:
docker compose exec db psql -U $${POSTGRES_USER} -d $${POSTGRES_DB}
reset:
@echo "WARNING: Ini akan menghapus semua data!"
@read -p "Lanjut? (y/N): " confirm && [ "$$confirm" = "y" ]
docker compose down -v
docker compose up --build -d
Sekarang make up, make logs, make reset — semua orang di tim pakai interface yang sama. Tidak perlu ingat flag apa yang harus dipakai.
Kalau kamu tertarik dengan cara saya mengelola dotfiles dan tooling sehari-hari, ada pembahasan lebih detail di artikel tentang setup terminal dan shell untuk developer.
Kesimpulan
Docker compose local development setup yang baik bukan soal panjang atau pendeknya file YAML. Ini soal kejelasan: siapa yang baca file ini 6 bulan lagi (kemungkinan besar kamu sendiri) harus langsung mengerti apa yang terjadi.
Tiga hal yang bisa kamu lakukan besok:
- Pisahkan base file dan override file kalau kamu belum melakukannya.
- Tambahkan healthcheck ke database service dan pakai
condition: service_healthydidepends_on. - Pindahkan semua credential ke
.envdan pastikan.envada di.gitignore.
Kalau kamu mau setup yang lebih jauh — misalnya integrasi dengan Traefik untuk local HTTPS atau multi-project networking — itu topik yang lebih dalam. Tapi tiga langkah di atas sudah cukup untuk membuat docker compose local development setup kamu jauh lebih tidak menyebalkan dari rata-rata yang ada di luar sana.
Coba jalankan docker compose config setelah kamu merge semua file — itu akan menampilkan konfigurasi final yang sudah di-resolve. Berguna untuk debugging dan memastikan variable substitution-nya benar.