CloudOpsGuide
docker

Docker Compose: Multi-Container Applications

Beginner
13 minutes
October 2026
CloudOpsGuide Team

Docker Compose: Multi-Container Applications

Define and run multi-container Docker applications — from basic setup to production patterns with volumes, networks, and health checks.

Table of Contents

Compose File Basics

# docker-compose.yml
services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
docker compose up -d
docker compose down
docker compose logs -f

The version field is deprecated in modern Compose — omit it.

Services

Full Service Definition

services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    image: my-api:latest
    container_name: my-api
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ./api:/app          # dev hot-reload
      - api-logs:/logs      # named volume
    networks:
      - backend
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3

Build vs Image

# Option 1: Pre-built image
services:
  app:
    image: myregistry.com/app:1.2.0

# Option 2: Build from Dockerfile
services:
  app:
    build: .
    image: my-app:dev

Volumes and Persistence

Named Volumes

services:
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:    # Docker manages location

Bind Mounts

services:
  app:
    volumes:
      - ./src:/app/src            # relative to compose file
      - /opt/app/logs:/var/log    # absolute host path

Read-Only Mounts

volumes:
  - ./nginx.conf:/etc/nginx/nginx.conf:ro

Networking

Default Network

All services in a compose file share a default network. Services reach each other by name:

services:
  web:
    image: myapp
    environment:
      DB_HOST: db    # ← service name resolves to IP
  db:
    image: postgres:16

Custom Networks

services:
  frontend:
    networks:
      - frontend-net
  api:
    networks:
      - frontend-net
      - backend-net     # API bridges both
  db:
    networks:
      - backend-net     # isolated from frontend

networks:
  frontend-net:
  backend-net:

Aliases

services:
  api:
    networks:
      backend:
        aliases:
          - api.internal

Environment Variables

Inline

environment:
  - NODE_ENV=production
  - LOG_LEVEL=info

.env File

# .env
DATABASE_URL=postgres://db:5432/myapp
API_KEY=abc123
services:
  app:
    env_file:
      - .env

Compose Variable Substitution

services:
  app:
    image: myapp:${TAG:-latest}
TAG=v1.2.0 docker compose up

Health Checks and Dependencies

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

  api:
    depends_on:
      db:
        condition: service_healthy    # wait for healthy, not just started
      redis:
        condition: service_started    # just wait for start

Conditions: service_started (default), service_healthy, service_completed_successfully.

Production Patterns

Restart Policies

services:
  app:
    restart: unless-stopped    # restart unless manually stopped
    # Other options: no, always, on-failure

Resource Limits

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

Multi-Environment Override

# docker-compose.yml        — base
# docker-compose.prod.yml   — prod overrides

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# docker-compose.prod.yml
services:
  app:
    restart: always
    environment:
      LOG_LEVEL: warn

Common Commands

# Start all services (detached)
docker compose up -d

# View logs (follow)
docker compose logs -f api

# Scale a service
docker compose up -d --scale worker=3

# Rebuild and restart one service
docker compose up -d --build api

# Stop everything
docker compose down

# Stop + remove volumes (destroys data)
docker compose down -v

# See running services
docker compose ps

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

# View resolved config (after env substitution)
docker compose config

Common Mistakes

1. Port Conflicts

# Both map to host port 80 — second one fails
services:
  web1: { ports: ["80:80"] }
  web2: { ports: ["80:80"] }   # ← conflict

# Fix: use different host ports
  web2: { ports: ["8080:80"] }

2. Secrets in Environment

# Bad — visible in docker inspect
environment:
  DB_PASSWORD: supersecret

# Better — use secrets or .env
env_file:
  - .env    # add .env to .gitignore

3. Depending on Service Started vs Healthy

# Bad — api starts before db is ready
depends_on:
  - db

# Good — waits for health check
depends_on:
  db:
    condition: service_healthy

Related Articles


Last Updated: October 2026
Author: CloudOpsGuide Team
Difficulty: Beginner
Estimated Reading Time: 13 minutes