IntermediateLesson 8 of 16

🎼 Docker Compose for Local Multi-Container Apps

Define and run multi-container applications with a single YAML file — web server, database, cache, and workers all up with one command.

🧒 Simple Explanation (ELI5)

Running a web app manually means typing 5 long docker run commands with networks, volumes, and port mappings — every time. Docker Compose is a conductor's score: you write down all the musicians (services), their instruments (images), and how they interact, then tap the baton and the whole orchestra starts at once.

💡
Compose vs Kubernetes

Docker Compose is for local development and simple deployments. Kubernetes is for production at scale. Compose is where you prototype — once it works locally, you translate services to K8s Deployments/Services for production.

🔧 docker-compose.yml Anatomy

yaml
version: "3.9"

services:
  # --- Web API ---
  api:
    build:
      context: .
      dockerfile: Dockerfile
    image: myapp-api:dev
    ports:
      - "3000:3000"           # host:container
    environment:
      - NODE_ENV=development
      - DB_HOST=db            # reaches postgres container by service name
      - DB_PORT=5432
      - REDIS_URL=redis://cache:6379
    volumes:
      - ./src:/app/src        # bind mount for hot-reload in dev
    depends_on:
      db:
        condition: service_healthy   # wait for DB health check
      cache:
        condition: service_started
    networks:
      - app-network
    restart: unless-stopped

  # --- PostgreSQL Database ---
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: secret   # in dev only; use secrets in prod
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  # --- Redis Cache ---
  cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    networks:
      - app-network

volumes:
  pgdata:             # named volume persists DB data across restarts

networks:
  app-network:
    driver: bridge

💻 Compose Commands

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

# Start and rebuild images first
docker compose up -d --build

# View status of all services
docker compose ps

# View logs from all services (follow)
docker compose logs -f

# View logs from one service
docker compose logs -f api

# Stop all services (preserve volumes)
docker compose down

# Stop and remove volumes too (resets data)
docker compose down -v

# Run a one-off command in a service container
docker compose exec api /bin/sh
docker compose exec db psql -U user -d myapp

# Scale a service (multiple instances)
docker compose up -d --scale api=3

# Pull latest images
docker compose pull

🐛 Debugging Scenario

Problem: API fails to connect to DB on startup — "connection refused". API starts before the database is ready.

yaml
# Fix: use depends_on with health check condition
services:
  api:
    depends_on:
      db:
        condition: service_healthy  # wait until DB passes health check

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

# Note: depends_on only ensures order of start, not "app readiness"
# Your app should also have retry logic for DB connections

🎯 Interview Questions

What does docker compose up vs docker compose start do?▾

docker compose up creates and starts containers. If containers already exist but are stopped, it recreates them if the config has changed, or starts them as-is. docker compose start only starts already-created but stopped containers — it does NOT create new containers or apply config changes. Use up for most workflows.

How do services in a Compose file find each other?▾

Docker Compose creates a user-defined bridge network for all services in the same compose file. Docker's embedded DNS server resolves service names to container IPs. A service named db is reachable at hostname db from any other service in the same Compose file, without any manual network configuration.

Scenario: You have dev and prod docker-compose files. How do you structure overrides?▾

Use Compose file layering: a docker-compose.yml base with shared config, a docker-compose.override.yml that is auto-applied in development (dev volumes, debug ports), and a docker-compose.prod.yml for production-specific settings. Run production with: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d. This keeps DRY config with environment-specific overrides.

📋 Summary