YAML for Docker Compose: Configuration Patterns and Tips
Docker Compose uses YAML to define multi-container applications. A well-structured docker-compose.yml is the difference between a smooth development environment and hours of debugging container issues. This guide covers essential patterns and common pitfalls.
Basic Structure
services:
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./html:/usr/share/nginx/html
depends_on:
- api
api:
build: ./api
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://db:5432/myapp
depends_on:
- db
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: myapp
POSTGRES_PASSWORD: secret
volumes:
pgdata:
Environment Variables
Inline
services:
api:
environment:
NODE_ENV: production
PORT: "3000"
DEBUG: "false"
From File
services:
api:
env_file:
- .env
- .env.local # Overrides .env values
Variable Substitution
Reference host environment variables:
services:
api:
image: myapp:${TAG:-latest} # Default to "latest"
environment:
API_KEY: ${API_KEY} # Required - fails if not set
LOG_LEVEL: ${LOG_LEVEL:-info} # Default to "info"
Networking
Custom Networks
services:
web:
networks:
- frontend
api:
networks:
- frontend
- backend
db:
networks:
- backend
networks:
frontend:
backend:
internal: true # No external access
Services on the same network can reach each other by service name. The db service is only reachable from api, not from web.
Port Mapping
ports:
- "8080:80" # host:container
- "443:443"
- "127.0.0.1:3000:3000" # Bind to localhost only
- "3000" # Random host port
Volumes
Named Volumes (Persistent)
services:
db:
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
driver: local
Bind Mounts (Development)
services:
api:
volumes:
- ./src:/app/src # Source code
- /app/node_modules # Anonymous volume (prevent override)
The anonymous volume pattern (/app/node_modules) prevents the host bind mount from overwriting container-installed dependencies.
Health Checks
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
api:
depends_on:
db:
condition: service_healthy
This ensures api only starts after db passes health checks β not just when the container starts.
Extension Fields (DRY Patterns)
Use x- prefixed fields with YAML anchors to reduce repetition:
x-common: &common
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
x-env: &common-env
TZ: UTC
LOG_FORMAT: json
services:
api:
<<: *common
build: ./api
environment:
<<: *common-env
PORT: "3000"
worker:
<<: *common
build: ./worker
environment:
<<: *common-env
QUEUE: default
For more on YAML anchors, see our YAML anchors and aliases guide.
Multi-Stage Compose
Development vs Production
# docker-compose.yml (base)
services:
api:
build: ./api
environment:
DATABASE_URL: postgres://db:5432/myapp
# docker-compose.override.yml (dev - auto-loaded)
services:
api:
volumes:
- ./api/src:/app/src
environment:
NODE_ENV: development
DEBUG: "true"
ports:
- "3000:3000"
- "9229:9229" # Debug port
# docker-compose.prod.yml
services:
api:
environment:
NODE_ENV: production
deploy:
replicas: 3
# Development (auto-loads override)
docker compose up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up
Common Pitfalls
1. Unquoted Port Numbers
# RISKY - YAML interprets 80:80 as a base-60 number
ports:
- 80:80
# SAFE - always quote
ports:
- "80:80"
2. Boolean Environment Variables
# WRONG - YAML converts to boolean
environment:
DEBUG: true # Becomes Python True, not string "true"
# CORRECT - quote all env values
environment:
DEBUG: "true"
3. depends_on Timing
depends_on only waits for container start, not readiness:
# Container starts but DB might not be ready
depends_on:
- db
# Wait for health check
depends_on:
db:
condition: service_healthy
Validate your Docker Compose files with our YAML Validator.
FAQ
What is the difference between docker-compose.yml and compose.yml?
Both file names work. compose.yml is the newer convention recommended by Docker. docker-compose.yml is the legacy name. Docker Compose v2+ looks for both, preferring compose.yml if both exist. For new projects, use compose.yml.
Should I use Docker Compose in production?
Docker Compose is excellent for development and simple deployments. For production at scale, consider Docker Swarm (uses Compose files natively) or Kubernetes. The advantage of Compose is that your development configuration directly informs production, reducing environment differences.
Related Resources
- YAML Validator β Validate Docker Compose YAML
- YAML Anchors and Aliases β DRY configuration patterns
- YAML Syntax Tutorial β YAML fundamentals