Twelve-Factor app configuration: what belongs in the environment?
Keep one build while changing deployment settings. Try missing values and a preview pointing at production, then review secret delivery and rotation.
Keep values that change between deployments, such as service addresses, credentials and hostnames, outside the code. Supply them independently for each deployment, validate them at startup, and keep secrets out of source control.
The same recipe can be cooked in several kitchens. The recipe is the code; each kitchen's address, oven setting and cupboard key are deployment settings. Writing a key into the recipe means everyone who gets a copy of it also gets the key.
A preview deployment and production can run the same build while talking to different databases, using different hostnames and holding different credentials. If changing any of those values means editing source and rebuilding, the boundary between code and deployment has been lost. The Twelve-Factor config principle gives that boundary a practical test: ask whether a value is likely to vary between deployments.
The boundary between code and config#
A database address, an external service credential and a deployment’s canonical hostname pass the test. The application’s route definitions and how its modules are connected usually do not: they describe the program itself. The criterion is variation between deployments, not whether a value happens to be spelled as a constant. A fixed retry algorithm does not become deploy config because a developer could put its number in an environment variable.
The original principle calls for individual environment variables. Keeping deploy values outside the repository means one build can move between environments, and the code can be published without publishing credentials. That last statement is a useful audit question, not a guarantee that every environment variable is secure. A value can still leak through logs, process inspection, diagnostics or overly broad access to the deployment platform.
| Situation | Take | Because |
|---|---|---|
| Database endpoint for this deployment | Deploy config | The endpoint changes without a code change |
| Payment provider credential | Secret | It varies by deployment and needs controlled access |
| HTTP route /orders → order handler | Code | It is part of application behaviour everywhere |
| Canonical hostname for this deployment | Deploy config | Preview and production use different hosts |
- Situation
- Database endpoint for this deployment
- Take
- Deploy config
- Because
- The endpoint changes without a code change
- Situation
- Payment provider credential
- Take
- Secret
- Because
- It varies by deployment and needs controlled access
- Situation
- HTTP route /orders → order handler
- Take
- Code
- Because
- It is part of application behaviour everywhere
- Situation
- Canonical hostname for this deployment
- Take
- Deploy config
- Because
- Preview and production use different hosts
One build, three deployments#
In this illustrative example, the same build runs on a developer machine, a preview URL and production. The executable is identical. The three sets of DATABASE_URL and PUBLIC_BASE_URL values differ; each deployment gets an account-appropriate payment credential, such as a test credential outside production. In the Twelve-Factor build, release and run model, combining a build with a deployment’s configuration creates a release. Changing that configuration creates another release, even when the build stays the same.
Supply each set through the platform and check required values at startup so a missing value fails visibly before the first request. For numeric or structured settings, parse and validate their types and allowed ranges too; the short Python example below checks only presence. A supplied value can still be wrong: a default that silently points a preview at the production database is present, but it defeats the intended separation. Ephemeral environments and seed data explains how to make that preview environment useful without copying production data into it.
Change the configuration without rebuilding
Illustrative example: keep build-42 and change the values supplied to a new process. If a preview receives the production database address, will checking for missing values catch it?
Build build-42 · Intended deployment: preview
These buttons fill independent inputs for convenience. The application does not infer them from an APP_ENV switch. Change either address separately below.
- Required-value check
- Waiting for Check startup.
- Illustrative target policy
- Waiting for Check startup.
The initial preview configuration supplies all three required values and would pass the presence check. Both addresses target preview services. No process has been started.
Inspect supplied values
- DATABASE_URL
- postgresql://preview-db.example.test/orders
- PUBLIC_BASE_URL
- https://preview.example.test
- PAYMENTS_API_TOKEN
- Marked supplied; no credential value stored or verified
Without DATABASE_URL, the presence check would stop startup. A supplied production database address would pass that check, but would violate this example's preview target policy. The controls need JavaScript.
All addresses are invented example.test values. The model checks presence and a separate policy that both addresses match the intended deployment. It does not connect, start a process, inspect a credential, verify permissions or cover every valid deployment design. Editing these inputs does not refresh a running process's environment.
The Python fragment below checks only missing or empty values. The workbench’s target comparison is an additional policy for this illustrative application; Twelve-Factor does not prescribe that particular check.
Do not place the example’s real values in a checked-in .env file. A local ignored file may help development, but ignoring a filename is a safeguard against one form of accidental commit, not credential storage or access control. Keep a committed example with names and harmless placeholders if the team needs to document the contract.
Avoid named environment bundles#
A common shortcut is APP_ENV=production, followed by code that selects a whole block of URLs, limits and credentials. It works until a preview deployment needs a production-like rate limit, a different database and a test payment account. Then the team invents preview-prod-ish, and each new combination becomes another branch. The Twelve-Factor guidance calls for orthogonal values: set the database URL, hostname and payment endpoint independently for each deployment. Names such as “staging” can still label a deployment in the platform; they should not be the program’s only source of all its configuration.
Independent inputs still need a contract. Document allowed values, which ones are required, and which combinations are invalid. Validate those combinations at startup. Orthogonal does not mean pretending that a payment credential and payment endpoint can be paired at random; it means making each value explicit rather than hiding the pair behind a growing list of environment names.
Secrets and rotation#
Environment variables are a delivery mechanism, not a vault. Give the deployment platform the least access it needs to fetch a secret, keep values out of build output and logs, and plan how a rotated credential reaches running processes. Some platforms can project a secret into a mounted file or fetch it through an identity-based API instead. The application can still treat the resulting value as deploy config without insisting that every secret literally arrive as an environment variable.
Rotation behaviour depends on the delivery method. For example, Kubernetes Secret volumes can receive updated data with a propagation delay; a subPath mount does not receive those automatic updates. An updated file also does not replace a value the application has already cached. For Secret-backed environment variables, an existing container does not see a changed Secret until it restarts. Decide whether the application rereads a file, fetches on demand or needs a restart, and test that path before a credential expires.
When the rule is not enough#
A dynamically changing feature flag is a different lifecycle from a database URL read at process start. It may need a flag service, audit trail and safe defaults during an outage. Large structured policy or certificate bundles may be better supplied as versioned files or a dedicated service. The useful invariant is that deployment-specific values are separate from application code and can be changed with controlled access; the right transport depends on size, refresh needs and threat model.
Record choices that have operational consequences in an architecture decision record: where values live, who can change them, which values may be logged, how a bad configuration is detected, and how a rotation or rollback works. The twelve-factor rule tells you where the boundary goes. The decision record tells the next operator how the boundary is maintained.
IF YOU REMEMBER ONE THING
If a value changes because the deployment changes, treat it as deployment configuration. Make its source, validation and rotation explicit; do not let a single environment name hide the real values.
Questions people also ask
3 QUESTIONSDoes every application setting belong in an environment variable?
No. The Twelve-Factor definition covers values likely to vary between deployments. Internal routing rules and the way modules are wired together belong with the code when they are the same everywhere.
Are environment variables a secret manager?
No. They are one way to deliver values to a process. A secret manager, access control, rotation procedure and careful logging still matter for credentials.
Why avoid a single APP_ENV switch?
It encourages the program to infer many settings from one named bundle. A new preview deployment then needs another special case. Independent values let each deployment declare exactly what it needs.