07 - Docker Compose

What Is Docker Compose?

Docker Compose lets you define and run multi-container applications with a single YAML file. Instead of running multiple docker run commands, you describe everything in docker-compose.yml.

bash
# Without Compose (painful) docker network create myapp docker volume create db-data docker run -d --name db --network myapp -v db-data:/var/lib/postgresql/data -e POSTGRES_PASSWORD=secret postgres:16 docker run -d --name redis --network myapp redis:7-alpine docker run -d --name api --network myapp -p 3000:3000 -e DB_HOST=db -e REDIS_HOST=redis myapi docker run -d --name web --network myapp -p 80:80 mynginx # With Compose (one command) docker compose up -d

Compose File Structure

yaml
# docker-compose.yml (or compose.yml) # Optional: specify compose version (modern compose ignores this) # version: "3.8" # Legacy, not needed with Docker Compose V2 services: # --- Service definitions --- web: image: nginx:alpine ports: - "80:80" api: build: ./api ports: - "3000:3000" environment: - DB_HOST=db db: image: postgres:16 volumes: - db-data:/var/lib/postgresql/data # --- Named volumes --- volumes: db-data: # --- Custom networks --- networks: frontend: backend:

Complete Real-World Example

yaml
# compose.yml - Full-stack web application services: # ======= Frontend ======= web: build: context: ./frontend dockerfile: Dockerfile args: - NODE_ENV=production ports: - "3000:3000" environment: - API_URL=http://api:8080 depends_on: api: condition: service_healthy networks: - frontend restart: unless-stopped # ======= Backend API ======= api: build: context: ./backend dockerfile: Dockerfile target: production # Multi-stage build target ports: - "8080:8080" environment: - DATABASE_URL=postgresql://app:secret@db:5432/myapp - REDIS_URL=redis://redis:6379 - JWT_SECRET=${JWT_SECRET} # From .env file depends_on: db: condition: service_healthy redis: condition: service_started healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 5s retries: 3 start_period: 10s networks: - frontend - backend restart: unless-stopped deploy: resources: limits: memory: 512M cpus: "1.0" # ======= Database ======= db: image: postgres:16-alpine environment: - POSTGRES_DB=myapp - POSTGRES_USER=app - POSTGRES_PASSWORD=secret volumes: - db-data:/var/lib/postgresql/data - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro ports: - "5432:5432" # Expose for local dev tools healthcheck: test: ["CMD-SHELL", "pg_isready -U app -d myapp"] interval: 10s timeout: 5s retries: 5 networks: - backend restart: unless-stopped # ======= Cache ======= redis: image: redis:7-alpine command: redis-server --appendonly yes --maxmemory 256mb volumes: - redis-data:/data networks: - backend restart: unless-stopped # ======= Background Worker ======= worker: build: context: ./backend target: production command: ["node", "worker.js"] environment: - DATABASE_URL=postgresql://app:secret@db:5432/myapp - REDIS_URL=redis://redis:6379 depends_on: db: condition: service_healthy redis: condition: service_started networks: - backend restart: unless-stopped volumes: db-data: driver: local redis-data: driver: local networks: frontend: driver: bridge backend: driver: bridge

Service Configuration Deep Dive

Build Options

yaml
services: api: # Simple build build: ./api # Detailed build config build: context: ./api # Build context directory dockerfile: Dockerfile.prod # Custom Dockerfile name target: production # Multi-stage target args: # Build arguments - NODE_ENV=production cache_from: # Cache sources - myregistry/api:cache platforms: # Multi-platform - linux/amd64 - linux/arm64

Environment Variables

yaml
services: api: # Inline variables environment: - DB_HOST=db - DB_PORT=5432 # Map syntax environment: DB_HOST: db DB_PORT: "5432" # From .env file env_file: - .env - .env.local # Overrides .env # Reference host environment variable environment: - API_KEY # Uses host's $API_KEY value - SECRET=${MY_SECRET:-default_value} # With default

.env File

bash
# .env (automatically loaded by Compose) POSTGRES_PASSWORD=secret JWT_SECRET=my-super-secret API_KEY=abc123 COMPOSE_PROJECT_NAME=myapp # Prefix for container names

