Kamu punya dua project: satu butuh Node 16, satu lagi butuh Node 20. Atau PHP 7.4 vs PHP 8.2. Atau PostgreSQL 13 vs 15. Kalau kamu install semua langsung di laptop, cepat atau lambat kamu bakal ketemu error klasik:
Error: The engine "node" is incompatible with this module.
Expected version ">=20.0.0". Got "16.14.0"
nvm, pyenv, phpenv — semua itu solusi, tapi tetap aja ribet kalau kamu harus switch-switch terus. Belum lagi kalau onboarding developer baru: "install ini dulu, terus ini, oh tunggu kamu Windows ya, beda lagi caranya."
Itu masalah yang bikin Docker untuk local development setup jadi pilihan yang masuk akal. Bukan karena hype, tapi karena sekali kamu tulis docker-compose.yml, semua orang di tim — atau kamu sendiri di laptop lain — tinggal jalanin satu perintah.
Kenapa Local Dev Tanpa Docker Nyebelin
Masalah utamanya bukan soal skill. Masalahnya adalah state laptop kamu itu unik. Ada yang install PostgreSQL via Homebrew, ada yang via installer, ada yang via apt. Versinya beda, config defaultnya beda, portnya kadang bentrok.
Terus kamu push ke repo, teman kamu clone, jalanin, langsung error. Kamu bilang "di laptop gue jalan kok." Klasik.
Selain itu:
- Service kayak Redis, RabbitMQ, atau Elasticsearch berat kalau diinstall native dan selalu nyala
- Kalau project selesai, uninstall-nya nggak pernah bersih
- Database development sering terkontaminasi data acak dari eksperimen lama
Docker solve semua ini dengan cara yang straightforward: setiap project punya environment-nya sendiri, terisolasi, dan bisa di-nuke kapan aja tanpa efek samping.
Struktur Project yang Gue Pakai
Sebelum nulis satu baris config pun, struktur folder dulu. Ini yang gue pakai untuk project Express + PostgreSQL + Redis:
my-project/
├── src/
│ └── index.js
├── .env
├── .env.example
├── docker-compose.yml
├── Dockerfile
└── package.json
Simple. Dockerfile untuk app kamu, docker-compose.yml untuk orchestrate semua service.
Dockerfile untuk Development (Bukan Production)
Ini yang sering salah kaprah: banyak tutorial kasih Dockerfile yang sama untuk dev dan production. Padahal beda kebutuhannya.
Untuk development, kamu mau:
- Hot reload (nodemon, air, watchexec, dll)
- Mount source code dari host supaya edit langsung keliatan
- Nggak perlu multi-stage build yang rumit
# Dockerfile
FROM node:20-alpine
WORKDIR /app
# Install dependencies dulu (layer caching)
COPY package*.json ./
RUN npm install
# Install nodemon untuk hot reload
RUN npm install -g nodemon
# Source code di-mount via volume, bukan COPY
# Jadi baris COPY src di sini sengaja dihilangkan
EXPOSE 3000
CMD ["nodemon", "src/index.js"]
Perhatikan: kita tidak COPY . . di sini. Source code akan di-mount sebagai volume dari docker-compose. Ini yang bikin hot reload bisa jalan — kamu edit file di laptop, container langsung detect perubahannya.
docker-compose.yml yang Lengkap
Ini bagian intinya. Satu file ini replace semua manual install service:
# docker-compose.yml
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
volumes:
# Mount source code untuk hot reload
- ./src:/app/src
# Jangan mount node_modules dari host!
- /app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://devuser:devpass@postgres:5432/devdb
- REDIS_URL=redis://redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
postgres:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass
POSTGRES_DB: devdb
volumes:
- postgres_data:/var/lib/postgresql/data
# Seed script otomatis dijalankan saat container pertama kali dibuat
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
Beberapa hal yang perlu dicatat dari config di atas:
condition: service_healthy untuk postgres — ini penting. Tanpa ini, app kamu bisa start sebelum PostgreSQL siap nerima koneksi, dan kamu dapat error koneksi yang bikin bingung padahal bukan bug di code.
Volume trick untuk node_modules — baris - /app/node_modules (tanpa path host) itu intentional. Ini bilang ke Docker: "pakai node_modules yang ada di dalam container, jangan timpa dengan yang dari host." Kalau nggak ada ini, node_modules di laptop kamu (yang mungkin compiled untuk OS yang beda) bakal masuk ke container dan bikin error aneh.
Jalanin dan Verifikasi
Sekali setup selesai, workflow hariannya simpel:
# Pertama kali atau setelah ubah Dockerfile
docker compose up --build
# Hari-hari biasa
docker compose up
# Background (detached)
docker compose up -d
# Lihat logs app saja
docker compose logs -f app
# Masuk ke container app
docker compose exec app sh
# Masuk ke psql
docker compose exec postgres psql -U devuser -d devdb
# Stop semua
docker compose down
# Stop + hapus volume (reset database)
docker compose down -v
Coba akses http://localhost:3000 — kalau app kamu jalan, berarti setup sudah benar.
Gotcha yang Pernah Gue Kena
1. Port sudah dipakai
Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:5432
Ini berarti PostgreSQL native kamu masih nyala di laptop. Solusi cepat:
# macOS
brew services stop postgresql
# Linux
sudo systemctl stop postgresql
Atau ganti port mapping di docker-compose: "5433:5432" — port host 5433, port container tetap 5432. Jangan lupa update DATABASE_URL-nya.
2. File permission di Linux
Kalau kamu di Linux dan tiba-tiba dapat permission error saat container mau nulis file, itu karena user di dalam container (biasanya UID 1000) beda sama user di host. Fix-nya di Dockerfile:
# Tambahkan setelah WORKDIR
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
Atau kalau males, set user: "${UID}:${GID}" di docker-compose service kamu.
3. Hot reload nggak jalan di Linux
nodemon pakai file system events. Di Linux dengan Docker Desktop atau WSL2, kadang events-nya nggak ke-trigger. Fix:
// package.json
"scripts": {
"dev": "nodemon --legacy-watch src/index.js"
}
Flag --legacy-watch bikin nodemon polling instead of event-based. Sedikit lebih boros CPU tapi reliable.
4. docker compose vs docker-compose
Kalau kamu pakai Docker Desktop versi baru, gunakan docker compose (tanpa strip). docker-compose adalah versi lama (v1, Python-based) yang sudah deprecated. Kalau masih pakai yang lama, upgrade Docker Desktop-nya.
Tambahan: Makefile untuk Shortcut
Ketik docker compose exec postgres psql -U devuser -d devdb tiap hari itu melelahkan. Bikin Makefile di root project:
# Makefile
.PHONY: up down logs shell db reset
up:
docker compose up -d
down:
docker compose down
logs:
docker compose logs -f app
shell:
docker compose exec app sh
db:
docker compose exec postgres psql -U devuser -d devdb
reset:
docker compose down -v && docker compose up -d
migrate:
docker compose exec app npm run migrate
Sekarang:
make up # start semua
make db # buka psql
make reset # nuke database dan mulai fresh
Jauh lebih manusiawi.
Langkah Lanjutan
Setup di atas sudah cukup untuk 80% project solo atau tim kecil. Kalau kamu mau lanjut:
Multi-stage Dockerfile — pisahkan Dockerfile dev dan production. Stage production pakai npm ci --only=production dan nggak include devDependencies.
Docker Compose override — pakai docker-compose.override.yml untuk config yang spesifik per developer (misalnya port beda, atau ada service tambahan yang cuma kamu butuhkan). File ini masuk .gitignore.
Traefik sebagai reverse proxy lokal — kalau kamu punya banyak project dan bosan ingat-ingat port, Traefik bisa route berdasarkan hostname. Jadi http://my-project.localhost langsung ke container yang benar.
Healthcheck endpoint di app — tambahkan /health endpoint di app kamu, terus pakai itu di healthcheck docker-compose. Lebih akurat dari sekadar cek apakah port terbuka.
Gue pakai setup Docker untuk local development setup ini di hampir semua project sekarang — dari side project kecil sampai yang udah ada beberapa service. Waktu onboarding developer baru turun dari "setengah hari install-install" jadi "clone repo, make up, selesai."
Kalau kamu stuck di bagian mana pun, cek dulu docker compose logs — 90% masalah jawabannya ada di sana.