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.

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.