Docker Compose¶
Docker Compose is a tool for defining and running multi-container Docker applications. Instead of managing each container with separate docker run commands - each with its own flags for ports, volumes, networks, and environment variables - you declare everything in a single YAML file and bring the entire stack up or down with one command.
Why Compose Exists¶
Running a web application typically means running at least three containers: the application itself, a database, and maybe a cache or message queue. Managing these by hand means remembering the exact docker run invocation for each one, creating networks, and starting them in the right order. Docker Compose solves this by letting you define the entire stack declaratively.
The benefits go beyond convenience:
- Reproducible environments: Every developer runs the same stack with
docker compose up. - Service discovery: Containers on a Compose network reach each other by service name - no IP addresses to manage.
- Single-command lifecycle: Start, stop, rebuild, and tear down the entire stack in one step.
- Environment parity: The same
compose.ymlworks in development, CI, and staging.
The Compose File¶
The core of Docker Compose is a YAML file named compose.yml (or docker-compose.yml for backward compatibility). It defines services, networks, and volumes.
A Production-Style Example¶
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
DATABASE_URL: postgres://admin:secret@db:5432/myapp
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
restart: unless-stopped
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: myapp
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d myapp"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
cache:
image: redis:7-alpine
restart: unless-stopped
volumes:
pgdata:
depends_on does not wait for readiness
By default, depends_on only waits for a container to start, not for the service inside to be ready. A PostgreSQL container can take several seconds to initialize its database after the process starts. Use condition: service_healthy with a healthcheck to ensure the database is actually accepting connections before the web service starts.
Key Directives¶
| Directive | Purpose |
|---|---|
services |
Defines each container in your application stack. |
image |
Use a pre-built image from a registry. |
build |
Build an image from a Dockerfile in the specified context. |
ports |
Map host ports to container ports (HOST:CONTAINER). |
volumes |
Mount named volumes or bind paths into the container. |
environment |
Set environment variables. Supports both mapping and list syntax. |
depends_on |
Control startup order. Use with condition for health-based ordering. |
healthcheck |
Define a command to check if the service is healthy. |
restart |
Restart policy: no, always, on-failure, unless-stopped. |
networks |
Attach the service to specific networks. |
command |
Override the default CMD from the image. |
profiles |
Assign the service to a profile so it only starts when that profile is active. |
Environment Variables and .env Files¶
Hardcoding secrets in compose.yml is fine for local development, but Compose supports .env files for separating configuration from the stack definition.
Variable Substitution¶
Compose resolves ${VARIABLE} references from the shell environment, a .env file in the same directory, or the environment directive.
services:
db:
image: postgres:${POSTGRES_VERSION:-16}
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
The env_file Directive¶
For services that need many environment variables, you can point to a file instead of listing them inline:
Files are loaded in order. Later files override earlier ones. Variables set directly in environment override everything.
Volumes: Named vs Bind Mounts¶
Compose supports two primary volume strategies, and choosing the right one matters.
Named Volumes (Production)¶
Named volumes are managed by Docker. They persist data across container restarts and removals, and they're the right choice for databases, file uploads, and any data that must survive a docker compose down.
Bind Mounts (Development)¶
Bind mounts map a host directory into the container. They're essential for development because changes to files on the host appear immediately inside the container - no rebuild needed.
services:
web:
build: .
volumes:
- .:/app # Source code (live reloading)
- /app/node_modules # Prevent host node_modules from overriding container's
ports:
- "3000:3000"
Named volumes survive docker compose down
Running docker compose down removes containers and networks but keeps named volumes. If you want to delete the data too, use docker compose down -v. This is destructive and irreversible - the database data is gone.
Networking¶
Compose automatically creates a bridge network for each project. Every service can reach every other service by its service name as a hostname.
services:
web:
build: .
# Can connect to "db" at hostname "db" on port 5432
# No port publishing needed for service-to-service communication
db:
image: postgres:16
# Only publish if you need direct host access (e.g., for a GUI client)
# ports:
# - "5432:5432"
For more complex setups, you can define multiple networks to isolate groups of services:
services:
web:
networks:
- frontend
- backend
db:
networks:
- backend
nginx:
networks:
- frontend
networks:
frontend:
backend:
In this configuration, nginx can reach web but cannot reach db directly - the database is only accessible on the backend network.
Common Compose Commands¶
# Start the stack (build images if needed)
docker compose up -d
# Rebuild images and start
docker compose up -d --build
# Stop and remove containers and networks
docker compose down
# Stop, remove containers, networks, AND volumes
docker compose down -v
# View logs from all services
docker compose logs -f
# View logs from a single service
docker compose logs -f web
# List running services
docker compose ps
# Run a one-off command in a service container
docker compose exec db psql -U admin -d myapp
# Run a command in a new container (doesn't attach to the running one)
docker compose run --rm web python manage.py migrate
# Scale a service to multiple replicas
docker compose up -d --scale worker=3
Override Files and Profiles¶
Override Files¶
Compose automatically merges compose.yml with compose.override.yml if it exists. This is the standard pattern for separating production defaults from development customizations.
# compose.yml (production defaults)
services:
web:
image: my-app:latest
restart: unless-stopped
# compose.override.yml (development overrides)
services:
web:
build: .
volumes:
- .:/app
environment:
DEBUG: "true"
In development, docker compose up merges both files - building from source with a bind mount. In production, deploy with docker compose -f compose.yml up to skip the override.
Profiles¶
Profiles let you include optional services that only start when explicitly requested.
services:
web:
build: .
db:
image: postgres:16
adminer:
image: adminer
ports:
- "8080:8080"
profiles:
- debug
# Normal startup (adminer is NOT started)
docker compose up -d
# Start with the debug profile (includes adminer)
docker compose --profile debug up -d
Interactive Quizzes¶
Further Reading¶
- Docker Compose Documentation - official getting-started guide and concepts
- Compose File Reference - complete specification of every directive
- Awesome Compose - sample Compose files for common application stacks
- Docker Compose in Production - guidelines for deploying Compose stacks beyond development
Previous: Docker Fundamentals | Next: Dockerfile Best Practices | Back to Index