DEV Community

Alex Chen
Alex Chen

Posted on

Docker Compose: Multi-Container Applications Made Easy (2026)

Docker Compose: Multi-Container Applications Made Easy (2026)

Real apps don't run in a single container. Docker Compose orchestrates multiple services together — here's how to use it like a pro.

The Basics: docker-compose.yml

# docker-compose.yml — the single source of truth for your app
version: "3.8"

services:
  # Service 1: Your application
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: myapp
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://app:password@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    volumes:
      - .:/app
      - /app/node_modules  # Anonymous volume prevents host overwriting
    depends_on:
      - db
      - cache
    networks:
      - app-network

  # Service 2: PostgreSQL database
  db:
    image: postgres:16-alpine
    container_name: myapp-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # Auto-run on first start
    ports:
      - "5432:5432"
    networks:
      - app-network

  # Service 3: Redis cache
  cache:
    image: redis:7-alpine
    container_name: myapp-redis
    restart: unless-stopped
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - app-network

volumes:
  postgres_data:   # Named volume — persists across container recreation
  redis_data:

networks:
  app-network:
    driver: bridge  # All services can communicate by service name
Enter fullscreen mode Exit fullscreen mode

Essential Commands

# Start everything (build if needed):
docker compose up -d          # -d = detached (background)
docker compose up -d --build  # Force rebuild even if image exists

# Stop and remove containers:
docker compose down            # Stops + removes containers + network
docker compose down -v         # Also removes named volumes (⚠️ data loss!)

# View status:
docker compose ps              # List running services
docker compose logs             # View all logs
docker compose logs -f app     # Follow logs for specific service
docker compose logs --tail=50   # Last 50 lines

# Execute commands inside container:
docker compose exec app sh       # Interactive shell in app container
docker compose exec db psql -U app  # Connect to Postgres directly

# Restart single service:
docker compose restart cache

# Scale services (for stateless ones):
docker compose up -d --scale app=3  # Run 3 instances of app

# Check resource usage:
docker compose top               # Process list per container
docker stats                     # Live CPU/Memory usage

# Clean up unused resources:
docker system prune -a           # Remove all stopped images, containers, networks
Enter fullscreen mode Exit fullscreen mode

Production Patterns

# Pattern 1: Health checks (essential for orchestration!)
services:
  app:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      timeout: 5s
      retries: 5

# Pattern 2: Environment variables from file (.env)
services:
  app:
    env_file:
      - .env
    environment:
      # Override or add specific vars
      - PORT=${PORT:-3000}

# Pattern 3: Multi-stage build (smaller images)
# Dockerfile:
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci && npm run build

FROM node:22-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -D appuser
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

# Pattern 4: Nginx reverse proxy
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app
    networks:
      - app-network

# nginx.conf:
# upstream app_server {
#   server app:3000;
# }
# server {
#   listen 80;
#   location / {
#     proxy_pass http://app_server;
#     proxy_set_header Host $host;
#     proxy_set_header X-Real-IP $remote_addr;
#     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#     proxy_set_header X-Forwarded-Proto $scheme;
#   }
# }

# Pattern 5: Secrets management
secrets:
  db_password:
    file: ./secrets/db_password.txt
  jwt_secret:
    external: true  # Created with: docker secret create jwt_secret ...

services:
  app:
    secrets:
      - db_password
      - jwt_secret
Enter fullscreen mode Exit fullscreen mode

Common Scenarios

# Scenario 1: Add a background worker
services:
  worker:
    build: .
    container_name: myapp-worker
    command: npm run worker  # Different entrypoint!
    environment:
      - DATABASE_URL=postgres://app:password@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache
    deploy:
      replicas: 2  # Run 2 worker instances
    networks:
      - app-network

# Scenario 2: Development vs Production overrides
# Base: docker-compose.yml (shared config)
# Dev override: docker-compose.override.yml (auto-loaded with `docker compose up`)
# Prod: docker-compose.prod.yml (`docker compose -f docker-compose.yml -f docker-compose.prod.yml up`)

# docker-compose.override.yml (dev only):
services:
  app:
    volumes:
      - .:/app        # Hot reload via bind mount
    environment:
      - NODE_ENV=development
  db:
    ports:
      - "5432:5432"   # Expose DB port locally only in dev

# Scenario 3: One-command setup for new developers
# Just: git clone && cp .env.example .env && docker compose up -d
# That's it. No Node.js, no Postgres, no Redis installation needed.
Enter fullscreen mode Exit fullscreen mode

What's your favorite Docker Compose trick? How do you handle local development?

Follow @armorbreak for more practical developer guides.

Top comments (0)