Kebanyakan developer pernah di posisi ini. Clone repo, install Node, Postgres, Redis, dan semua yang diminta README. Setengah jam kemudian masih debugging kenapa koneksi database ditolak, atau kenapa port Redis bentrok dengan sesuatu yang sudah jalan di mesin. Terus pull branch baru, dan siklusnya mulai lagi.
Docker Compose solve masalah ini. Satu file, satu command, semua service spinning up secara identik di mesin setiap developer. Nggak ada lagi "works on my machine."
Prerequisites
- Docker Engine 24+ dan Docker Compose v2 (cek dengan
docker versiondandocker compose version) - Sebuah Node.js project (project baru juga bisa)
- Familiar dengan terminal
Struktur Project
Mulai dari Node.js app simpel yang connect ke PostgreSQL dan Redis:
my-app/
src/
index.js
package.json
docker-compose.yml
.env
.dockerignore
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js"
},
"dependencies": {
"express": "^4.21.0",
"pg": "^8.13.0",
"ioredis": "^5.4.0"
}
}
// src/index.js
const express = require('express');
const { Pool } = require('pg');
const Redis = require('ioredis');
const app = express();
const redis = new Redis({ host: 'redis', port: 6379 });
const pool = new Pool({
host: 'postgres',
port: 5432,
database: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
});
app.get('/', async (req, res) => {
await redis.set('hits', (await redis.get('hits') || 0) + 1);
const hits = await redis.get('hits');
const dbResult = await pool.query('SELECT NOW()');
res.json({
message: 'Hello from Docker Compose!',
hits,
serverTime: dbResult.rows[0].now,
});
});
app.listen(3000, () => console.log('Running on port 3000'));
Perhatikan database host-nya postgres, bukan localhost. Di dalam Docker Compose, service satu sama lain berkomunikasi pakai nama service. Kalau kamu pakai localhost, app bakal connect ke dirinya sendiri dan gagal.
docker-compose.yml
Ini file lengkapnya, nanti kita bedah satu per satu:
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- ./src:/app/src
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=myapp
- DB_USER=postgres
- DB_PASSWORD=postgres
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- devnet
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d myapp"]
interval: 5s
timeout: 3s
retries: 5
networks:
- devnet
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- devnet
volumes:
pgdata:
networks:
devnet:
Penjelasan tiap section
Service app. Build Dockerfile (dijelaskan di bawah), map port 3000, dan mount ./src ke dalam container. Volume mount inilah yang bikin hot reload jalan. Edit file di local, dan process yang running di dalam container langsung nangkep perubahannya.
Service postgres. Pakai official image Alpine. Section volumes nyimpen data di antara restart, jadi kamu nggak kehilangan database tiap kali docker compose down. Healthcheck memastikan app nggak coba connect sebelum Postgres siap.
Service redis. Pola yang sama. Image Alpine ringan, healthcheck pakai redis-cli ping.
depends_on dengan condition. Ini bagian kuncinya. Tanpa condition: service_healthy, Docker cuma cek apakah container sudah start, bukan apakah service di dalamnya benar-benar menerima koneksi. App kamu bakal coba connect, gagal, dan crash.
Dockerfile
Kamu butuh Dockerfile untuk service app:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
Nggak pakai multi-stage build di sini. Ini local development, bukan production. Kita tetap simpel. Flag --watch di script dev me-restart Node process saat file berubah.
Tambah .dockerignore supaya file yang nggak perlu nggak ikut ke build context:
node_modules
npm-debug.log
.git
.env
.env.*
Environment Variable
Simpan secret di luar docker-compose.yml. Pakai file .env di root project:
# .env
DB_HOST=postgres
DB_PORT=5432
DB_NAME=myapp
DB_USER=postgres
DB_PASSWORD=postgres
Docker Compose otomatis baca file ini dan inject nilainya ke service yang referensinya pakai syntax ${VARIABLE_NAME}. Kalau nggak mau pakai file .env, kamu bisa set variabelnya inline di docker-compose.yml atau export di shell.
Jalankan Stack-nya
# Build dan start semuanya
docker compose up --build
# Jalankan di background
docker compose up --build -d
# Lihat log
docker compose logs -f app
# Stop semuanya
docker compose down
# Stop dan hapus volume (database fresh)
docker compose down -v
Jalankan docker compose up --build untuk pertama kali. Start berikutnya tanpa perubahan code di Dockerfile skip step build. Tambah -d buat jalanin semua di background.
docker compose down -v hapus database. Berguna kalau kamu butuh clean slate. Tanpa -v, data kamu persist di antara restart.
Hot Reload dalam Aksi
Volume mount ./src:/app/src yang ngurusin beratnya di sini. Saat kamu edit src/index.js di host, perubahannya langsung terrefleksi di dalam container. Karena dev script pakai node --watch, Node detect perubahan file dan restart.
Coba ini:
- Start stack dengan
docker compose up - Buka
src/index.js - Ganti response message
- Simpan file
- Hit
http://localhost:3000lagi
Response baru muncul tanpa restart container manapun. Full cycle rebuild-and-restart cuma butuh waktu kurang dari satu detik.
Connect ke Service dari Host
Mapping ports bikin service accessible dari luar Docker. Connect ke Postgres dari pgAdmin, Tableau, atau psql di local:
psql -h localhost -p 5432 -U postgres -d myapp
Begitu juga untuk Redis:
redis-cli -h localhost -p 6379
Ini berguna untuk debugging. Kamu bisa inspeksi state database sambil app running tanpa perlu install Postgres atau Redis di mesin.
Kesalahan yang Sering Muncul
Container start tapi app crash dengan connection refused. App mencoba connect ke localhost atau 127.0.0.1. Di dalam container, localhost merujuk ke container itu sendiri, bukan host atau container lain. Pakai nama service (postgres, redis) sebagai hostname.
Port sudah dipakai. Kalau Postgres atau Redis sudah jalan di host, mapping ports gagal. Stop service local atau ganti host port: "5433:5432 map port 5432 di container ke port 5433 di host.
Build pertama lambat. Docker download base image di run pertama. Build berikutnya pakai cache dan jauh lebih cepat.
Data hilang setelah docker compose down. Tanpa named volume (kayak pgdata), data disimpan di writable layer container dan terhapus saat container di-remove. Section volumes di service Postgres mencegah ini.
Health check gagal. Kalau pg_isready atau redis-cli ping return error, dependent service nggak pernah start. Pastikan database name dan user di healthcheck match dengan nilai POSTGRES_DB dan POSTGRES_USER.
Memperluas Setup
Setelah dasarnya jalan, pertimbangkan tambahan ini:
- Tambah worker service (misal Bull queue consumer) dengan copy konfigurasi app dan ganti
CMD - Mount sertifikat TLS untuk service yang butuh
- Tambah reverse proxy kayak Nginx atau Caddy di depan app buat HTTPS lokal
- Pakai Docker Compose profiles buat optional jalankan extra service:
docker compose --profile debug up
Kapan Harus Berhenti Pakai Ini
Docker Compose works well untuk local development dan deployment sederhana. Dia nggak handle orkestrasi, auto-scaling, atau rolling update. Kalau kamu butuh fitur-fitur itu, pindah ke Kubernetes atau managed platform. Tapi untuk daily development, Compose susah dikalahkan soal kesederhanaan.