Learning Guides
Menu

Docker Compose

8 min readDocker for Developers

Docker Compose

Docker Compose allows you to define and run multi-container applications. Instead of managing containers individually, you define your entire application stack in a single YAML file.

Why Docker Compose?

Managing multiple containers manually becomes unwieldy:

BASH
# Without Compose: Multiple long commands
docker network create myapp-network
 
docker run -d \
  --name postgres \
  --network myapp-network \
  -e POSTGRES_PASSWORD=secret \
  -v postgres-data:/var/lib/postgresql/data \
  postgres:16
 
docker run -d \
  --name redis \
  --network myapp-network \
  redis:7
 
docker run -d \
  --name app \
  --network myapp-network \
  -e DATABASE_URL=postgres://postgres:secret@postgres:5432/app \
  -e REDIS_URL=redis://redis:6379 \
  -p 3000:3000 \
  myapp:latest

Note

Docker Compose simplifies this to a single command: docker compose up. All configuration lives in one file, making it easy to understand, version control, and share.

Getting Started

Compose File Basics

Create a docker-compose.yml (or compose.yml) file:

YAML
services:
  app:
    image: myapp:latest
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:secret@postgres:5432/app
      - REDIS_URL=redis://redis:6379
    depends_on:
      - postgres
      - redis
 
  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres-data:/var/lib/postgresql/data
 
  redis:
    image: redis:7
 
volumes:
  postgres-data:

Basic Commands

BASH
# Start all services in the foreground
docker compose up
 
# Start in detached mode (background)
docker compose up -d
 
# Stop all services
docker compose down
 
# Stop and remove volumes
docker compose down -v
 
# View running services
docker compose ps
 
# View logs
docker compose logs
docker compose logs -f app
 
# Execute command in a service
docker compose exec app sh

Compose File Reference

Version and Services

YAML
# Modern compose files don't need 'version:'
services:
  web:
    image: nginx:1.25

Note

The version key is deprecated in recent Docker Compose. Just start with services:.

Image Configuration

YAML
services:
  # Use a pre-built image
  redis:
    image: redis:7-alpine
 
  # Build from Dockerfile
  app:
    build: .
 
  # Build with options
  app:
    build:
      context: .
      dockerfile: Dockerfile.prod
      args:
        NODE_ENV: production
      target: production

Ports

YAML
services:
  web:
    image: nginx
    ports:
      # Short syntax
      - "8080:80"
      - "443:443"
 
      # Long syntax
      - target: 80
        published: 8080
        protocol: tcp
        mode: host

Environment Variables

YAML
services:
  app:
    image: myapp
    environment:
      # List syntax
      - NODE_ENV=production
      - DATABASE_URL=postgres://localhost/db
 
    # Or map syntax
    environment:
      NODE_ENV: production
      DATABASE_URL: postgres://localhost/db
 
  api:
    image: myapi
    # Load from file
    env_file:
      - .env
      - .env.local

Environment File

BASH
# .env file
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret
POSTGRES_DB=myapp
API_KEY=abc123

Volumes

YAML
services:
  app:
    image: myapp
    volumes:
      # Named volume
      - app-data:/app/data
 
      # Bind mount
      - ./src:/app/src
 
      # Read-only bind mount
      - ./config:/app/config:ro
 
  postgres:
    image: postgres:16
    volumes:
      - postgres-data:/var/lib/postgresql/data
      # Initialize with SQL scripts
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
 
volumes:
  app-data:
  postgres-data:

Networks

YAML
services:
  frontend:
    networks:
      - frontend-network
 
  api:
    networks:
      - frontend-network
      - backend-network
 
  database:
    networks:
      - backend-network
 
networks:
  frontend-network:
  backend-network:
    driver: bridge

Dependencies

YAML
services:
  app:
    depends_on:
      - postgres
      - redis
 
  # With health check conditions
  app:
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
 
  postgres:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

Warning

depends_on only waits for the container to start, not for the service inside to be ready. Use health checks for services that need initialization time.

Health Checks

YAML
services:
  api:
    image: myapi
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Resource Limits

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

Restart Policies

YAML
services:
  app:
    image: myapp
    restart: always # or: no, on-failure, unless-stopped
 
  # With failure count
  worker:
    image: myworker
    deploy:
      restart_policy:
        condition: on-failure
        max_attempts: 3
        delay: 5s

Building with Compose

Simple Build

YAML
services:
  app:
    build: .
    ports:
      - "3000:3000"

Advanced Build Options

