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
Author
REXE Teknoloji Network & Security Team
Editor
REXE Teknoloji Technical Editorial
First published
Last updated

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.

Frequently Asked Questions

What does the docker compose config command do?

'docker compose config' shows the final configuration after all override files and environment variables are applied. It's useful for debugging and verifying that your configuration is correct.

I'm using depends_on but my service tries to connect before the dependency is ready. Why?

'depends_on' by default only waits for the container to start, not for it to be ready. Use 'condition: service_healthy' to wait until the healthcheck passes. It's also recommended to add retry logic in your application.

Is it appropriate to use Docker Compose in production?

Docker Compose is suitable for single-server deployments. If you need multi-server orchestration, Kubernetes or Docker Swarm is preferred. Docker Compose is widely used in production for small to medium-scale applications.

How are variables from the .env file used inside docker-compose.yml?

Docker Compose automatically reads the .env file in the same directory. Access values with '${VARIABLE_NAME}' syntax. For example: 'image: myapp:${APP_VERSION:-latest}'. Use ':-' for default values.

How do I prevent port conflicts when scaling services?

Use 'expose' instead of 'ports' for scaled services. Expose only opens the port to the internal network. Route external traffic through a load balancer (nginx, traefik). This way multiple instances can use the same internal port.

What is the difference between docker compose down and docker compose stop?

'docker compose stop' stops containers but doesn't remove them. 'docker compose down' stops and removes containers. Adding the '-v' flag also removes volumes. 'stop' is recommended for development, use 'down' carefully in production.

Related Articles

Server Automation with Ansible: Infrastructure as Code

Ansible installation, inventory file, playbook writing, role structure, variables, encrypted vault, ad-hoc commands, and server configuration automation. Step-by-step guide.

20 min
ansibleautomationinfrastructure as code
Network TrafficInbound GbpsOutbound Gbps