Depends On

yaml
services: api: depends_on: # Simple form (just startup order) - db - redis # Advanced form (with health conditions) db: condition: service_healthy # Wait for healthcheck to pass redis: condition: service_started # Just wait for container start migrations: condition: service_completed_successfully # Wait for exit 0

Volumes

yaml
services: api: volumes: # Named volume - data:/app/data # Bind mount (short syntax) - ./src:/app/src # Bind mount (long syntax) - type: bind source: ./src target: /app/src read_only: true # Anonymous volume (prevent overwriting node_modules) - /app/node_modules # tmpfs - type: tmpfs target: /tmp tmpfs: size: 100000000 # 100 MB volumes: data: driver: local # External volume (created outside Compose) shared: external: true

Networking

yaml
services: api: networks: - frontend - backend # With aliases networks: backend: aliases: - api-server - backend-api networks: frontend: driver: bridge backend: driver: bridge ipam: config: - subnet: 172.28.0.0/16

Resource Limits

yaml
services: api: deploy: resources: limits: memory: 512M cpus: "1.5" reservations: memory: 256M cpus: "0.5"

Essential Compose Commands

bash
# Start all services docker compose up # Start in background docker compose up -d # Start specific services docker compose up -d api db # Build and start docker compose up -d --build # Stop all services docker compose down # Stop and remove volumes too docker compose down -v # Stop and remove everything (images too) docker compose down --rmi all -v # View running services docker compose ps # View logs docker compose logs docker compose logs -f api # Follow specific service docker compose logs --tail 50 # Last 50 lines # Execute command in running service docker compose exec api bash docker compose exec db psql -U app -d myapp # Run a one-off command docker compose run --rm api npm test # Scale a service docker compose up -d --scale worker=3 # Restart a service docker compose restart api # Rebuild a specific service docker compose build api docker compose up -d api # View service resource usage docker compose top docker compose stats

Multiple Compose Files (Override Pattern)

yaml
# docker-compose.yml (base) services: api: build: ./api environment: - NODE_ENV=production # docker-compose.override.yml (auto-loaded for dev) services: api: build: target: development volumes: - ./api/src:/app/src # Live reload environment: - NODE_ENV=development - DEBUG=true ports: - "9229:9229" # Debug port # docker-compose.prod.yml (production overrides) services: api: image: myregistry/api:latest # Use pre-built image restart: always deploy: replicas: 3
bash
# Development (base + override automatically) docker compose up -d # Production (base + prod, skip override) docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d # Or use COMPOSE_FILE env var export COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml docker compose up -d

Compose Profiles

Group services that should start together:

yaml
services: api: build: ./api # No profile = always starts db: image: postgres:16 # No profile = always starts adminer: image: adminer profiles: - debug # Only starts with --profile debug prometheus: image: prom/prometheus profiles: - monitoring grafana: image: grafana/grafana profiles: - monitoring
bash
# Start only default services (api, db) docker compose up -d # Start with debug tools docker compose --profile debug up -d # Start with monitoring docker compose --profile monitoring up -d # Start everything docker compose --profile debug --profile monitoring up -d

Docker Compose Watch (Dev Live Reload)

yaml
# compose.yml services: api: build: ./api develop: watch: - action: sync # Sync files without rebuilding path: ./api/src target: /app/src - action: rebuild # Rebuild on dependency changes path: ./api/package.json - action: sync+restart # Sync and restart the service path: ./api/config target: /app/config
bash
docker compose watch # Starts services with file watching

FAANG Interview Angle

Common questions:

  1. "How does Docker Compose differ from Docker Swarm or Kubernetes?"
  2. "How do services communicate in Docker Compose?"
  3. "How would you handle secrets in Compose?"
  4. "What's your development workflow with Docker Compose?"
  5. "How do you manage different environments?"

Key answers:

  • Compose is for defining multi-container apps on a single host; Swarm/K8s handle orchestration across hosts
  • Services on the same Compose network communicate by service name (DNS)
  • Use .env files locally, Docker secrets or external vaults in production
  • Base compose + override file pattern (dev/staging/prod)
  • docker compose watch for development hot-reload

Official Links