YAML
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.prod
      args:
        - NODE_ENV=production
        - APP_VERSION=1.0.0
      target: production
      cache_from:
        - myapp:latest
      labels:
        - "com.example.app=myapp"
    image: myapp:1.0.0

Build Commands

BASH
# Build all services
docker compose build
 
# Build specific service
docker compose build app
 
# Build without cache
docker compose build --no-cache
 
# Pull base images before building
docker compose build --pull
 
# Build and start
docker compose up --build

Complete Examples

Full-Stack Application

YAML
# docker-compose.yml
services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - REACT_APP_API_URL=http://localhost:4000
    depends_on:
      - api
 
  api:
    build: ./backend
    ports:
      - "4000:4000"
    environment:
      - DATABASE_URL=postgres://postgres:secret@postgres:5432/app
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=your-secret-key
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ./backend/src:/app/src
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
 
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    depends_on:
      - frontend
      - api
 
volumes:
  postgres-data:
  redis-data:

Development Setup

YAML
# docker-compose.dev.yml
services:
  app:
    build:
      context: .
      target: development
    volumes:
      - .:/app
      - /app/node_modules # Exclude node_modules
    ports:
      - "3000:3000"
      - "9229:9229" # Debugger
    environment:
      - NODE_ENV=development
    command: npm run dev
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: dev
    ports:
      - "5432:5432"
    volumes:
      - postgres-dev:/var/lib/postgresql/data
 
volumes:
  postgres-dev:

Running Development Environment

BASH
# Use development compose file
docker compose -f docker-compose.dev.yml up
 
# Or combine with base file
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

Testing Setup

YAML
# docker-compose.test.yml
services:
  test:
    build:
      context: .
      target: test
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgres://postgres:test@postgres:5432/test
    depends_on:
      postgres:
        condition: service_healthy
    command: npm test
 
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: test
      POSTGRES_DB: test
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      timeout: 2s
      retries: 10

Compose Commands Reference

Lifecycle Commands

BASH
# Start services
docker compose up
docker compose up -d              # Detached
docker compose up --build         # Rebuild images
docker compose up --force-recreate # Recreate containers
 
# Stop services
docker compose stop              # Stop containers
docker compose down              # Stop and remove
docker compose down -v           # Also remove volumes
docker compose down --rmi all    # Also remove images
 
# Restart
docker compose restart
docker compose restart api

Viewing Status

BASH
# List services
docker compose ps
docker compose ps -a             # Include stopped
 
# View logs
docker compose logs
docker compose logs -f           # Follow
docker compose logs -f api       # Specific service
docker compose logs --tail 100   # Last 100 lines
 
# View resource usage
docker compose top

Executing Commands

BASH
# Run command in running container
docker compose exec api sh
docker compose exec api npm run migrate
 
# Run command in new container
docker compose run --rm api npm test
docker compose run --rm -e DEBUG=true api sh

Scaling Services

BASH
# Scale service to multiple instances
docker compose up -d --scale worker=3
 
# View scaled services
docker compose ps

Note

When scaling, avoid publishing host ports or use dynamic port allocation. Multiple containers can't bind to the same host port.

Managing Multiple Environments

Override Files

Docker Compose automatically loads docker-compose.override.yml:

YAML
# docker-compose.yml (base)
services:
  app:
    image: myapp:latest
 
# docker-compose.override.yml (development defaults)
services:
  app:
    build: .
    volumes:
      - .:/app
BASH
# Automatically uses both files
docker compose up
 
# Explicitly specify files
docker compose -f docker-compose.yml -f docker-compose.prod.yml up

Environment-Specific Files

PLAINTEXT
project/
├── docker-compose.yml           # Base configuration
├── docker-compose.override.yml  # Development (loaded by default)
├── docker-compose.prod.yml      # Production overrides
└── docker-compose.test.yml      # Testing overrides
BASH
# Development (uses override automatically)
docker compose up
 
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up
 
# Testing
docker compose -f docker-compose.yml -f docker-compose.test.yml up

Quick Reference

Common Commands

CommandPurpose
docker compose upCreate and start containers
docker compose up -dStart in background
docker compose downStop and remove containers
docker compose psList running services
docker compose logsView service logs
docker compose execExecute command in service
docker compose buildBuild service images
docker compose pullPull service images
docker compose restartRestart services

Compose File Keys

KeyPurpose
servicesDefine application services
volumesDefine named volumes
networksDefine networks
configsDefine configuration files
secretsDefine secrets

In the next chapter, we'll explore advanced Docker Compose features including profiles, environment management, and production configurations.