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