Docker Compose and Multi-Container Applications

Docker Compose simplifies the management of multi-container applications by allowing you to define and run complex applications using a single YAML file. This section covers Compose fundamentals, advanced patterns, and real-world application examples.

Introduction to Docker Compose

What is Docker Compose?

Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services, networks, and volumes, then create and start all services with a single command.

Installing Docker Compose

# Docker Compose comes with Docker Desktop
# For Linux, install separately:
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# Verify installation
docker-compose --version

Basic Compose File Structure

# docker-compose.yml
version: '3.8'

services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    
  database:
    image: postgres:13
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=secret
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

networks:
  default:
    driver: bridge

Compose File Deep Dive

Service Configuration

version: '3.8'

services:
  web:
    # Build from Dockerfile
    build:
      context: ./web
      dockerfile: Dockerfile.prod
      args:
        - NODE_ENV=production
        - API_URL=http://api:3000
    
    # Or use pre-built image
    image: my-web-app:latest
    
    # Container name
    container_name: web-server
    
    # Restart policy
    restart: unless-stopped
    
    # Port mapping
    ports:
      - "80:80"
      - "443:443"
    
    # Environment variables
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
    
    # Environment file
    env_file:
      - .env
      - .env.production
    
    # Volume mounts
    volumes:
      - ./config:/etc/nginx/conf.d:ro
      - web_data:/var/www/html
      - /var/run/docker.sock:/var/run/docker.sock:ro
    
    # Network configuration
    networks:
      - frontend
      - backend
    
    # Dependencies
    depends_on:
      - database
      - cache
    
    # Resource limits
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    
    # Health check
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Advanced Service Options

services:
  app:
    build: .
    
    # Command override
    command: ["npm", "run", "start:prod"]
    
    # Working directory
    working_dir: /app
    
    # User specification
    user: "1000:1000"
    
    # Hostname
    hostname: app-server
    
    # DNS configuration
    dns:
      - 8.8.8.8
      - 8.8.4.4
    
    # Extra hosts
    extra_hosts:
      - "api.local:192.168.1.100"
      - "db.local:192.168.1.101"
    
    # Security options
    security_opt:
      - no-new-privileges:true
    
    # Capabilities
    cap_add:
      - NET_ADMIN
    cap_drop:
      - ALL
    
    # Logging configuration
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Real-World Application Examples

Example 1: Full-Stack Web Application

# docker-compose.yml
version: '3.8'

services:
  # Frontend (React)
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.prod
    container_name: react-frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - frontend-net
    restart: unless-stopped

  # Backend API (Node.js)
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: node-backend
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:password@postgres:5432/myapp
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - postgres
      - redis
    networks:
      - frontend-net
      - backend-net
    volumes:
      - ./uploads:/app/uploads
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Database (PostgreSQL)
  postgres:
    image: postgres:13-alpine
    container_name: postgres-db
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - backend-net
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 30s
      timeout: 10s
      retries: 5

  # Cache (Redis)
  redis:
    image: redis:6-alpine
    container_name: redis-cache
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    networks:
      - backend-net
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Reverse Proxy (Nginx)
  nginx:
    image: nginx:alpine
    container_name: nginx-proxy
    ports:
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - frontend
      - backend
    networks:
      - frontend-net
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge

Example 2: Microservices Architecture

# docker-compose.microservices.yml
version: '3.8'

