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

KindExampleWhere it belongsIn version control?
CodeApplication logicThe repositoryYes
ConfigFeature flags, timeouts, log levelDefaults in the repository, overridden per environmentYes, except values that differ per environment
SecretDatabase password, API key, signing keyA secret manager, injected at runtimeNever
Derived secretSession key from a master secretDerived at start-upOnly the master secret is stored
Local developer valuesA throwaway database URLA local file, git-ignoredNever
# .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 .env copied in with COPY . . 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-config

Read 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 pathHow it happensPrevention
Build logsA script echoes the variableNever print environment values; mask them in CI
Error pagesA stack trace includes a connection stringSanitise errors; never return raw exceptions
Docker image layersA secret copied in and deleted in a later layerUse build secrets, never COPY a secret
Docker build argumentsBuild args are visible in image historyUse a mounted build secret
Front-end bundleA server-side variable imported into client codePrefix discipline and a bundle review
Crash dumps and logsFull request headers loggedRedact Authorization, Cookie and query tokens
A fork or a mirrorA secret committed once is in the history foreverRotate immediately; do not just delete the commit
  1. Treat a secret that has ever been in a repository as compromised. Rotating is the only fix; rewriting history does not remove copies.
  2. 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.
  3. Design for two live secrets during rotation: issue the new one, deploy it, confirm, then revoke the old one.
  4. Audit what each environment can reach. A staging environment with production database credentials is one compromised laptop away from a real incident.
  5. 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.

Build and deploy workflows Security hardening, cost control and migrations

Last refreshed 2026-09-18.