Use Multiple Docker Compose Files for Dev, Staging, and Prod
Keep one clear Compose baseline and layer environment-specific overrides on top so your deployments stay understandable as they grow.
How to split a Compose setup into a shared base plus development, staging, and production overrides without duplicating whole files.
Teams and solo operators who want local convenience, safer previews, and more production discipline from the same app definition.
Copy-pasting separate full Compose files often creates drift, where staging or production behaves differently for reasons nobody notices until deploy day.
Before you begin
- Docker Compose installed and working.
- A small app stack you already understand at a basic level.
- Different environment needs, such as mounted source code in development or stricter restart behavior in production.
- A willingness to inspect merged Compose output instead of trusting YAML by feel.
Many Compose projects start clean and then sprawl. Development needs live reload, staging needs a preview domain, and production needs stable image tags, resource limits, and backup hooks. The wrong answer is usually three unrelated files copied from each other. The better answer is one shared base and thin override files for the differences.
Step 1: Understand how Compose file layering works
Docker Compose can merge multiple files in order. Later files override or extend earlier ones. That makes the base file the common truth and the environment-specific files the place for differences.
A healthy pattern looks like this:
compose.ymlfor shared services, volumes, networks, and sane defaultscompose.dev.ymlfor source mounts, debug ports, and local-only conveniencescompose.staging.ymlfor preview domains, test data paths, and near-production checkscompose.prod.ymlfor pinned images, restart policies, and production-only behavior
This lets you keep environments similar where they should be similar, while still respecting that development and production are not the same job.
Step 2: Create the base file and environment overrides
Start with a project layout like this:
myapp/
├── compose.yml
├── compose.dev.yml
├── compose.staging.yml
├── compose.prod.yml
├── env/
│ ├── dev.env
│ ├── staging.env
│ └── prod.env
└── data/Create the shared base in compose.yml:
services:
app:
image: myorg/myapp:${APP_TAG}
env_file:
- ${ENV_FILE}
ports:
- "${APP_PORT}:3000"
volumes:
- app-data:/app/data
restart: unless-stopped
db:
image: mariadb:11
env_file:
- ${ENV_FILE}
volumes:
- db-data:/var/lib/mysql
restart: unless-stopped
volumes:
app-data:
db-data:Create a development override in compose.dev.yml:
services:
app:
build:
context: .
image: myapp-dev
ports:
- "3000:3000"
volumes:
- ./:/app
command: npm run dev
restart: "no"Create a staging override in compose.staging.yml:
services:
app:
environment:
APP_ENV: staging
labels:
com.example.environment: stagingCreate a production override in compose.prod.yml:
services:
app:
environment:
APP_ENV: production
restart: always
db:
restart: alwaysCreate matching env files, for example env/dev.env:
APP_TAG=latest
ENV_FILE=./env/dev.env
APP_PORT=3000
MYSQL_DATABASE=myapp
MYSQL_USER=myapp
MYSQL_PASSWORD=devpassword
MYSQL_ROOT_PASSWORD=devrootpasswordFor staging and production, use stronger values and more careful secret handling than plain text examples.
Step 3: Run the right combination for each environment
Compose uses files in the order you pass them. Start development like this:
docker compose -f compose.yml -f compose.dev.yml --env-file env/dev.env up -dRender staging config without starting it yet:
docker compose -f compose.yml -f compose.staging.yml --env-file env/staging.env configStart staging:
docker compose -f compose.yml -f compose.staging.yml --env-file env/staging.env up -dRender production config before deploy:
docker compose -f compose.yml -f compose.prod.yml --env-file env/prod.env configThen start production:
docker compose -f compose.yml -f compose.prod.yml --env-file env/prod.env up -dIf you run these commands often, wrap them in documented shell aliases or Make targets so the team uses the same commands consistently.
Step 4: Verify the merged configuration before every important deploy
The most important command in this workflow is:
docker compose -f compose.yml -f compose.prod.yml --env-file env/prod.env configThis shows the fully merged configuration. It helps you catch problems like:
- A port unexpectedly exposed in production
- A development volume mount still present in staging
- A missing environment variable
- An override not applying because the service name does not match
Also inspect the running containers after startup:
docker compose -f compose.yml -f compose.prod.yml --env-file env/prod.env ps
docker compose -f compose.yml -f compose.prod.yml --env-file env/prod.env logs --tail=50This is where the pattern becomes operationally valuable. You can reason about what changed without maintaining three mostly duplicated files.
Rollback and recovery notes
Changing file layering is mostly a configuration risk, not a destructive data risk, unless you also changed volumes or database targets. Before production changes:
- Take a backup if the deploy also changes the database or app storage.
- Keep the previous env file and image tag available.
- Save the output of the last known-good
docker compose configduring important releases.
If a production override causes trouble, roll back by redeploying the last known-good image tag and env file combination:
docker compose -f compose.yml -f compose.prod.yml --env-file env/prod.env up -dThe key is that rollback only works well if your versions and environment files are explicit instead of floating invisibly.
Step 5: Confirm the structure is actually helping
By the end, you should have:
- One reusable base Compose file
- Thin environment-specific overrides
- Clear commands for dev, staging, and production
- A habit of inspecting merged config before major changes
That is a big improvement over maintaining multiple nearly identical Compose files by hand.
Troubleshooting common multi-file Compose mistakes
An override seems ignored.
Check the service name matches exactly and confirm the override file is passed after the base file.
Variables are blank in one environment.
Verify the correct --env-file path and remember that Compose variable substitution and container env_file behavior are related but different.
Development settings leaked into production.
Run docker compose config for production and inspect volume mounts, commands, and ports before redeploying.
I duplicated too much anyway.
Move only the differences into the override files. If half the file is repeated, your base probably needs more shared structure.
Staging and production still drift over time.
That usually means someone changed one env file or one manual command without documenting it. Standardize the commands and review the merged config regularly.
What to do next
Once your environments are cleaner, the next common pain point is file ownership and mount permissions. Continue with Fix Docker Volume and Bind Mount Permission Problems.
