How do you handle configuration differences across environments with the same image?

7 minadvancedenvironment-configurationtwelve-factor-appbuild-once-deploy-many

Quick Answer

Build the image once, and inject environment-specific configuration at runtime — via environment variables, mounted config files, or secrets — rather than baking environment-specific values into the image or building a separate image per environment. This follows the twelve-factor app principle of strict separation between build and run stages, ensuring the exact same, already-tested artifact is what actually gets promoted from staging to production, rather than subtly different images per environment that could behave differently for reasons unrelated to the environment configuration itself.

Detailed Answer

The anti-pattern: building a separate image per environment

# BAD: separate builds per environment, baking in environment-specific config
docker build -t myapp:staging --build-arg API_URL=https://staging-api.example.com .
docker build -t myapp:production --build-arg API_URL=https://api.example.com .

This means myapp:staging and myapp:production are, strictly speaking, different artifacts. Even if the only intended difference is a configuration value, nothing guarantees the build process itself produced byte-for-byte identical images apart from that one value. A subtle build-time issue (a flaky dependency resolution, a build tool behaving slightly differently) could introduce an unintended difference between what was tested in staging and what actually ships to production. This directly undermines the confidence that "what we tested is exactly what we're deploying."

The correct pattern: build once, configure at runtime

docker build -t myapp:1.0 .          # ONE build, used everywhere

docker run -e API_URL=https://staging-api.example.com myapp:1.0        # staging
docker run -e API_URL=https://api.example.com myapp:1.0                 # production

The exact same image, byte-for-byte, is what runs in every environment. Only the runtime configuration (environment variables, mounted config files, secrets) differs. This is precisely the "build once, deploy many times, unchanged" principle covered throughout this stack (see the fundamentals topic and the tags/digests question). It means that if something works correctly in staging, you have real, direct confidence that the identical artifact will behave the same way in production, since nothing about the image itself changed between the two.

The twelve-factor app's "config" principle

This directly reflects Factor III (Config) of the twelve-factor app methodology. It calls for strict separation between an application's code (which should be identical across environments) and its configuration (which legitimately varies by environment). Configuration belongs in the environment (environment variables, mounted files), never hardcoded into the build artifact itself.

# Kubernetes ConfigMaps/Secrets (see that stack), or Compose environment/.env
# files, or a cloud platform's own environment-variable configuration --
# all apply this same principle at whatever layer is actually deploying the container

This principle is exactly why Kubernetes ConfigMaps/Secrets (see that stack) and Compose's environment/env_file mechanisms (see that topic) both exist as first-class concepts. They are the standard, orchestrator-level tools for injecting environment-specific configuration into an unchanged, promoted image, rather than requiring separate builds.

What this means for CI/CD pipeline design

1. Build the image ONCE, from a specific commit, tagged with that commit's SHA
2. Run tests against THAT SAME image
3. Push it to a registry
4. Deploy that SAME image (by digest, ideally -- see that question) to staging,
   with staging-specific configuration injected at deploy time
5. After validation, promote the SAME image (same digest) to production,
   with production-specific configuration injected at deploy time

This "build once, promote the same artifact through environments" pattern is a core CI/CD design principle. It eliminates an entire class of "it worked in staging but broke in production" bugs. Those bugs stem from staging and production having actually run subtly different artifacts, rather than the same one with different configuration.