Multi-Environment Overrides
Most applications need different settings for development, staging, and production -- different ports, volumes, resource limits, and image tags. Compose handles this with override files that layer on top of a base configuration.
How It Works
flowchart LR
Base["compose.yaml<br/>(shared architecture)"] --> Override["compose.override.yaml<br/>(local dev defaults)"]
Base --> Prod["compose.prod.yaml<br/>(production hardening)"]
Base --> Staging["compose.staging.yaml<br/>(staging tweaks)"]
Override -->|"docker compose up"| Dev["Dev Stack"]
Prod -->|"docker compose -f ... -f ... up"| ProdStack["Production Stack"]
style Base fill:#e3f2fd,stroke:#1565c0
style Dev fill:#e8f5e9,stroke:#2e7d32
style ProdStack fill:#fff3e0,stroke:#ef6c00
Compose automatically merges compose.override.yaml with the base file. For other environments, you specify files explicitly.
File Layout
project/
├── compose.yaml # Base: services, networks, volumes
├── compose.override.yaml # Auto-loaded for local dev
├── compose.prod.yaml # Production overrides
├── .env.example # Template for env vars
└── .env # Local env vars (gitignored)
What Goes Where
| Setting | Base File | Dev Override | Production Override |
|---|---|---|---|
| Service definitions | ✓ | ||
| Network topology | ✓ | ||
| Named volumes | ✓ | ||
| Source code bind mounts | ✓ | ||
Debug ports (127.0.0.1) | ✓ | ||
| Pinned image tags | ✓ | ||
| Resource limits | ✓ | ||
| Strict restart policies | ✓ | ||
| Public port bindings | ✓ |
Example: Base File
# compose.yaml -- shared architecture
services:
api:
image: registry.example.com/api:${API_TAG:-latest}
networks: [back]
depends_on:
db: { condition: service_healthy }
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
retries: 5
networks: [back]
networks:
back: {}
volumes:
pgdata: {}
Example: Dev Override
# compose.override.yaml -- auto-loaded for local development
services:
api:
build: ./api # Build from source instead of pulling image
volumes:
- ./api/src:/app/src # Live code reload
ports:
- "127.0.0.1:3000:3000" # Local debug access
environment:
LOG_LEVEL: debug
Example: Production Override
# compose.prod.yaml -- explicit production hardening
services:
api:
image: registry.example.com/api:1.5.2 # Pinned tag
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
db:
restart: unless-stopped
deploy:
resources:
limits:
memory: 1G
Deploying Each Environment
# Local development (auto-merges compose.override.yaml)
docker compose up -d
# Production (explicit file selection)
docker compose -f compose.yaml -f compose.prod.yaml up -d
# Staging
docker compose -f compose.yaml -f compose.staging.yaml up -d
Always Validate Before Deploy
Render the merged configuration and review it:
# See exactly what will be deployed in production
docker compose -f compose.yaml -f compose.prod.yaml config
This catches merge issues, missing variables, and unexpected overrides before they cause problems.
Key Takeaways
- Keep the base file environment-agnostic with shared service definitions, networks, and volumes.
- Use
compose.override.yamlfor local dev conveniences (bind mounts, debug ports, build from source). It is merged automatically. - Use
compose.prod.yamlfor production hardening (pinned tags, resource limits, restart policies). Specify explicitly with-f. - Always run
docker compose configwith your file flags to see the merged result before deploying. - Do not duplicate entire service definitions across override files -- only include the keys you are changing.
What's Next
- Continue to Production Compose Workflow to learn the full deploy, verify, and rollback process.