14.2.1 Environment Config Strategy
A focused guide to Environment Config Strategy, connecting core concepts with practical Docker and container operations.
An environment configuration strategy for Docker defines a consistent, repeatable way of supplying the values that differ between development, staging, and production, so that the same image can be promoted unchanged across every stage while still behaving correctly in each one.
The core principle: one image, many environments
The foundation of a sound environment configuration strategy is that the container image built and tested in one environment is the exact same image deployed to the next, with nothing rebuilt in between. Everything that legitimately differs between environments, database endpoints, feature flags, external API credentials, log verbosity, is supplied at run time rather than baked into the image at build time.
docker build -t my-api:1.4.0 .
docker tag my-api:1.4.0 registry.example.com/my-api:1.4.0
docker push registry.example.com/my-api:1.4.0
The same tag, 1.4.0, is pulled and run in staging and then in production; only the configuration supplied alongside it changes.
Layered environment files
A common pattern is a base configuration file shared across all environments, with a per-environment override file supplying only the values that differ:
.env.base
.env.staging
.env.production
docker run --env-file .env.base --env-file .env.production my-api
Later --env-file flags override earlier ones for any key present in both, so the base file can hold sane shared defaults while each environment file overrides only what genuinely needs to differ.
Compose override files per environment
Docker Compose extends this layering to the full service definition, not just environment variables, through its override file mechanism:
docker compose -f docker-compose.yml -f docker-compose.staging.yml up -d
docker compose -f docker-compose.yml -f docker-compose.production.yml up -d
services:
api:
environment:
- LOG_LEVEL=debug
ports:
- "3000:3000"
services:
api:
environment:
- LOG_LEVEL=warn
deploy:
replicas: 3
The base docker-compose.yml defines the service shape once, and each environment-specific file supplies only its deltas, which keeps the bulk of the definition from drifting between environments.
Naming and discovery conventions
A consistent naming scheme for environment variables, prefixed or grouped logically, makes a growing configuration surface easier to audit:
APP_LOG_LEVEL
APP_DATABASE_URL
APP_FEATURE_NEW_CHECKOUT
Treating feature flags as a distinct, clearly named subset of configuration (rather than mixing them indistinguishably with infrastructure settings like database URLs) makes it easier to reason about which values are operational versus which are product decisions that a non-infrastructure team might also need to change.
Centralized configuration sources
As the number of services grows, distributing environment files by hand to every host becomes error-prone. Centralized configuration stores, such as a key-value store or a dedicated configuration service, let a single source of truth drive multiple containers and hosts consistently:
docker run -e CONFIG_SOURCE=consul://config-server:8500/myapp my-api
The application fetches its configuration from the centralized source at startup rather than relying on values baked into the invocation, which removes the need to keep many individual environment files in sync across hosts.
Validating configuration parity between environments
A frequent source of "works in staging, fails in production" incidents is configuration drift, where a value silently diverges between environments without anyone noticing. Comparing the effective configuration of running containers across environments is a useful periodic check:
docker exec staging-api env | sort > staging.env
docker exec production-api env | sort > production.env
diff staging.env production.env
A diff limited to genuinely expected differences (replica count, log level, endpoint hostnames) is a sign of a healthy strategy; an unexpected difference is often the first clue to an incident before it manifests as a user-facing failure.
Secrets within an environment configuration strategy
Secrets deserve a separate handling path from ordinary configuration values within the overall strategy, since they carry different rotation, access-control, and audit requirements:
services:
api:
env_file:
- production.env
secrets:
- db_password
Ordinary configuration can reasonably live in a file checked into a private configuration repository; secret values should not, and instead come from a secrets manager or an external store that the env file references by name rather than by value.
Configuration as part of the deployment pipeline
A mature environment configuration strategy treats configuration changes with the same rigor as code changes: reviewed, version-controlled, and deployed through the same pipeline rather than applied as an ad hoc manual edit on a running host:
git diff staging.env production.env
git commit -am "Increase production connection pool size"
deploy_production:
script:
- docker compose -f docker-compose.yml -f docker-compose.production.yml up -d
Common mistakes
- Building a separate image per environment instead of promoting one image with externally supplied configuration, breaking the guarantee that what was tested is what is deployed.
- Letting environment-specific configuration files drift out of version control, so the only record of what is actually running in production lives on the production host itself.
- Mixing secrets directly into the same files and review process as ordinary, non-sensitive configuration values.
- Allowing configuration changes to be applied manually on a running host without going through the same pipeline as a code deployment, leaving no audit trail for what changed and why.
- Assuming environments are configuration-identical except for what was deliberately changed, without periodically verifying that assumption against the actual running containers.
A durable environment configuration strategy keeps a single image moving unchanged through every stage, layers environment-specific values on top of shared defaults through an explicit and version-controlled mechanism, separates secrets from ordinary settings, and treats configuration changes as deployment events rather than informal edits.