Docker Compose Multi-Container Production

Docker Compose in Production: Multi-Service Architecture

RT
Rachel Torres
DevOps Lead
Mar 28, 2025
18 min read

What You'll Learn

A production guide to Docker Compose — multi-service orchestration, environment management, health checks, networking, secrets, scaling, and real-world full-stack application patterns.

Why Docker Compose?

Running a modern application means running multiple services — a web server, a database, a cache, a queue. Docker Compose lets you define all of these in a single YAML file and launch them with one command: docker-compose up -d.

Complete Full-Stack docker-compose.yml

docker-compose.yml — Next.js + Nginx + Postgres + Redis
version: '3.9'

x-common-env: &common-env    # YAML anchor for shared env
  NODE_ENV: production
  TZ: UTC

services:
  # ─── Nginx Reverse Proxy ───
  nginx:
    image: nginx:1.25-alpine
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
      - nginx_cache:/var/cache/nginx
    depends_on:
      app:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - frontend
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  # ─── Node.js Application ───
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
      args:
        NODE_VERSION: "20"
    container_name: app
    expose:
      - "3000"
    environment:
      <<: *common-env
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASS}@postgres:5432/${DB_NAME}
      REDIS_URL: redis://redis:6379/0
      JWT_SECRET: ${JWT_SECRET}
    env_file:
      - .env.production
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped
    networks:
      - frontend
      - backend
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

  # ─── PostgreSQL Database ───
  postgres:
    image: postgres:16-alpine
    container_name: postgres
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: ${DB_NAME}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped
    networks:
      - backend
    deploy:
      resources:
        limits:
          memory: 256M

  # ─── Redis Cache ───
  redis:
    image: redis:7-alpine
    container_name: redis
    command: >
      redis-server
      --appendonly yes
      --appendfsync everysec
      --maxmemory 128mb
      --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped
    networks:
      - backend

  # ─── Background Worker ───
  worker:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    container_name: worker
    command: ["node", "dist/worker.js"]
    environment:
      <<: *common-env
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASS}@postgres:5432/${DB_NAME}
      REDIS_URL: redis://redis:6379/0
    depends_on:
      - postgres
      - redis
    restart: unless-stopped
    networks:
      - backend

# ─── Networks ───
networks:
  frontend:
    name: myapp_frontend
  backend:
    name: myapp_backend
    internal: true   # No external internet access

# ─── Named Volumes ───
volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local
  nginx_cache:
    driver: local

Environment File (.env)

.env — Never commit to Git!
# Database
DB_USER=myapp
DB_PASS=super_secret_password_here
DB_NAME=myapp_production

# Application
JWT_SECRET=your-very-long-jwt-secret-key-here
NODE_ENV=production

# External Services
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=SG.xxxxxxxxxxxxxxxxxxxx

# .gitignore — add .env*
echo ".env*" >> .gitignore
echo "!.env.example" >> .gitignore

Essential Compose Commands

bash
# Start / Stop
docker-compose up -d                    # Start all (detached)
docker-compose up -d --build            # Rebuild images first
docker-compose down                     # Stop + remove containers
docker-compose down -v                  # Also remove volumes!
docker-compose restart app              # Restart specific service

# Scaling
docker-compose up -d --scale app=3     # 3 instances of app

# Logs
docker-compose logs -f                  # Follow all logs
docker-compose logs -f app postgres     # Specific services
docker-compose logs --tail=100 app      # Last 100 lines

# Execute / Shell
docker-compose exec app sh              # Shell into running service
docker-compose exec postgres psql -U myapp  # psql session
docker-compose run --rm app node -e "require('./src/db').migrate()" # One-off command

# Status
docker-compose ps                       # Status of all services
docker-compose top                      # Process listing
docker-compose config                   # Validate and view merged config
docker-compose images                   # Images used by services

# Profiles (run specific groups)
docker-compose --profile monitoring up -d  # Only services with profile: monitoring

✅ Production Best Practices

  • Always set memory/CPU limits
  • Use restart: unless-stopped
  • Add healthchecks to all services
  • Never use :latest tag in production
  • Use env_file + .gitignored .env
  • Configure log rotation options
  • Use internal: true for DB networks

❌ Common Mistakes

  • Committing .env files to Git
  • Using depends_on without healthchecks
  • Not setting resource limits
  • Exposing database ports (3306, 5432) to host
  • Using volumes for source code in production
  • Not configuring log rotation (disk fills!)
  • Running containers as root

Keep Reading

D
DevOps

Docker Networking Demystified: Bridge, Host & Overlay

8 min read Read More
C
Cloud

AWS IAM Roles vs Users vs Policies

10 min read Read More
P
Programming

Understanding Python's GIL & Multiprocessing

14 min read Read More