Real applications rarely run in a single container. Docker Compose defines multi-container stacks in a YAML file and manages them with one command.

Why Docker Compose?

A web app typically needs:

  • Application server (Node.js, Python)
  • Database (PostgreSQL, MySQL)
  • Cache (Redis)
  • Reverse proxy (Nginx)

Compose lets you define, start, and stop the entire stack:

  docker compose up -d    # start all services
docker compose down     # stop and remove
  

Basic docker-compose.yml

  # docker-compose.yml
services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:
  

Start the stack:

  docker compose up -d
docker compose ps
docker compose logs -f web
  

Service Configuration

Build vs Image

  services:
  api:
    # Build from Dockerfile in current directory
    build:
      context: .
      dockerfile: Dockerfile
      args:
        BUILD_VERSION: "1.0.0"

  nginx:
    # Use pre-built image from registry
    image: nginx:1.25-alpine
  

Environment Variables

  services:
  web:
    environment:
      NODE_ENV: production
      PORT: 3000
    env_file:
      - .env
      - .env.production
  

.env file (not committed to Git):

  DATABASE_URL=postgres://user:pass@db:5432/myapp
JWT_SECRET=your-secret-here
  

Port Mapping

  ports:
  - "8080:80"       # host:container
  - "127.0.0.1:3000:3000"  # bind to localhost only
  

Volumes

Persist data beyond container lifecycle:

  volumes:
  # Named volume (managed by Docker)
  pgdata:

services:
  db:
    volumes:
      - pgdata:/var/lib/postgresql/data

  web:
    # Bind mount — sync local files for development
    volumes:
      - ./src:/app/src:ro   # read-only mount
  

Networks

Services on the same Compose network communicate by service name:

  networks:
  frontend:
  backend:

services:
  web:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend    # not exposed to frontend network
  

The web service reaches the database at hostname db (the service name).

Development vs Production Overrides

  # docker-compose.override.yml (auto-loaded in dev)
services:
  web:
    build:
      target: development
    volumes:
      - ./src:/app/src
    command: npm run dev
    environment:
      NODE_ENV: development
  
  # docker-compose.prod.yml
services:
  web:
    restart: always
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
  

Production:

  docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
  

Full Stack Example — Laravel + MySQL + Redis

  services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/var/www/html
    depends_on:
      - db
      - redis

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - .:/var/www/html
    depends_on:
      - app

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel
    volumes:
      - mysqldata:/var/lib/mysql

  redis:
    image: redis:7-alpine

volumes:
  mysqldata:
  

Common Commands

  docker compose up -d              # Start detached
docker compose up --build         # Rebuild images first
docker compose down               # Stop and remove containers
docker compose down -v            # Also remove volumes (DATA LOSS)
docker compose ps                 # List services
docker compose logs web           # Logs for one service
docker compose logs -f --tail=50  # Follow last 50 lines
docker compose exec web sh        # Shell into running container
docker compose exec db psql -U user -d myapp
docker compose restart web        # Restart one service
docker compose pull               # Pull latest images
  

depends_on and Healthchecks

depends_on alone only waits for container start — not readiness. Use healthchecks:

  db:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U user"]
    interval: 5s
    retries: 5

web:
  depends_on:
    db:
      condition: service_healthy
  

Scaling Services

  docker compose up -d --scale web=3
  

Requires a load balancer (Nginx, Traefik) in front — Compose alone does not load-balance.

What Comes Next

Learn production security, image scanning, and orchestration alternatives in Production Docker, then automate builds with CI/CD.