Dockerisasi Aplikasi Web: Membuat Environment Development yang Konsisten

I Nyoman Donostia 894 kata
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.

I Nyoman Donostia

I Nyoman Donostia

Fullstack Developer - Bali, Indonesia

Artikel Terkait