Dockerisasi Aplikasi Web: Membuat Environment Development yang Konsisten
Kenapa Docker?
Sebelum docker, setiap developer di tim saya punya setup berbeda: ada yang PHP 7.4, ada yang 8.2. Ada yang MySQL 5.7, ada yang 8.0. Akibatnya: "works on my machine" syndrome. Bug hanya muncul di satu mesin developer, susah debug.
Docker menyelesaikan ini dengan mengisolasi environment. Satu Dockerfile, satu image, jalan identik di mana pun.
1. Dockerfile untuk Node.js Backend
# backend/Dockerfile
FROM node:20-alpine
WORKDIR /app
# Copy package files dulu (cache layer)
COPY package*.json ./
RUN npm ci --production=false
# Copy source code
COPY . .
# Build TypeScript
RUN npm run build
# Hapus devDependencies untuk production image
RUN npm prune --production
EXPOSE 3000
CMD ["node", "dist/server.js"]
2. Multi-stage Build untuk Image Lebih Kecil
# backend/Dockerfile.multi
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --production
# Stage 2: Runtime
FROM node:20-alpine AS runtime
WORKDIR /app
# Copy hanya yang diperlukan dari stage build
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# User non-root untuk security
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Image dari 850MB → 180MB. Build sekali, pakai untuk staging dan production.
3. docker-compose.yml Full-Stack
# docker-compose.yml
version: '3.9'
services:
# ===== Backend API =====
backend:
build: ./backend
container_name: app-backend
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://app:app@db:5432/app
- REDIS_URL=redis://redis:6379
ports:
- "3000:3000"
volumes:
- ./backend:/app
- /app/node_modules
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: npm run dev # Hot reload dengan nodemon
# ===== Frontend (Vite + Vue/React) =====
frontend:
build: ./frontend
container_name: app-frontend
environment:
- VITE_API_URL=http://localhost:3000
ports:
- "5173:5173"
volumes:
- ./frontend:/app
- /app/node_modules
command: npm run dev -- --host # expose ke host
# ===== Database =====
db:
image: postgres:16-alpine
container_name: app-db
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d # init scripts
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 5
# ===== Redis Cache =====
redis:
image: redis:7-alpine
container_name: app-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
# ===== Nginx Reverse Proxy =====
nginx:
image: nginx:alpine
container_name: app-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/ssl:/etc/nginx/ssl
depends_on:
- backend
- frontend
# ===== MailHog untuk dev email =====
mailhog:
image: mailhog/mailhog
container_name: app-mailhog
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
volumes:
postgres_data:
redis_data:
4. Konfigurasi Nginx Reverse Proxy
# nginx/conf.d/app.conf
upstream backend {
server backend:3000;
}
upstream frontend {
server frontend:5173;
}
server {
listen 80;
server_name localhost;
# API proxy
location /api/ {
proxy_pass http://backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Frontend dev server
location / {
proxy_pass http://frontend/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # WebSocket untuk HMR
}
}
5. Volume Patterns
Anonymous Volume: jangan sync node_modules
volumes:
- ./backend:/app # sync project folder
- /app/node_modules # anonymous: gunakan dari container
Named Volume: persistent data
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data: # persistent di host
Bind Mount: hot reload dev
volumes:
- ./backend/src:/app/src # sync hanya src
6. .dockerignore Penting!
# .dockerignore
node_modules
npm-debug.log
git
gitignore
Dockerfile*
docker-compose*
dockerignore
.env
.env.*
coverage
.nyc_output
.vscode
.idea
*.md
Tanpa .dockerignore, build context bisa 500MB+ (karena node_modules ikut dikirim). Dengan ignore: 50MB context.
7. Development Workflow
# Clone repo
git clone https://github.com/myorg/myapp.git
cd myapp
# First run: build & start
docker-compose up -d --build
# Cek status
docker-compose ps
# Lihat log satu service
docker-compose logs -f backend
# Masuk container untuk debug
docker-compose exec backend sh
# Rebuild setelah ubah Dockerfile
docker-compose up -d --build backend
# Stop semua
docker-compose down
# Stop + hapus volumes (hati-hati!)
docker-compose down -v
8. Database Migration otomatis
# docker-compose.override.yml
services:
migrate:
build: ./backend
depends_on:
db:
condition: service_healthy
environment:
- DATABASE_URL=postgresql://app:app@db:5432/app
command: npm run db:migrate
restart: "no" # exit setelah migration selesai
docker-compose run --rm migrate
docker-compose up -d backend
9. Production Compose
# docker-compose.prod.yml
version: '3.9'
services:
backend:
build: ./backend
restart: always
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
deploy:
replicas: 2
resources:
limits:
memory: 512M
cpus: '0.5'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
deploy:
resources:
limits:
memory: 1G
nginx:
image: nginx:alpine
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/prod.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- ./static:/var/www/static:ro
10. Tips Optimization
Layer Caching
Urutan COPY penting! Yang jarang berubah ditaruh depan.
# ✅ Cache friendly
COPY package*.json ./
RUN npm ci
COPY . . # ubah source code = tidak rebuild npm
# 🚫 Cache killer
COPY . .
RUN npm ci # rebuild setiap source code berubah
.env Handling
# docker-compose.yml
services:
backend:
env_file:
- .env
- .env.local # override local
⚠️ Jangan commit .env berisi secrets! Hanya commit .env.example.
Kesimpulan
Docker bukan "magic" — ia belajar investasi awal yang dibayar cepat. Invest 1 hari belajar Dockerfile + docker-compose, dan setiap developer di tim Anda berjalan dengan environment sama persis. Tidak lagi "works on my machine".
Mulai dari setup sederhana (1 service + 1 db), perlahan tambah redis, nginx, mailhog, monitoring. Akhirnya Anda punya development environment yang konsisten dan lengkap.