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.

Docker ComposeEnvironmentsDeployment workflow
What you learn

How to split a Compose setup into a shared base plus development, staging, and production overrides without duplicating whole files.

Best for

Teams and solo operators who want local convenience, safer previews, and more production discipline from the same app definition.

Risk to watch

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.

Expected outcome: You will end up with one core service definition, less duplication, and safer deploy commands for each environment.

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.yml for shared services, volumes, networks, and sane defaults
  • compose.dev.yml for source mounts, debug ports, and local-only conveniences
  • compose.staging.yml for preview domains, test data paths, and near-production checks
  • compose.prod.yml for 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: staging

Create a production override in compose.prod.yml:

services:
  app:
    environment:
      APP_ENV: production
    restart: always
  db:
    restart: always

Create 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=devrootpassword

For 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 -d

Render staging config without starting it yet:

docker compose -f compose.yml -f compose.staging.yml --env-file env/staging.env config

Start staging:

docker compose -f compose.yml -f compose.staging.yml --env-file env/staging.env up -d

Render production config before deploy:

docker compose -f compose.yml -f compose.prod.yml --env-file env/prod.env config

Then start production:

docker compose -f compose.yml -f compose.prod.yml --env-file env/prod.env up -d

If 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 config

This 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=50

This 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 config during 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 -d

The 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.