services:
  # API Gateway
  gateway:
    build: ./gateway
    ports:
      - "80:8080"
    environment:
      - USER_SERVICE_URL=http://user-service:3000
      - ORDER_SERVICE_URL=http://order-service:3000
      - PRODUCT_SERVICE_URL=http://product-service:3000
    depends_on:
      - user-service
      - order-service
      - product-service
    networks:
      - microservices-net

  # User Service
  user-service:
    build: ./services/user
    environment:
      - DATABASE_URL=postgresql://user:pass@user-db:5432/users
      - REDIS_URL=redis://redis:6379
    depends_on:
      - user-db
      - redis
    networks:
      - microservices-net
      - user-db-net
    deploy:
      replicas: 2

  user-db:
    image: postgres:13-alpine
    environment:
      - POSTGRES_DB=users
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - user_db_data:/var/lib/postgresql/data
    networks:
      - user-db-net

  # Order Service
  order-service:
    build: ./services/order
    environment:
      - DATABASE_URL=postgresql://order:pass@order-db:5432/orders
      - USER_SERVICE_URL=http://user-service:3000
    depends_on:
      - order-db
    networks:
      - microservices-net
      - order-db-net

  order-db:
    image: postgres:13-alpine
    environment:
      - POSTGRES_DB=orders
      - POSTGRES_USER=order
      - POSTGRES_PASSWORD=pass
    volumes:
      - order_db_data:/var/lib/postgresql/data
    networks:
      - order-db-net

  # Product Service
  product-service:
    build: ./services/product
    environment:
      - MONGODB_URI=mongodb://product-db:27017/products
    depends_on:
      - product-db
    networks:
      - microservices-net
      - product-db-net

  product-db:
    image: mongo:5.0
    volumes:
      - product_db_data:/data/db
    networks:
      - product-db-net

  # Shared Redis Cache
  redis:
    image: redis:6-alpine
    volumes:
      - redis_data:/data
    networks:
      - microservices-net

  # Message Queue
  rabbitmq:
    image: rabbitmq:3-management
    environment:
      - RABBITMQ_DEFAULT_USER=admin
      - RABBITMQ_DEFAULT_PASS=secret
    ports:
      - "15672:15672"  # Management UI
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    networks:
      - microservices-net

volumes:
  user_db_data:
  order_db_data:
  product_db_data:
  redis_data:
  rabbitmq_data:

networks:
  microservices-net:
    driver: bridge
  user-db-net:
    driver: bridge
    internal: true
  order-db-net:
    driver: bridge
    internal: true
  product-db-net:
    driver: bridge
    internal: true

Example 3: Development Environment

# docker-compose.dev.yml
version: '3.8'

services:
  # Development API with hot reload
  api:
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    volumes:
      - ./api:/app
      - /app/node_modules
    ports:
      - "3000:3000"
      - "9229:9229"  # Debug port
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://dev:dev@postgres:5432/devdb
      - REDIS_URL=redis://redis:6379
    depends_on:
      - postgres
      - redis
    command: npm run dev:debug

  # Frontend with hot reload
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.dev
    volumes:
      - ./frontend:/app
      - /app/node_modules
    ports:
      - "3001:3000"
    environment:
      - REACT_APP_API_URL=http://localhost:3000
      - CHOKIDAR_USEPOLLING=true
    command: npm start

  # Development database
  postgres:
    image: postgres:13-alpine
    environment:
      - POSTGRES_DB=devdb
      - POSTGRES_USER=dev
      - POSTGRES_PASSWORD=dev
    ports:
      - "5432:5432"  # Expose for external tools
    volumes:
      - postgres_dev_data:/var/lib/postgresql/data
      - ./database/init:/docker-entrypoint-initdb.d

  # Development Redis
  redis:
    image: redis:6-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_dev_data:/data

  # Development tools
  adminer:
    image: adminer:latest
    ports:
      - "8080:8080"
    depends_on:
      - postgres

  redis-commander:
    image: rediscommander/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - "8081:8081"
    depends_on:
      - redis

volumes:
  postgres_dev_data:
  redis_dev_data:

Docker Compose Commands

Basic Operations

# Start services
docker-compose up
docker-compose up -d  # Detached mode

# Start specific services
docker-compose up web database

# Build and start
docker-compose up --build

# Stop services
docker-compose stop
docker-compose down  # Stop and remove containers

# Stop and remove everything (including volumes)
docker-compose down -v

# Restart services
docker-compose restart
docker-compose restart web  # Restart specific service

Service Management

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

# View running services
docker-compose ps

# View logs
docker-compose logs
docker-compose logs -f web  # Follow logs for specific service
docker-compose logs --tail=100 api

# Execute commands in services
docker-compose exec web /bin/bash
docker-compose exec database psql -U user -d myapp

# Run one-off commands
docker-compose run --rm web npm test
docker-compose run --rm database pg_dump -U user myapp > backup.sql

Configuration Management

# Validate compose file
docker-compose config

# View resolved configuration
docker-compose config --services
docker-compose config --volumes

# Use different compose files
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up

# Override with environment-specific files
docker-compose -f docker-compose.yml -f docker-compose.override.yml up

