How do you handle configuration differences across environments with the same image?
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 gets promoted from staging to production, instead of subtly different images per environment that could behave differently for reasons unrelated to configuration.
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 config value, nothing guarantees the build process 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. That 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, runs in every environment. Only the runtime configuration — environment variables, mounted config files, secrets — differs. This is the "build once, deploy many times, unchanged" principle. 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.
The twelve-factor app's "config" principle
This directly reflects Factor III — Config of the twelve-factor app methodology, which calls for a strict separation between an application's code (identical across environments) and its configuration (which legitimately varies by environment). Configuration belongs in the environment — environment variables or mounted files — never hardcoded into the build artifact.
# 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 and Compose's environment/env_file mechanisms exist as first-class concepts. They're the standard, orchestrator-level tools for injecting environment-specific configuration into an unchanged, promoted image, instead of 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 — the kind caused by staging and production actually running subtly different artifacts, rather than the same one with different configuration.