Pernah ngalamin situasi ini: kamu clone repo dari teman, jalanin npm install, terus terminal langsung muntah error panjang karena versi Node-nya beda? Atau lebih parah — project jalan di laptop kamu, tapi pas di-deploy ke server production langsung meledak karena versi Python atau library-nya nggak sama?
Itu bukan bug kode. Itu bug environment.
Solusinya bukan "install ulang semuanya" atau "semoga aja sama." Solusinya adalah Docker untuk development environment — supaya environment kamu identik di mana pun project itu dijalankan.
Artikel ini akan tunjukin cara setup Docker buat development lokal yang beneran gue pakai sehari-hari, lengkap dengan gotcha yang pernah bikin gue stuck berjam-jam.
Kenapa Environment Lokal Tanpa Docker Nyebelin
Masalah klasiknya begini: kamu punya beberapa project yang butuh versi berbeda dari tool yang sama.
- Project A butuh Node 16
- Project B butuh Node 20
- Project C butuh Python 3.9, project D butuh Python 3.12
- Satu project butuh PostgreSQL 14, yang lain PostgreSQL 16
Tanpa Docker, kamu harus pakai version manager kayak nvm, pyenv, atau asdf — dan itu oke, tapi tetap ada batasnya. Dependency sistem (library C, binary tambahan, environment variable) masih bisa konflik.
Dengan Docker, tiap project punya container-nya sendiri. Isolasi penuh. Nggak ada "works on my machine" lagi — karena machine-nya sama, yaitu container itu.
Konsep Dasar yang Perlu Kamu Pahami Dulu
Sebelum langsung ke kode, ada tiga konsep yang wajib nempel di kepala:
Image — blueprint container. Kamu build sekali, bisa dipakai berkali-kali. Kayak template VM tapi jauh lebih ringan.
Container — instance yang jalan dari sebuah image. Bisa kamu start, stop, hapus, bikin ulang — tanpa ngaruh ke image-nya.
Volume — cara Docker nge-persist data di luar container. Kalau container dihapus, data di volume tetap ada. Ini penting banget buat database.
Satu lagi: bind mount. Ini yang bikin Docker enak buat development — kamu mount folder project lokal kamu ke dalam container, jadi perubahan kode langsung kelihatan tanpa harus rebuild image.
Setup Docker Compose untuk Project Node.js + PostgreSQL
Gue ambil contoh yang paling umum: web app Node.js yang butuh database PostgreSQL.
Struktur folder project-nya:
my-app/
├── src/
│ └── index.js
├── package.json
├── Dockerfile
└── docker-compose.yml
Mulai dari Dockerfile:
# Dockerfile
FROM node:20-alpine
# Set working directory di dalam container
WORKDIR /app
# Copy package files dulu (biar layer cache-nya efisien)
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy sisa kode
COPY . .
# Port yang di-expose
EXPOSE 3000
# Command default — tapi ini di-override di docker-compose untuk dev
CMD ["node", "src/index.js"]
Sekarang docker-compose.yml:
# docker-compose.yml
version: '3.9'
services:
app:
build: .
ports:
- "3000:3000"
volumes:
# Bind mount: folder lokal → container
- .:/app
# Jangan timpa node_modules container dengan yang lokal
- /app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://devuser:devpass@db:5432/devdb
depends_on:
db:
condition: service_healthy
command: npx nodemon src/index.js
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=devpass
- POSTGRES_DB=devdb
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
Contoh src/index.js yang sederhana buat test:
// src/index.js
const http = require('http');
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const server = http.createServer(async (req, res) => {
if (req.url === '/health') {
try {
await pool.query('SELECT 1');
res.writeHead(200);
res.end(JSON.stringify({ status: 'ok', db: 'connected' }));
} catch (err) {
res.writeHead(500);
res.end(JSON.stringify({ status: 'error', message: err.message }));
}
return;
}
res.writeHead(200);
res.end('Hello from Docker!');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
Pastikan package.json punya dependency yang dibutuhkan:
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"pg": "^8.11.0"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}
Jalanin semuanya dengan satu perintah:
docker compose up --build
Test endpoint health:
curl http://localhost:3000/health
# Output: {"status":"ok","db":"connected"}
Gotcha yang Pernah Bikin Gue Stuck
1. node_modules di bind mount
Ini yang paling sering bikin bingung. Kalau kamu mount seluruh folder project ke container, node_modules yang ada di lokal kamu (yang mungkin di-build untuk OS yang berbeda) akan menimpa node_modules yang ada di dalam container.
Solusinya ada di baris ini di docker-compose.yml:
volumes:
- .:/app
- /app/node_modules # anonymous volume untuk "protect" node_modules
Baris kedua itu bikin Docker pakai node_modules dari dalam container, bukan dari folder lokal kamu. Kalau kamu skip ini, kamu bakal dapat error aneh kayak binary nggak ketemu atau module nggak kompatibel.
2. Service belum ready pas app start
Tanpa healthcheck dan depends_on yang proper, app kamu bisa start sebelum PostgreSQL siap menerima koneksi. Hasilnya error koneksi di detik-detik pertama.
Konfigurasi healthcheck di service db dan condition: service_healthy di service app yang gue tulis di atas mengatasi ini. Docker akan tunggu sampai PostgreSQL beneran siap, baru jalanin app.
3. Perubahan package.json nggak ke-detect
Kalau kamu tambah dependency baru, kamu perlu rebuild image:
docker compose up --build
Banyak yang lupa ini dan heran kenapa module baru nggak ketemu padahal udah npm install di lokal. Ingat: yang jalan di container adalah node_modules di dalam container, bukan di lokal kamu.
4. Port conflict
Kalau kamu dapat error Bind for 0.0.0.0:5432 failed: port is already allocated, berarti ada proses lain (mungkin PostgreSQL yang install langsung di sistem) yang pakai port yang sama.
Solusi cepat: ganti port mapping di docker-compose:
ports:
- "5433:5432" # port lokal 5433, port container tetap 5432
Terus update connection string kamu kalau connect dari luar container:
postgresql://devuser:devpass@localhost:5433/devdb
Tapi kalau connect antar container (app ke db), tetap pakai port 5432 dan hostname db — karena itu network internal Docker.
Perintah Docker yang Sering Gue Pakai
Beberapa command yang gue pakai hampir tiap hari:
# Jalanin semua service (background)
docker compose up -d
# Lihat log real-time service tertentu
docker compose logs -f app
# Masuk ke dalam container (debugging)
docker compose exec app sh
# Jalanin perintah satu kali di dalam container
docker compose exec app npx prisma migrate dev
# Stop semua service
docker compose down
# Stop dan hapus volume (reset database)
docker compose down -v
# Rebuild image tanpa cache
docker compose build --no-cache
Yang paling sering gue pakai adalah docker compose exec — buat jalanin migration, buka psql, atau debug langsung di dalam environment yang sama dengan app.
# Buka psql langsung
docker compose exec db psql -U devuser -d devdb
Optimasi untuk Development yang Lebih Cepat
Multi-stage build untuk production
Kalau project kamu udah mau naik ke production, pisahin Dockerfile untuk dev dan prod:
# Dockerfile
# Stage 1: Development
FROM node:20-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npx", "nodemon", "src/index.js"]
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
CMD ["node", "src/index.js"]
Di docker-compose.yml, target stage yang mana:
services:
app:
build:
context: .
target: development # ganti ke 'production' untuk prod
File .dockerignore
Jangan lupa bikin .dockerignore supaya build lebih cepat dan image lebih kecil:
node_modules
.git
.gitignore
*.log
.env
README.md
Tanpa ini, Docker akan copy node_modules lokal kamu ke dalam build context — yang bisa bikin proses build lambat banget.
Yang Gue Lakuin Selanjutnya
Setup ini udah gue pakai di hampir semua project baru. Hasilnya: onboarding developer baru tinggal git clone + docker compose up --build, selesai. Nggak ada lagi sesi debugging "kok di laptop gue nggak jalan?"
Kalau kamu mau lanjut dari sini, beberapa hal yang worth di-explore:
- Docker untuk project Python/Django — konsepnya sama, tapi ada gotcha soal virtual environment yang perlu dihindari di dalam container.
- Traefik sebagai reverse proxy — kalau kamu jalanin banyak project Docker sekaligus dan mau akses via subdomain, bukan port. Untuk perbandingan lebih detail tentang berbagai solusi hosting dan infrastruktur, this guide on Niagahoster vs Hostinger Indonesia bisa memberikan perspektif tentang pilihan deployment yang tepat.
- Docker Compose profiles — buat pisahin service yang cuma dibutuhkan di kondisi tertentu (misalnya, service monitoring yang cuma jalan kalau dibutuhkan).
Coba dulu setup dasar di atas. Kalau ada error spesifik yang kamu temuin, drop di kolom komentar — kemungkinan besar gue pernah ketemu hal yang sama.