Introduction: The Need for Docker Compose

Real-world applications typically consist of multiple services. Various components such as web servers, databases, caches, and message queues must work together. Managing such multi-container environments with individual docker run commands is cumbersome and error-prone.

Docker Compose solves this problem by allowing you to define multiple containers in a single YAML file and manage the entire application stack with a single command.

1. What is Docker Compose?

1.1 Definition of Docker Compose

Docker Compose is a tool for defining and running multi-container Docker applications. You declaratively define your application's services, networks, and volumes in a docker-compose.yml file.

Key features of Docker Compose:

  • Declarative Definition: Define entire application stack in YAML file
  • Single Command Management: Start all services with docker compose up
  • Environment Isolation: Creates independent network per project
  • Variable Support: Flexible configuration with environment variables and .env files
  • Development Convenience: Especially useful for local development environment setup

1.2 Docker Compose V2

Docker Desktop and latest Docker Engine include Compose V2 by default. V2 is rewritten in Go and uses the format docker compose (without hyphen).

# V1 (legacy)
docker-compose up

# V2 (recommended)
docker compose up

2. docker-compose.yml Structure

2.1 Basic Structure

# docker-compose.yml
version: "3.9"  # Compose file version (optional)

services:       # Service (container) definitions
  web:
    image: nginx:alpine
    ports:
      - "80:80"

  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: secret

volumes:        # Volume definitions (optional)
  db-data:

networks:       # Network definitions (optional)
  backend:

2.2 Minimal Configuration Example

# Simplest docker-compose.yml
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"

3. Service Definition Details

3.1 image - Specify Image

services:
  web:
    # Docker Hub image
    image: nginx:alpine

  db:
    # Registry image
    image: registry.example.com/mydb:1.0

  app:
    # Specific digest
    image: myapp@sha256:abc123...

3.2 build - Build Image

services:
  app:
    # Simple build
    build: .

  api:
    # Detailed build options
    build:
      context: ./api
      dockerfile: Dockerfile.prod
      args:
        VERSION: "2.0"
        BUILD_ENV: production
      target: production  # Multi-stage build target

  frontend:
    # Tag image after build
    build: ./frontend
    image: myapp/frontend:latest

3.3 ports - Port Mapping

services:
  web:
    image: nginx
    ports:
      # host:container
      - "80:80"
      - "443:443"

      # Auto-assign host port
      - "80"

      # Bind to specific IP only
      - "127.0.0.1:3000:3000"

      # UDP port
      - "53:53/udp"

      # Long format
      - target: 80
        published: 8080
        protocol: tcp
        mode: host

3.4 volumes - Volume Mounts

services:
  db:
    image: postgres:15
    volumes:
      # Named Volume
      - db-data:/var/lib/postgresql/data

      # Bind Mount (host path)
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

      # Read-only mount
      - ./config:/etc/app/config:ro

      # Long format
      - type: volume
        source: db-data
        target: /var/lib/postgresql/data
        volume:
          nocopy: true

volumes:
  db-data:  # Volume declaration
  cache-data:
    driver: local

3.5 environment - Environment Variables

services:
  app:
    image: myapp
    environment:
      # Map format
      NODE_ENV: production
      DEBUG: "false"
      DATABASE_URL: postgres://user:pass@db:5432/mydb

  db:
    image: postgres
    environment:
      # Array format
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=myapp

  worker:
    image: myworker
    # Load environment variables from file
    env_file:
      - .env
      - .env.local

3.6 command and entrypoint

services:
  app:
    image: node:20
    # Override default command
    command: npm run dev

  worker:
    image: python:3.11
    # Array format
    command: ["python", "-m", "celery", "worker"]

  custom:
    image: alpine
    # Override entrypoint
    entrypoint: /custom-entrypoint.sh
    command: ["--config", "/etc/app/config.yml"]

4. Dependency Management (depends_on)

4.1 Basic Dependencies

services:
  web:
    image: nginx
    depends_on:
      - api
      - db

  api:
    image: myapi
    depends_on:
      - db
      - redis

  db:
    image: postgres:15

  redis:
    image: redis:alpine

4.2 Conditional Dependencies (service_healthy)

services:
  web:
    image: nginx
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

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

  redis:
    image: redis:alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

Important: depends_on only guarantees startup order, not that the service is "ready". In production, use healthcheck with condition: service_healthy.

5. Network Configuration

5.1 Default Network Behavior

Docker Compose automatically creates a default network for the project. Services on the same network can communicate with each other using service names.

services:
  web:
    image: nginx
    # Can access db service via hostname "db"

  db:
    image: postgres
    # Can access web service via hostname "web"

5.2 Custom Networks

