Environment variables, secrets and configuration
Per-environment configuration, keeping secrets out of the repository, runtime injection, rotation, and the build-log leaks that expose values.
Configuration versus secrets
| Kind | Example | Where it belongs | In version control? |
|---|---|---|---|
| Code | Application logic | The repository | Yes |
| Config | Feature flags, timeouts, log level | Defaults in the repository, overridden per environment | Yes, except values that differ per environment |
| Secret | Database password, API key, signing key | A secret manager, injected at runtime | Never |
| Derived secret | Session key from a master secret | Derived at start-up | Only the master secret is stored |
| Local developer values | A throwaway database URL | A local file, git-ignored | Never |
# .env.example - committed, contains names and placeholders only
DATABASE_URL=postgres://user:password@localhost:5432/app
REDIS_URL=redis://localhost:6379
SESSION_SECRET=change-me
LOG_LEVEL=debug
# .gitignore
.env
.env.*
!.env.example
*.pem
*.key- A committed example file with placeholder values documents the configuration without leaking anything. It is the single highest-value file in this whole area.
- Anything in the build context is in the image. A
.envcopied in withCOPY . .stays in every layer forever, even if a later layer deletes it. - Client-side configuration is public. Anything prefixed for the browser is readable by any visitor, so never put a secret in it.
Injecting at runtime
Preference order
1. platform secret store, injected as environment variables typical PaaS
2. mounted file from a secret volume, read once at start-up containers
3. a secrets manager called at start-up with the platform identity
4. environment variables set by the orchestrator from a secret Kubernetes
...
last resort: a file on the host with 0600 permissions, owned by the service user
never: a secret in the image, in the repository, or in a build argument# Kubernetes: a secret referenced by the pod, never read from a literal in the manifest
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
image: registry.example/app:1.4.0
envFrom:
- secretRef:
name: app-secrets
- configMapRef:
name: app-configRead configuration once at start-up and fail loudly if a required value is missing. An application that starts with a default empty password and then fails on the first request is far harder to diagnose than one that refuses to boot.
const required = ["DATABASE_URL", "SESSION_SECRET"];
const missing = required.filter((k) => !process.env[k]);
if (missing.length) {
console.error("Missing configuration: " + missing.join(", "));
process.exit(1); // fail at boot, not on the first request
}
export const config = Object.freeze({
databaseUrl: process.env.DATABASE_URL,
sessionSecret: process.env.SESSION_SECRET,
logLevel: process.env.LOG_LEVEL ?? "info"
});Leaks and rotation
| Leak path | How it happens | Prevention |
|---|---|---|
| Build logs | A script echoes the variable | Never print environment values; mask them in CI |
| Error pages | A stack trace includes a connection string | Sanitise errors; never return raw exceptions |
| Docker image layers | A secret copied in and deleted in a later layer | Use build secrets, never COPY a secret |
| Docker build arguments | Build args are visible in image history | Use a mounted build secret |
| Front-end bundle | A server-side variable imported into client code | Prefix discipline and a bundle review |
| Crash dumps and logs | Full request headers logged | Redact Authorization, Cookie and query tokens |
| A fork or a mirror | A secret committed once is in the history forever | Rotate immediately; do not just delete the commit |
- Treat a secret that has ever been in a repository as compromised. Rotating is the only fix; rewriting history does not remove copies.
- Rotate on a schedule and on every departure of a person who had access. Rotation that has never been exercised will not work when you need it.
- Design for two live secrets during rotation: issue the new one, deploy it, confirm, then revoke the old one.
- Audit what each environment can reach. A staging environment with production database credentials is one compromised laptop away from a real incident.
- Keep an inventory of which service needs which secret. Rotation is impossible without it.
⚠️
If a secret lands in a build log, rotating it is the fix - not deleting the log. Build logs are copied into caches, artefacts and third-party dashboards, and hosted build systems keep them far longer than you expect.
FAQ
Are environment variables safe for secrets?
They are visible to any process running as the same user and often appear in crash dumps and debug output. For high-value secrets, a file mounted from a secret manager, or an API fetch at start-up with a short-lived identity, is stronger.
How do I give a developer local configuration?
Ship a committed
.env.example and a documented start-up command. A developer with placeholder values and a local database should be able to run the application in minutes.Related
Build and deploy workflows Security hardening, cost control and migrations
Last refreshed 2026-09-18.