Environment-Specific Configurations

Development Override

# docker-compose.override.yml (automatically loaded)
version: '3.8'

services:
  web:
    build:
      target: development
    volumes:
      - ./src:/app/src
    environment:
      - NODE_ENV=development
      - DEBUG=true
    ports:
      - "3000:3000"
      - "9229:9229"  # Debug port

  database:
    ports:
      - "5432:5432"  # Expose for development tools
    environment:
      - POSTGRES_DB=devdb

Production Configuration

# docker-compose.prod.yml
version: '3.8'

services:
  web:
    build:
      target: production
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  database:
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
    volumes:
      - /opt/postgres/data:/var/lib/postgresql/data

Using Environment Files

# .env file
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
POSTGRES_DB=myapp
API_KEY=your-api-key-here
NODE_ENV=production
# docker-compose.yml
version: '3.8'

services:
  web:
    image: my-app:latest
    environment:
      - NODE_ENV=${NODE_ENV}
      - API_KEY=${API_KEY}
    
  database:
    image: postgres:13
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}

Advanced Compose Patterns

Health Checks and Dependencies

services:
  database:
    image: postgres:13
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s

  web:
    image: my-app:latest
    depends_on:
      database:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Secrets Management

# docker-compose.yml
version: '3.8'

services:
  web:
    image: my-app:latest
    secrets:
      - db_password
      - api_key
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password
      - API_KEY_FILE=/run/secrets/api_key

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    external: true

Multi-Stage Builds with Compose

# Dockerfile
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:16-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

FROM node:16-alpine AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["npm", "start"]
# docker-compose.yml
services:
  web:
    build:
      context: .
      target: ${BUILD_TARGET:-production}

Monitoring and Logging

Centralized Logging

# docker-compose.logging.yml
version: '3.8'

services:
  web:
    image: my-app:latest
    logging:
      driver: "fluentd"
      options:
        fluentd-address: localhost:24224
        tag: web.app

  elasticsearch:
    image: elasticsearch:7.14.0
    environment:
      - discovery.type=single-node
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data

  fluentd:
    build: ./fluentd
    volumes:
      - ./fluentd/conf:/fluentd/etc
    ports:
      - "24224:24224"
    depends_on:
      - elasticsearch

  kibana:
    image: kibana:7.14.0
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch

volumes:
  elasticsearch_data:

Monitoring Stack

# docker-compose.monitoring.yml
version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards
      - ./grafana/datasources:/etc/grafana/provisioning/datasources

  node-exporter:
    image: prom/node-exporter:latest
    ports:
      - "9100:9100"

volumes:
  prometheus_data:
  grafana_data:

Best Practices

Compose File Organization

# Project structure
project/
├── docker-compose.yml          # Base configuration
├── docker-compose.override.yml # Development overrides
├── docker-compose.prod.yml     # Production configuration
├── docker-compose.test.yml     # Testing configuration
├── .env                        # Environment variables
├── .env.example               # Environment template
└── services/
    ├── web/
    │   ├── Dockerfile
    │   └── src/
    ├── api/
    │   ├── Dockerfile
    │   └── src/
    └── database/
        └── init/

Security Best Practices

services:
  web:
    image: my-app:latest
    user: "1000:1000"  # Non-root user
    read_only: true     # Read-only filesystem
    tmpfs:
      - /tmp
      - /var/cache
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE

Summary

In this section, you learned:

Docker Compose Fundamentals

  • Compose file structure and syntax
  • Service configuration options
  • Volume and network management
  • Environment-specific configurations

Real-World Applications

  • Full-stack web application setup
  • Microservices architecture patterns
  • Development environment configuration
  • Production deployment considerations

Advanced Patterns

  • Health checks and service dependencies
  • Secrets management
  • Multi-stage builds with Compose
  • Monitoring and logging integration

Best Practices

  • Project organization and file structure
  • Security configurations
  • Environment management
  • Service scaling and resource limits

Key Takeaways:

  • Compose simplifies multi-container application management
  • Use override files for environment-specific configurations
  • Implement health checks for reliable service dependencies
  • Always consider security when configuring services
  • Monitor and log your applications for production readiness

Next, we’ll explore production deployment strategies, security best practices, and performance optimization techniques for Docker applications.