✦ For everyone, free.

Practical knowledge for real and everyday life

Home

17.2.2 Config Externalization

A focused guide to Config Externalization, connecting core concepts with practical Docker and container operations.

Config externalization, as a Docker best practice, is the discipline of ensuring absolutely no environment-specific value, a hostname, a feature flag, a credential, a timeout, exists hardcoded anywhere within an image's own built content, verified actively rather than assumed, since the entire benefit of building one image and promoting it unchanged through every environment depends on this being completely, not just mostly, true.

The practical test: would this image work anywhere?

A genuinely externalized image should behave correctly when supplied with appropriate configuration for any environment, development, staging, production, with no rebuild required between them; if achieving this requires even a single conditional branch based on an environment name baked into the image at build time, externalization is incomplete:

if (process.env.NODE_ENV === 'production') {
  dbHost = 'prod-db.internal';
} else {
  dbHost = 'dev-db.internal';
}
const dbHost = process.env.DB_HOST;

The first example bakes environment-specific hostnames directly into the application's own logic, which means the image's behavior is permanently tied to whatever environment names were known at build time; the second genuinely externalizes the value, taking it entirely from runtime configuration with no environment-specific knowledge embedded in the code at all.

Auditing for hardcoded values as an active practice

Rather than assuming config externalization has been achieved, actively searching the codebase for patterns suggesting a hardcoded, environment-specific value catches violations before they become a problem:

grep -rn "\.internal\|\.prod\.\|localhost:[0-9]" src/ --include="*.js"
grep -rn "if.*NODE_ENV\|if.*ENVIRONMENT" src/

Running this kind of audit periodically, or as an automated check within the CI pipeline itself, surfaces exactly the kind of accidental, easily overlooked hardcoding that erodes the one-image-many-environments guarantee config externalization is meant to provide.

Distinguishing configuration from secrets explicitly

Not every externalized value carries the same handling requirements; ordinary configuration, a log level, a feature flag, a non-sensitive endpoint URL, can reasonably live in a plain environment variable or configuration file, while secrets require the more careful handling, mounted files or a dedicated secrets manager, covered separately:

environment:
  - LOG_LEVEL=info
secrets:
  - db_password

Treating every externalized value identically, applying secrets-level handling to ordinary configuration or, more dangerously, applying ordinary configuration handling to genuine secrets, either over-engineers the simple case or under-protects the sensitive one; explicit, deliberate categorization avoids both mistakes.

Startup-time validation as the verification mechanism

An application that validates its required configuration immediately at startup, failing fast and clearly if something required is missing, provides a direct, automatic check that externalization was actually configured correctly for the current environment, rather than discovering a gap through a confusing runtime failure deep into the application's actual operation:

const required = ['DATABASE_URL', 'JWT_SECRET', 'LOG_LEVEL'];
const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
  console.error(`Missing required configuration: ${missing.join(', ')}`);
  process.exit(1);
}

This startup-time check is the practical, automated complement to the discipline of never hardcoding environment-specific values: it confirms, on every single container start, that whatever externalized configuration mechanism is in use actually supplied everything the application genuinely needs.

One image, validated across every environment

The concrete, verifiable proof that config externalization has actually been achieved is successfully running the exact same image, by digest, across every environment with only the externally supplied configuration differing:

docker run --env-file dev.env my-api@sha256:3f29a8c1...
docker run --env-file staging.env my-api@sha256:3f29a8c1...
docker run --env-file production.env my-api@sha256:3f29a8c1...

If this works correctly across every environment with no rebuild, no image modification, and no environment-specific branching anywhere in the application's own code, externalization has genuinely been achieved; any environment requiring a different image, or a code change, to function correctly indicates a gap somewhere in the externalization discipline.

Avoiding config logic creep within the application

Over time, it is easy for small, seemingly reasonable environment-aware branches to creep into application code, each individually minor but collectively eroding the externalization guarantee; periodically reviewing and removing this kind of accumulated, environment-specific logic keeps the discipline genuinely intact rather than slowly degrading unnoticed:

const timeout = process.env.NODE_ENV === 'production' ? 30000 : 5000;
const timeout = parseInt(process.env.REQUEST_TIMEOUT_MS, 10) || 5000;

The corrected version externalizes the actual timeout value itself, with a sensible default, rather than branching on an environment name to select between two hardcoded values, which is the more genuinely externalized pattern even for a value that might reasonably differ between environments.

Common mistakes

  • Branching application logic based on a hardcoded environment name rather than externalizing the actual value that differs between environments.
  • Assuming config externalization has been achieved without actively auditing the codebase for hardcoded, environment-specific values.
  • Treating every externalized value with identical handling, either over-engineering ordinary configuration or under-protecting genuine secrets.
  • Not validating required configuration at application startup, discovering a missing externalization gap only through a confusing runtime failure later.
  • Allowing small, individually reasonable environment-aware code branches to accumulate over time, gradually eroding the one-image-many-environments guarantee.

Config externalization as a Docker best practice is verified, not assumed, through active auditing for hardcoded values, startup-time configuration validation, and the concrete, practical test of successfully running one unmodified image, by digest, across every environment with nothing but externally supplied configuration actually differing between them.

Content in this section