Kamu pernah push code, terus teman satu tim bilang "eh, di gue error nih" — padahal di laptop kamu jalan mulus? Atau lebih parah: staging server tiba-tiba meledak karena versi Node.js-nya beda dua minor version sama lokal kamu?
Itu bukan bug. Itu environment drift. Dan cara paling efektif buat bunuh masalah ini adalah containerize development workflow kamu dari awal, bukan nanti-nanti.
Artikel ini bakal tunjukin setup Docker yang gue pakai sehari-hari untuk project backend — dari struktur file, sampai gotcha yang bikin gue buang dua jam sebelum nemu solusinya.
Kenapa Environment Drift Itu Nyebelin Banget
Masalahnya bukan cuma "versi beda". Ini daftarnya yang bikin pusing:
- Developer A pakai Node 18, Developer B pakai Node 20 — behavior
fetchAPI beda. - PostgreSQL di lokal versi 14, di server versi 15 — ada syntax yang deprecated.
- Library sistem (
libssl,libpq) beda versi antar OS. - Kamu pakai macOS, teammate kamu pakai Ubuntu, CI/CD-nya pakai Alpine.
Solusi lama: README panjang berisi "install ini dulu, terus ini, jangan lupa set env ini". Solusi yang lebih baik: satu docker-compose.yml yang langsung jalan.
Struktur Project yang Gue Pakai
Sebelum nulis satu baris Dockerfile, struktur folder dulu:
my-app/
├── src/
│ └── index.ts
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── docker-compose.override.yml
├── .dockerignore
├── package.json
└── tsconfig.json
Dua Dockerfile — satu untuk development, satu untuk production. Ini penting. Jangan pakai image production yang berat untuk dev loop kamu.
Dockerfile.dev — Buat Development Loop yang Cepat
Ini Dockerfile.dev yang gue pakai untuk project Node.js + TypeScript:
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
# Install dependencies dulu — layer ini di-cache selama package.json nggak berubah
COPY package*.json ./
RUN npm install
# Source code di-mount via volume, bukan di-copy
# Jadi kita nggak perlu rebuild image setiap save file
EXPOSE 3000
CMD ["npx", "ts-node-dev", "--respawn", "--transpile-only", "src/index.ts"]
Perhatikan: source code tidak di-COPY di sini. Kita mount-nya via volume di docker-compose.yml. Ini yang bikin hot-reload jalan.
docker-compose.yml — Orchestrasi Semua Service
# docker-compose.yml
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app
- /app/node_modules # <-- ini penting, lihat bagian gotcha
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://dev:devpass@db:5432/myapp
depends_on:
db:
condition: service_healthy
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: devpass
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev -d myapp"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
Sekarang kamu tinggal:
docker compose up
Satu command. Semua service jalan. Semua developer di tim kamu dapat environment yang identik.
Gotcha #1: node_modules Keoverwrite Volume
Ini yang bikin gue buang dua jam.
Kalau kamu mount seluruh project directory ke /app tanpa pengecualian, folder node_modules yang ada di dalam container bakal keoverwrite sama folder node_modules dari host — yang mungkin kosong atau compiled untuk OS yang beda.
Solusinya ada di baris ini di docker-compose.yml:
volumes:
- .:/app
- /app/node_modules # anonymous volume, "sembunyikan" node_modules dari host
Baris kedua itu bikin Docker pakai anonymous volume untuk /app/node_modules di dalam container — jadi nggak keoverwrite sama mount dari host. node_modules tetap hasil npm install di Alpine, bukan dari macOS kamu.
Gotcha #2: depends_on Bukan Jaminan Service Siap
depends_on di Docker Compose cuma ngecek apakah container sudah start, bukan apakah service di dalamnya sudah siap terima koneksi.
PostgreSQL butuh beberapa detik buat initialize. Kalau app kamu langsung connect saat container baru nyala, kamu bakal dapat error ECONNREFUSED atau role does not exist.
Solusinya: pakai condition: service_healthy + healthcheck seperti di config di atas. Docker bakal nunggu sampai pg_isready return sukses sebelum start service app.
Kalau kamu pakai versi Docker Compose yang lebih lama (v2 syntax), ini nggak work. Pastiin kamu pakai version: '3.9' ke atas.
Dockerfile untuk Production
Jangan deploy image development kamu ke production. Selain besar, ada tool debug dan source map yang nggak perlu ada di sana.
# Dockerfile (production)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production=false
COPY . .
RUN npm run build
# Stage kedua — image final yang lebih kecil
FROM node:20-alpine AS runner
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
Ini multi-stage build. Stage builder compile TypeScript, stage runner cuma ambil output dist/ dan production dependencies. Image finalnya jauh lebih kecil — biasanya dari ~400MB jadi ~80–120MB.
.dockerignore — Jangan Lupa Ini
Banyak yang skip file ini. Akibatnya: Docker context yang dikirim saat build gede banget karena ikut-ikutan node_modules, .git, dan file lain yang nggak perlu.
# .dockerignore
node_modules
.git
.gitignore
*.log
dist
.env
.env.*
README.md
.DS_Store
Dengan .dockerignore yang proper, docker build kamu bakal jauh lebih cepat karena context yang dikirim ke Docker daemon lebih kecil.
Override Config untuk Tiap Developer
Kadang ada developer yang butuh port beda, atau mau tambah service tambahan (misalnya Mailhog untuk testing email) tanpa commit ke repo.
Solusinya: docker-compose.override.yml. Docker Compose otomatis merge file ini dengan docker-compose.yml kalau ada.
# docker-compose.override.yml (jangan di-commit, masuk .gitignore)
services:
app:
ports:
- "3001:3000" # port lokal beda
environment:
- DEBUG=app:*
mailhog:
image: mailhog/mailhog
ports:
- "1025:1025"
- "8025:8025"
Tambahkan docker-compose.override.yml ke .gitignore supaya config personal tiap developer nggak masuk repo.
Workflow Sehari-hari Setelah Setup
Setelah semua terkonfigurasi, workflow harian kamu jadi sesederhana ini:
# Pertama kali atau setelah ada perubahan Dockerfile
docker compose build
# Jalankan semua service
docker compose up
# Jalankan di background
docker compose up -d
# Lihat log service tertentu
docker compose logs -f app
# Masuk ke shell container
docker compose exec app sh
# Jalankan migration database
docker compose exec app npm run migrate
# Stop semua service
docker compose down
# Stop + hapus volume (reset database)
docker compose down -v
Onboarding developer baru? Mereka cukup:
git clone <repo>
cd <repo>
cp .env.example .env
docker compose up
Selesai. Nggak perlu install Node, PostgreSQL, atau Redis di laptop mereka.
Gotcha #3: File Permission di Linux Host
Kalau kamu develop di Linux (bukan macOS atau WSL2), ada masalah permission yang sering muncul: file yang dibuat di dalam container punya owner root, jadi susah diedit dari host.
Solusinya: tambahkan user di Dockerfile.dev:
FROM node:20-alpine
# Buat user dengan UID yang sama dengan user host kamu
ARG UID=1000
RUN adduser -D -u $UID appuser
WORKDIR /app
COPY package*.json ./
RUN npm install
USER appuser
EXPOSE 3000
CMD ["npx", "ts-node-dev", "--respawn", "--transpile-only", "src/index.ts"]
Terus build dengan:
docker compose build --build-arg UID=$(id -u)
Atau set di docker-compose.yml:
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
args:
UID: ${UID:-1000}
Yang Gue Lakuin Selanjutnya
Setup di atas sudah cukup untuk sebagian besar project solo atau tim kecil. Tapi kalau kamu mau push lebih jauh, ini yang gue lagi eksplor:
-
Dev Container (VS Code) —
.devcontainer/devcontainer.jsonsupaya VS Code langsung attach ke container, extension dan semua. Ini bagus kalau tim kamu campuran OS. -
Makefile sebagai shortcut — daripada hafal semua command
docker compose exec, wrap di Makefile:
up:
docker compose up -d
down:
docker compose down
logs:
docker compose logs -f app
shell:
docker compose exec app sh
migrate:
docker compose exec app npm run migrate
- Testcontainers — kalau kamu nulis integration test, library ini spawn container PostgreSQL/Redis real saat test run, terus destroy setelah selesai. Nggak perlu mock database lagi.
Containerize development workflow itu investasi satu-dua jam di awal yang balik modal setiap kali ada developer baru masuk tim, atau setiap kali kamu sendiri setup ulang laptop. Mulai dari docker-compose.yml yang simpel, tambah kompleksitas hanya kalau memang butuh.