Skip to main content
Back to Category

Docker Compose Guide: Multi-Container Application Management

Define and manage multi-container applications with Docker Compose: service dependencies, environment variables, health checks, scaling, and production deployment. Comprehensive guide.

Read time: 16 min DevOps & Automation
docker-composedockercontaineryamldevopsmicroservicedeployment

Table of Contents

Docker Compose Guide: Multi-Container Application Management

Docker Compose is a tool that lets you define and manage multiple Docker containers using a single YAML file. It's essential for microservice architectures, web applications, and development environments.

What is Docker Compose?

With Docker Compose you can:

  • Define multiple services in a single docker-compose.yml file
  • Manage inter-service dependencies
  • Centrally manage network and volume configurations
  • Start/stop the entire stack with a single command

Docker Compose v2 runs with the docker compose command (no hyphen) and is integrated into Docker CLI. The legacy docker-compose command is still supported.

Basic docker-compose.yml Structure

hljs yaml
version: '3.9'

services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    restart: unless-stopped

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret_password
      MYSQL_DATABASE: myapp
    volumes:
      - db_data:/var/lib/mysql
    restart: unless-stopped

volumes:
  db_data:

Services, Volumes and Networks

Defining Services

hljs yaml
services:
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    image: myapp:latest
    container_name: app-container
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DB_HOST: database
    depends_on:
      database:
        condition: service_healthy
    networks:
      - app-network
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

Defining Volumes

hljs yaml
volumes:
  # Named volume (managed by Docker)
  db_data:
    driver: local

  # External volume (pre-created)
  existing_volume:
    external: true

  # Custom driver options
  nfs_volume:
    driver: local
    driver_opts:
      type: nfs
      o: addr=192.168.1.100,rw
      device: ":/nfs/share"

Defining Networks

hljs yaml
networks:
  # Default bridge network
  app-network:
    driver: bridge

  # With custom subnet
  custom-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

  # External network
  existing-net:
    external: true

depends_on and Healthcheck

Control service startup order with depends_on:

hljs yaml
services:
  web:
    image: nginx:alpine
    depends_on:
      api:
        condition: service_healthy
      db:
        condition: service_healthy

  api:
    build: ./api
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

depends_on only controls startup order. Without condition: service_healthy you cannot guarantee the service is ready.

Environment Variables and env_file

Direct Definition

hljs yaml
services:
  app:
    image: myapp:latest
    environment:
      - NODE_ENV=production
      - PORT=3000
      - DB_HOST=database
      - DB_PORT=5432

Using .env File

hljs yaml
services:
  app:
    image: myapp:latest
    env_file:
      - .env
      - .env.production

.env file:

hljs bash
# .env
NODE_ENV=production
DB_HOST=database
DB_PORT=5432
DB_NAME=myapp
DB_USER=user
DB_PASS=secret_password
JWT_SECRET=very_secret_key
REDIS_URL=redis://redis:6379

Add .env to .gitignore. Never commit sensitive information to Git.

Scaling (--scale)

You can horizontally scale services with Docker Compose:

hljs bash
# Start api service with 3 instances
docker compose up -d --scale api=3

# Change running instance count
docker compose up -d --scale api=5

# Scale a specific service
docker compose scale api=3 worker=2

Load balancer configuration for scaling:

hljs yaml
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - api

  api:
    build: ./api
    # Don't define ports - nginx will route
    expose:
      - "3000"
    environment:
      NODE_ENV: production

Override Files

Use override files for different environments:

hljs yaml
# docker-compose.override.yml (development)
services:
  app:
    build:
      context: .
      target: development
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      NODE_ENV: development
    command: npm run dev

  db:
    ports:
      - "5432:5432"  # Expose externally in development
hljs yaml
# docker-compose.prod.yml (production)
services:
  app:
    image: registry.example.com/myapp:latest
    restart: always
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '1.0'
          memory: 1G

Usage:

hljs bash
# Development (override applied automatically)
docker compose up -d

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Production Best Practices

Full Production Example

hljs yaml
version: '3.9'

services:
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - certbot_data:/etc/letsencrypt:ro
    depends_on:
      - api
    restart: always
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  api:
    image: registry.example.com/api:${APP_VERSION:-latest}
    env_file: .env.production
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: always
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M

  db:
    image: postgres:15-alpine
    env_file: .env.production
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    restart: always
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

volumes:
  postgres_data:
  redis_data:
  certbot_data:

Essential Commands

hljs bash
# Start all services
docker compose up -d

# Start specific service
docker compose up -d api

# Follow logs
docker compose logs -f
docker compose logs -f api

# Stop services
docker compose down

# Stop services and remove volumes
docker compose down -v

# Restart a service
docker compose restart api

# View service status
docker compose ps

# Execute command in service
docker compose exec api bash

# Rebuild images
docker compose build --no-cache
docker compose up -d --build

# Validate configuration
docker compose config

Conclusion

Docker Compose is the most practical way to manage multi-container applications. It provides a consistent environment from development to production. With override files you can flexibly configure different environments, and with healthcheck and depends_on you can safely manage service dependencies.