Skip to content

Docker Compose

Running containers individually with docker run is fine until you need three services talking to each other and you're typing 200-character CLI flags at 2AM Docker Compose defines multi-container applications in YAML — one file to rule them all , one command to start everything

docker-compose.yml — Your Application Blueprint

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp

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

volumes:
  pgdata:

Services — The Containers You're Running

Each service maps to one container. Docker Compose handles networking so services can reach each other by service name

Common service patterns:

services:
  # Node.js backend
  api:
    build: ./api
    ports:
      - "3000:3000"
    env_file: .env
    volumes:
      - ./api:/app          # Mount for live reload in dev

  # PostgreSQL database  
  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}

  # Redis for caching
  cache:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}

  # Nginx reverse proxy
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - api

Networks — How Containers Talk

By default , Docker Compose creates one network and all services join it You can define custom networks for isolation

services:
  app:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend

  nginx:
    networks:
      - frontend

networks:
  frontend:
  backend:
    internal: true    # No external access to backend network

Service app can reach db by the hostname db (service name) Service nginx cannot reach db — network isolation prevents it

Volumes — Persistence That Survives Rebuilds

services:
  db:
    volumes:
      - pgdata:/var/lib/postgresql/data  # Named volume
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql  # Bind mount init script

  app:
    volumes:
      - .:/app          # Bind mount for dev (live reload)
      - /app/node_modules  # Anonymous volume (node_modules from image)

volumes:
  pgdata:    # Named volume persists even after docker compose down

Volume types: * Named volumes — Docker-managed , survives compose down , use for databases * Bind mounts — host path mapped to container , use for dev live reload * Anonymous volumes — created on the fly , mostly avoid

Environment Variables — Don't Hardcode Secrets

services:
  app:
    # Method 1: inline (bad for secrets)
    environment:
      - NODE_ENV=production

    # Method 2: .env file (better)
    env_file: .env

    # Method 3: from host environment (for CI)
    environment:
      - DATABASE_URL

Compose automatically reads .env file from the same directory You reference variables in compose YAML with ${VAR_NAME} syntax

Profiles — Run Subsets of Services

services:
  app:
    image: myapp

  db:
    image: postgres:16-alpine

  redis:
    image: redis:7-alpine

  adminer:
    image: adminer
    profiles:
      - tools       # Only starts with --profile tools

  mailhog:
    image: mailhog/mailhog
    profiles:
      - tools       # Only starts with --profile tools

  prometheus:
    image: prom/prometheus
    profiles:
      - monitoring
# Start only app + db + redis
docker compose up -d

# Start with tools profile
docker compose --profile tools up -d

# Start with everything
docker compose --profile "*" up -d

Fullstack App Example — The Works

version: '3.8'

services:
  # React frontend
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - REACT_APP_API_URL=http://localhost:4000
    depends_on:
      - api

  # Express API
  api:
    build: ./api
    ports:
      - "4000:4000"
    env_file: ./api/.env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  # PostgreSQL
  db:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis cache
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes

  # Nginx reverse proxy
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - frontend
      - api

volumes:
  pgdata:
  redis_data:

Essential Compose Commands

# Start all services
docker compose up -d

# Rebuild and start
docker compose up -d --build

# View logs
docker compose logs -f app

# Execute command in service
docker compose exec app bash

# Stop services (keeps volumes)
docker compose down

# Stop services AND remove volumes (careful — data gone)
docker compose down -v

# List running services
docker compose ps

# View resource usage
docker compose top

Prerequisites

Dockerfile section , Node.js section


next → devops_04_container_security.md