services:
  frontend:
    image: nginx
    networks:
      - frontend-net

  api:
    image: myapi
    networks:
      - frontend-net
      - backend-net

  db:
    image: postgres
    networks:
      - backend-net

networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge
    internal: true  # Block external access

6. Practical Example: WordPress + MySQL

# wordpress/docker-compose.yml
services:
  wordpress:
    image: wordpress:latest
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress_password
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wordpress-data:/var/www/html
    depends_on:
      db:
        condition: service_healthy
    networks:
      - wordpress-net

  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress_password
      MYSQL_ROOT_PASSWORD: root_password
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - wordpress-net

volumes:
  wordpress-data:
  db-data:

networks:
  wordpress-net:
    driver: bridge

7. Practical Example: Node.js + MongoDB + Redis

# nodejs-stack/docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
      MONGODB_URI: mongodb://mongo:27017/myapp
      REDIS_URL: redis://redis:6379
    volumes:
      - .:/app
      - /app/node_modules
    depends_on:
      mongo:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-net
    command: npm run dev

  mongo:
    image: mongo:7
    restart: unless-stopped
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: secret
      MONGO_INITDB_DATABASE: myapp
    volumes:
      - mongo-data:/data/db
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - app-net

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    networks:
      - app-net

volumes:
  mongo-data:
  redis-data:

networks:
  app-net:
    driver: bridge

8. Docker Compose Commands

8.1 Basic Commands

# Start services (background)
docker compose up -d

# Start services (foreground, show logs)
docker compose up

# Build images then start
docker compose up --build

# Start specific services only
docker compose up -d web db

# Stop and remove services
docker compose down

# Also remove volumes
docker compose down -v

# Also remove images
docker compose down --rmi all

8.2 Status Check Commands

# List running services
docker compose ps

# All services (including stopped)
docker compose ps -a

# Check service logs
docker compose logs

# Specific service logs
docker compose logs web

# Real-time log streaming
docker compose logs -f

# Last 100 lines only
docker compose logs --tail=100

8.3 Service Management Commands

# Restart services
docker compose restart
docker compose restart web

# Stop services (keep containers)
docker compose stop

# Start stopped services
docker compose start

# Scale services
docker compose up -d --scale web=3

# Apply configuration changes
docker compose up -d --force-recreate

8.4 Execute Commands in Container (exec)

# Execute command in container
docker compose exec web sh
docker compose exec db psql -U postgres

# Execute as specific user
docker compose exec --user root web sh

# Run with new container (run)
docker compose run --rm app npm test

9. Environment-Specific Configuration Separation

9.1 Using .env File

# .env file
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret123
APP_PORT=3000
NODE_ENV=development
# docker-compose.yml
services:
  app:
    ports:
      - "${APP_PORT}:3000"
    environment:
      NODE_ENV: ${NODE_ENV}

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

9.2 Using Multiple Compose Files

# docker-compose.yml (base configuration)
services:
  app:
    build: .
    environment:
      NODE_ENV: production

  db:
    image: postgres:15
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:
# docker-compose.override.yml (development, auto-applied)
services:
  app:
    build:
      context: .
      target: development
    volumes:
      - .:/app
    environment:
      NODE_ENV: development
    ports:
      - "3000:3000"

  db:
    ports:
      - "5432:5432"
# Development environment (base + override auto-merged)
docker compose up

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

9.3 Using Profiles (Optional Services)

services:
  app:
    image: myapp

  db:
    image: postgres:15

  # Only for development
  adminer:
    image: adminer
    profiles:
      - debug
    ports:
      - "8080:8080"

  # Only for testing
  test-runner:
    image: myapp-test
    profiles:
      - test
# Start default services only
docker compose up

# Include debug profile
docker compose --profile debug up

# Multiple profiles
docker compose --profile debug --profile test up

10. Useful Configuration Options

10.1 restart Policy

services:
  app:
    image: myapp
    restart: "no"           # Don't restart (default)
    # restart: always       # Always restart
    # restart: on-failure   # Restart only on failure
    # restart: unless-stopped # Restart until manually stopped

10.2 Resource Limits

services:
  app:
    image: myapp
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 256M

Conclusion

Docker Compose is an essential tool for multi-container application management. Summary of topics covered:

  • docker-compose.yml Structure: Roles and definitions of services, volumes, networks
  • Service Definition: Core settings including image, build, ports, volumes, environment
  • Dependency Management: Reliable startup order with depends_on and healthcheck
  • Networks: Inter-service communication and network isolation
  • Practical Examples: Real use cases like WordPress, Node.js stack
  • Commands: Key commands including up, down, ps, logs, exec
  • Environment-Specific Config: Using .env files, override files, profiles

In the next part, we will cover Docker Networks and Volumes in depth, exploring container networking and persistent storage in detail.