Skip to main content
Technology & EngineeringContainerization206 lines

Docker Compose

Docker Compose configuration for multi-service development, testing, and local orchestration

Quick Summary11 lines
You are an expert in Docker Compose for containerized application development and deployment.

## Key Points

- Use `depends_on` with `condition: service_healthy` instead of relying on startup order alone.
- Keep secrets out of compose files; use `env_file` pointing to `.env` files excluded from version control.
- Use `tmpfs` mounts for test databases to avoid disk I/O and leftover state.
- Forgetting that `compose.override.yaml` is loaded automatically, which can silently change behavior when you intended to use only the base file.
- Using `links` instead of the default Compose network; `links` is a legacy feature and unnecessary in modern Compose.
skilldb get containerization-skills/Docker ComposeFull skill: 206 lines
Paste into your CLAUDE.md or agent config

Docker Compose — Containerization

You are an expert in Docker Compose for containerized application development and deployment.

Overview

Docker Compose defines and runs multi-container applications using a declarative YAML file. It manages service dependencies, networking, volumes, and environment configuration, making it the standard tool for local development environments and integration testing.

Core Concepts

Service Definition

Each service maps to a container. Compose handles building, networking, and lifecycle:

# compose.yaml
services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/app
    depends_on:
      db:
        condition: service_healthy

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

volumes:
  pgdata:

Networking

Compose creates a default network for all services. Services communicate by service name as hostname:

services:
  web:
    build: ./web
    networks:
      - frontend
      - backend

  api:
    build: ./api
    networks:
      - backend

  redis:
    image: redis:7-alpine
    networks:
      - backend

networks:
  frontend:
  backend:

Volumes and Bind Mounts

services:
  app:
    build: .
    volumes:
      # Bind mount for live reload in development
      - ./src:/app/src
      # Named volume for persistent data
      - app-data:/app/data
      # Anonymous volume to prevent overwriting node_modules
      - /app/node_modules

volumes:
  app-data:

Implementation Patterns

Development vs. Production Overrides

Use a base file with environment-specific overrides:

# compose.yaml (base)
services:
  api:
    image: myapp/api:latest
    environment:
      NODE_ENV: production

# compose.override.yaml (auto-loaded in dev)
services:
  api:
    build: ./api
    volumes:
      - ./api/src:/app/src
    environment:
      NODE_ENV: development
      DEBUG: "true"
    ports:
      - "9229:9229"  # debugger

Integration Testing

# compose.test.yaml
services:
  test-runner:
    build:
      context: .
      target: test
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      DATABASE_URL: postgres://test:test@db:5432/testdb
    command: npm run test:integration

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: testdb
    tmpfs:
      - /var/lib/postgresql/data  # ephemeral for speed

  redis:
    image: redis:7-alpine

Run with: docker compose -f compose.test.yaml run --rm test-runner

Profiles for Optional Services

services:
  api:
    build: ./api

  monitoring:
    image: grafana/grafana:latest
    profiles:
      - observability
    ports:
      - "3001:3000"

Start with monitoring: docker compose --profile observability up

Best Practices

  • Use depends_on with condition: service_healthy instead of relying on startup order alone.
  • Keep secrets out of compose files; use env_file pointing to .env files excluded from version control.
  • Use tmpfs mounts for test databases to avoid disk I/O and leftover state.

Core Philosophy

Docker Compose is the bridge between a developer's laptop and a production-like environment. Its primary value is making multi-service applications trivially reproducible: a new team member should be able to run docker compose up and have a working development environment in minutes, not hours. Every compose file should be written with this onboarding experience in mind.

Compose files are living documentation of your application's service topology. They declare which services exist, how they connect, what they depend on, and what configuration they need. When the compose file accurately reflects the production architecture (same database engine, same message broker, same service boundaries), the gap between "works on my machine" and "works in production" shrinks dramatically. Divergence between the compose setup and production is a source of integration bugs that only surface during deployment.

The separation between base configuration and environment-specific overrides is a core design pattern. The base compose.yaml describes the service topology and production-like defaults. The compose.override.yaml (auto-loaded in development) adds bind mounts for live reload, debug ports, and development-only settings. Test-specific compose files strip away unnecessary services and use ephemeral storage for speed. This layered approach avoids duplicating service definitions across environments.

Anti-Patterns

  • Hardcoding secrets directly in compose files. Putting database passwords, API keys, and tokens in plain text in compose.yaml means they end up in version control. Use env_file directives pointing to .env files that are excluded from Git, or use Docker secrets for sensitive values.

  • Using Compose for production deployments. Compose is designed for development and testing. It lacks health-check-based rolling deployments, node scheduling, secret rotation, and the resilience features that production workloads require. Use Kubernetes, ECS, or Docker Swarm for production orchestration.

  • Relying on startup order instead of health checks. depends_on without condition: service_healthy only ensures that the dependency container has started, not that it is ready to accept connections. The application will crash on startup if the database is still initializing.

  • Bind-mounting node_modules or vendor directories. Mounting the project root into the container can overwrite the container's installed dependencies with the host's (or an empty directory). Use anonymous volumes or .dockerignore to prevent dependency directory conflicts.

  • One mega compose file with 15+ services. A compose file that starts every service in the organization makes startup slow, resource-hungry, and fragile. Scope compose files to the services a developer actually needs, and use profiles or separate files for optional auxiliary services.

Common Pitfalls

  • Forgetting that compose.override.yaml is loaded automatically, which can silently change behavior when you intended to use only the base file.
  • Using links instead of the default Compose network; links is a legacy feature and unnecessary in modern Compose.

Install this skill directly: skilldb add containerization-skills

Get CLI access →