Same compose.yml, different environments. Here’s the cleanest approach.
Basic setup
Create different env files for each environment:
.env.dev
DATABASE_URL=postgresql://localhost:5432/dev_db
API_KEY=dev_key_12345
LOG_LEVEL=debug
REPLICAS=1
.env.prod
DATABASE_URL=postgresql://prod-db.example.com:5432/prod_db
API_KEY=${SECURE_API_KEY} # From CI/CD secrets
LOG_LEVEL=error
REPLICAS=3
How to use them
# Development
docker compose --env-file .env.dev up
# Production
docker compose --env-file .env.prod up
# Override specific vars
API_KEY=test_key docker compose --env-file .env.dev up
Layering configs
You can use multiple env files:
# Base + environment-specific
docker compose \
--env-file .env.base \
--env-file .env.prod \
up
Note: Later files override earlier ones.
Recommended project structure
This works well:
project/
├── compose.yml
├── .env # Git-ignored, local overrides
├── .env.example # Committed, template for team
└── environments/
├── .env.dev # Development defaults
├── .env.staging # Staging config
└── .env.prod # Production (maybe in CI/CD)
Debugging
# See what Compose is using
docker compose --env-file .env.prod config
# Check specific variable
docker compose run --rm web printenv DATABASE_URL
Git strategy
What I ignore:
.env
.env.local
.env.*.local
What I commit:
.env.example
.env.dev
New team members just run cp .env.example .env and they’re ready.