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 applications 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-app
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://app:secret@db:5432/mydb
      - REDIS_URL=redis://redis:6379
    volumes:
      - .:/app                    # Live reload during development
      - /app/node_modules         # Don't override node_modules
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Service 2: PostgreSQL database
  db:
    image: postgres:16-alpine
    container_name: myapp-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data   # Persist data across container restarts
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # Init script on first run
    ports:
      - "5432:5432"              # Comment out in production!
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Service 3: Redis cache
  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    restart: unless-stopped
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redisdata:/data
    ports:
      - "6379:6379"              # Comment out in production!
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

# Named volumes (persist data even when containers are removed)
volumes:
  pgdata:
    driver: local
  redisdata:
    driver: local

# Custom network (services can communicate by service name!)
networks:
  app-network:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

Essential Commands

# === Lifecycle Commands ===
docker compose up -d            # Start all services (detached/background mode)
docker compose down             # Stop and remove all containers + networks
docker compose down -v          # Also remove named volumes (⚠️ data loss!)

# === Development vs Production ===
# Development (with hot-reload):
docker compose up               # Foreground (see logs live)

# Production (detached):
docker compose up -d --build    # Build if needed, then start backgrounded

# === Managing Individual Services ===
docker compose up -d db redis   # Start only specific services
docker compose stop app         # Stop one service (others keep running)
docker compose restart app      # Restart one service
docker compose logs app         # View logs for one service
docker compose logs -f --tail=50 app  # Follow last 50 lines of app logs
docker compose logs -f          # Follow ALL services' logs interleaved

# === Building & Images ===
docker compose build            # Rebuild all services
docker compose build app        # Rebuild only app service
docker compose build --no-cache # Force rebuild without layer cache

# === Inside Containers ===
docker compose exec app sh      # Open shell inside running app container
docker compose exec db psql -U app mydb  # Connect to Postgres directly
docker compose exec redis redis-cli       # Connect to Redis CLI

# === Status & Debugging ===
docker compose ps               # List all containers with status
docker compose top              # Running processes in each container
docker compose config           # Validate and view resolved config
docker compose images           # Show images used by services
docker compose port app 3000    # Map which host port maps to container port 3000

# === Cleanup ===
docker system prune -a          # Remove unused images, networks, builders
docker volume prune             # Remove unused volumes
Enter fullscreen mode Exit fullscreen mode

Development Workflow

# docker-compose.dev.yml — Development overrides
# Use: docker compose -f docker-compose.yml -f docker-compose.dev.yml up
version: "3.8"
services:
  app:
    build:
      context: .
      target: development     # Multi-stage build dev stage
    environment:
      - NODE_ENV=development
      - DEBUG=app:*
    volumes:
      - .:/app                # Live code sync
      - /app/node_modules
    command: npm run dev       # Watch mode / nodemon
    ports:
      - "9229:9229"           # Node.js debugger port

  db:
    ports:
      - "5432:5432"           # Expose DB for local tools (pgAdmin, DBeaver)

  # Add development-only service:
  adminer:
    image: adminer:latest
    ports:
      - "8080:8080"           # Web-based DB admin UI at localhost:8080
Enter fullscreen mode Exit fullscreen mode
# docker-compose.prod.yml — Production overrides
# Use: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
version: "3.8"
services:
  app:
    image: myapp:${VERSION:-latest}  # Use pre-built image from registry
    restart: always
    environment:
      - NODE_ENV=production
    volumes:
      - ./logs:/app/logs       # Only persist logs, not code
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  db:
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}  # From .env file or secrets manager
    volumes:
      - production_pgdata:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          memory: 1G

  redis:
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
Enter fullscreen mode Exit fullscreen mode

Common Patterns

# === Pattern 1: Nginx Reverse Proxy ===
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - app
    networks:
      - frontend

  app:
    # ... no exposed ports! Nginx handles all external traffic
    networks:
      - frontend
      - backend

# nginx.conf upstream block:
# upstream app_backend {
#   server app:3000;
# }
# server {
#   listen 80;
#   server_name example.com;
#   location / {
#     proxy_pass http://app_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;
#     proxy_set_header X-Forwarded-Proto $scheme;
#   }
# }

# === Pattern 2: Background Worker ===
services:
  worker:
    build: .
    command: npm run worker        # Separate process for queue jobs
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=redis://redis:6379
    depends_on:
      redis:
        condition: service_healthy
    deploy:
      replicas: 2                 # Run 2 worker instances!
    networks:
      - app-network

# Scale workers based on load:
# docker compose up -d --scale worker=4

# === Pattern 3: One-off Jobs (migrations, seeds) ===
# Run once and exit:
# docker compose run --rm app npm run migrate
# docker compose run --rm app npm run seed
# docker compose exec db pg_dump -U app mydb > backup.sql

# === Pattern 4: Health Check Dependent Startup ===
services:
  app:
    depends_on:
      db:
        condition: service_healthy   # Wait until DB is ACTUALLY ready
      redis:
        condition: service_healthy
# This is CRITICAL — containers start in parallel by default.
# Without health checks, app might try to connect before DB accepts connections!

# === Pattern 5: Secrets Management ===
# Option A: .env file (never commit to git!)
# echo "DB_PASSWORD=supersecret" > .env
# .env is loaded automatically by docker compose

# Option B: Docker secrets (Swarm mode):
# echo "my_password" | docker secret create db_password -
# In compose:
#   environment:
#     - DB_PASSWORD_FILE=/run/secrets/db_password
#   secrets:
#     - db_password
#   secrets:
#     db_password:
#       external: true

# Option C: External secrets manager (recommended for production):
#   environment:
#     - DB_PASSWORD=${DB_PASSWORD}  # From CI/CD or vault
Enter fullscreen mode Exit fullscreen mode

Troubleshooting Guide

# Container won't start?
docker compose logs app              # Check logs first!
docker compose ps                   # Is it actually running?
docker compose exec app env         # Are env vars correct?

# Port already in use?
sudo lsof -i :3000                  # What's using the port?
# Change mapping: "3001:3000"

# Database connection refused?
docker compose logs db               # Is DB running?
docker compose exec db psql -U app -c 'SELECT 1'  # Can we connect to DB?
# Check: is app trying to connect to "localhost:5432" instead of "db:5432"?
# In compose, use SERVICE name as hostname, NOT localhost!

# Volume has old data?
docker compose down -v              # Remove volumes and start fresh
# ⚠️ This deletes all persisted data!

# Image won't rebuild?
docker compose build --no-cache app # Clear build cache
docker builder prune                # Remove ALL build cache

# Container runs out of space?
docker system df                     # See disk usage
docker system prune -a              # Clean up unused images

# Network issues between containers?
docker network ls                    # List networks
docker network inspect app_network  # Check if services are on same network
# All services in same `networks:` list can reach each other by service name
Enter fullscreen mode Exit fullscreen mode

What's your favorite Docker Compose trick? What multi-container setup are you running?

Follow @armorbreak for more practical developer guides.

Top comments (0)