Skip to main content

Command Palette

Search for a command to run...

Multi-Env Setups (dev / test / staging)

Designing dev, test, and staging as one system

Updated
4 min read
Multi-Env Setups (dev / test / staging)
N
Senior-level Fullstack Web Developer with 10+ years experience, including 2 years of Team Lead position. Specializing in responsive design and full-stack web development across the Vue.js and .NET ecosystems. Skilled in Azure/AWS cloud infrastructure, focused on DevOps techniques such as CI/CD. Experienced in system design, especially with software architecture patterns such as microservices, BFF (backend-for-frontend). Hands-on with Agile practices in team leading, and AI-assisted coding.

Series: Containers, Actually: Building Real Local Dev Environments

Multi-environment setups sound straightforward on paper:

  • dev for development

  • test for verification

  • staging for pre-production

In practice, they are one of the fastest ways to create confusion, drift, and subtle bugs—unless the environments are designed as variations of the same system, not separate inventions.

This article shows how to extend a Docker-based workflow into multiple environments without copying files, logic, or mistakes.


The Core Principle: One System, Multiple Contexts

The most important rule:

You do not have three systems.
You have one system, running under three contexts.

If dev, test, and staging are defined separately, they will diverge.
If they are derived, they stay aligned.

Docker Compose is particularly good at expressing this relationship.


What Actually Changes Between Environments

Before touching config, clarify what should differ.

Usually:

  • Environment variables

  • Data persistence

  • External integrations

  • Debug tooling

  • Resource limits

Usually not:

  • Service topology

  • Runtime versions

  • Inter-service communication

  • Build logic

If topology differs, bugs become non-reproducible.


Folder & File Structure for Multi-Env Compose

A proven, low-friction structure:

docker-compose.yml
docker-compose.dev.yml
docker-compose.test.yml
docker-compose.staging.yml

.env.dev
.env.test
.env.staging
  • docker-compose.ymlbase system

  • environment-specific files → behavioral overrides

This structure scales cleanly without duplication.


Base Compose File: The System Contract

docker-compose.yml defines what exists.

version: "3.9"

services:
  app:
    build: ./docker/php
    environment:
      DB_HOST: mysql
      REDIS_HOST: redis
    depends_on:
      - mysql
      - redis

  mysql:
    image: mysql:8
    environment:
      MYSQL_DATABASE: app
      MYSQL_USER: app
      MYSQL_PASSWORD: password
    volumes:
      - mysql-data:/var/lib/mysql

  redis:
    image: redis:alpine

volumes:
  mysql-data:

This file should:

  • Be boring

  • Be stable

  • Change rarely

It’s the shape of the system.


Development Environment (dev)

Development optimizes for:

  • Speed

  • Visibility

  • Convenience

docker-compose.dev.yml

services:
  app:
    volumes:
      - ./app:/var/www/html
    environment:
      APP_ENV: dev
      APP_DEBUG: "true"

  mysql:
    ports:
      - "3306:3306"

.env.dev

APP_ENV=dev
DB_PASSWORD=dev-secret

Key traits:

  • Bind mounts enabled

  • Debug flags on

  • Ports exposed

  • Data persists


Test Environment (test)

Test environments optimize for:

  • Clean state

  • Determinism

  • Repeatability

docker-compose.test.yml

services:
  app:
    environment:
      APP_ENV: test
      APP_DEBUG: "false"

  mysql:
    volumes: []  # disable persistence

.env.test

APP_ENV=test
DB_PASSWORD=test-secret

Key traits:

  • No persistent volumes

  • No exposed ports

  • Clean startup every run

  • Ideal for CI and automated tests

This is where bugs surface honestly.


Staging Environment (staging)

Staging optimizes for:

  • Production similarity

  • Safety

  • Observation

docker-compose.staging.yml

services:
  app:
    environment:
      APP_ENV: staging
      APP_DEBUG: "false"

  mysql:
    volumes:
      - mysql-staging-data:/var/lib/mysql

volumes:
  mysql-staging-data:

.env.staging

APP_ENV=staging
DB_PASSWORD=staging-secret

Key traits:

  • Persistent data

  • No dev tooling

  • Same versions as prod

  • No bind mounts (often)


Running Each Environment

Development

docker-compose \
  -f docker-compose.yml \
  -f docker-compose.dev.yml \
  --env-file .env.dev \
  up -d

Test

docker-compose \
  -f docker-compose.yml \
  -f docker-compose.test.yml \
  --env-file .env.test \
  up --abort-on-container-exit

Staging

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

Same system.
Different behavior.


Environment-Specific Volumes (Critical Detail)

Never reuse volumes across environments.

Bad:

mysql-data

Good:

mysql-data
mysql-test-data
mysql-staging-data

Volumes are state, and state must not bleed.


Avoiding the “Env Explosion” Trap

Common anti-patterns:

  • Copying entire compose files

  • Hardcoding environment logic in code

  • Creating docker-compose-prod.yml locally

Instead:

  • Keep overrides minimal

  • Push environment branching upward (compose, env vars)

  • Keep application logic environment-agnostic

If the app behaves wildly differently per env, the problem is in the app—not Docker.


Multi-Env vs Branch-Based Environments

A key insight:

Branches are for code.
Environments are for behavior.

Avoid:

  • “feature/dev”, “feature/staging” environments

  • Branch-specific Docker configs

Prefer:

  • Same Docker structure

  • Different .env and overrides

This keeps mental overhead manageable.


How This Scales to CI/CD

This pattern integrates cleanly with CI:

docker-compose \
  -f docker-compose.yml \
  -f docker-compose.test.yml \
  --env-file .env.test \
  up

The test environment becomes:

  • Predictable

  • Disposable

  • Reproducible locally

That’s parity without pain.


What Multi-Env Setups Are Not

They are not:

  • Separate architectures

  • Separate repos

  • Separate Dockerfiles

  • Separate mental models

They are controlled variations of one system.


The Real Lesson

Multi-env setups fail when environments are treated as destinations.

They succeed when environments are treated as lenses—different ways of observing the same system.

Docker Compose makes this possible because it encourages:

  • Composition over duplication

  • Declaration over scripting

  • Structure over convenience


Final Takeaway

If dev, test, and staging behave differently, something is wrong.
If they behave the same under different constraints, you’ve done it right.

Multi-env setups aren’t about multiplying environments.
They’re about reducing surprises—before production does it for you.

Containers, Actually: Building Real Local Dev Environments

Part 3 of 18

This series explores full-stack local development with Docker—from core concepts and best practices to a real Windows implementation. The goal is to understand how things run, why they work, and how to build reproducible production environment.

Up next

CI/CD Parity with docker-compose

Making local and CI environments tell the same story