Skip to content
Knowledge Hub

12-factor and build once, deploy many: config lives in the environment Intermediate

Estimated reading time: 3 min read

This page answers: what 12-factor actually says about config, and why the same image must go from staging to production.

12-factor has 12 principles; three of them are directly relevant:

Factor Original wording Practical consequence
III. Config “Store config in the environment” Config is read from env vars, not stored in code or the image
V. Build, release, run “Strictly separate build and run stages” Build produces one artifact; a release = the build combined with that deploy’s config
X. Dev/prod parity “Keep development, staging, and production as similar as possible” Same kind of backing services, same way of loading config in every environment

The original definition: config is “everything that is likely to vary between deploys”.

The 12-factor litmus test: could the codebase be open sourced at any moment without leaking credentials?

  • 12-factor does not treat internal config (routes, DI wiring) as config. Anything that does not change per deploy stays in code.
  • 12-factor recommends env vars as “granular controls”, not grouped into “environments” such as config.production.json.

Code like if (NODE_ENV === 'production') timeout = 5000 is grouping in disguise. Bad/good examples are in Common mistakes.

The app writes logs to stdout (“writes its event stream, unbuffered, to stdout”) and lets the environment route them.

Consequence: the log file path is not config the app needs to know. There is no need for LOG_FILE_PATH.

The same image for staging and production; with missing config the new pod never becomes ready
The same image for staging and production; with missing config the new pod never becomes readyStep 1/5: CI builds exactly once per commit and pushes an image tagged with the git SHA. The image contains no URLs, passwords, or environment names.CI buildruns once per commitImage sha-3f9c2e1immutable, has a digestPod stagingstaging configConfigMap + Secretproduction configPod v2 productionsame imagePod v1 productioncurrently runningservingpush image
CI build → Image sha-3f9c2e1push image
Step 1/5

CI builds exactly once per commit and pushes an image tagged with the git SHA. The image contains no URLs, passwords, or environment names.

The consequence of factor V: the image does not know where it is running. No npm run build:prod, no .env.production baked into the image.

Reason Explanation
Test exactly what will run Staging tests the exact bytes that go to production. Rebuilding for production means running an artifact that was never tested.
Cheap rollback Rollback means running the old digest with the current config, no build needed.
No secrets in the image If the image contains a password, anyone who can pull the image can read the password.

The Docker docs say it plainly: build arguments and env vars “are inappropriate for passing secrets to your build” because “they persist in the final image”. If you need a secret at build time (e.g. a private npm token), use a secret mount.

A common exception: frontend build-time variables (the bundler substitutes values into the JS at build time). For backends there is no good reason to do this.

# staging and production use the SAME digest, only the ConfigMap/Secret differ
image: registry.example.com/payment-api@sha256:...
  • A tag (:latest, :1.4.2) can be overwritten; a digest cannot.
  • Pipeline promotion = changing the target environment, not rerunning docker build.

Next: Parse and validate env.