12-Factor Config Across Runtimes

Khimananda Oli 8 min read Programming and Languages
12-Factor Config Across Runtimes

By Khimananda Oli | Last reviewed: August 2026

Hardcoded credentials and environment-specific logic buried in source code remain the primary cause of deployment failures and security breaches I encounter during audits. True portability requires strict adherence to 12-Factor Config Across Runtimes, where configuration is strictly separated from code and injected dynamically at startup. This guide provides the concrete implementation patterns you need to enforce this separation consistently across Kubernetes, Docker, and traditional Linux servers without breaking existing workflows.

External StoreVault / SSM / GitRuntime InjectorK8s / Docker / SystemdApp ProcessReads ENV Only12-Factor Config FlowSame Artifact + Different Config = Different Environment
Core architecture of 12-Factor Config Across Runtimes: externalized values are injected by the platform, keeping the application artifact immutable.

How do you implement 12-Factor Config Across Runtimes correctly?

The third factor of the Twelve-Factor App methodology states that configuration storing environment-specific details must be strictly separated from code. In practice, this means your application should not know whether it is running in a Kathmandu data center or an AWS us-east-1 region. The binary or container image remains identical; only the injected configuration changes. Achieving true 12-Factor Config Across Runtimes requires moving beyond simple .env files in development and establishing a rigorous injection contract for production.

A common mistake I see in Nepal-based startups and global enterprises alike is treating configuration as a file management problem rather than an interface problem. Your application defines an interface (required environment variables), and the runtime fulfills that contract. If the runtime fails to provide a required variable, the application must crash immediately at startup, not fail silently hours later. This "fail-fast" behavior is non-negotiable for reliable operations.

Defining the Configuration Contract

Before touching Kubernetes manifests or systemd units, define exactly what your application needs. Document every environment variable, its type, default value, and whether it is required. This specification serves as the binding agreement between developers and operators.

# Example: Configuration Contract for Payment Service
# PAYMENT_GATEWAY_URL (string, required) - Base URL for payment processor
# PAYMENT_API_KEY (secret, required) - Auth token, never log this
# DB_POOL_SIZE (integer, optional, default: 10) - Max database connections
# LOG_LEVEL (enum: debug|info|warn|error, optional, default: info)

Validate this contract at boot. Libraries like python-decouple, node-env-var, or Go's viper can enforce types and presence checks before the first request is served. For deeper validation strategies, refer to Kubernetes secrets management done right which covers safe handling of sensitive inputs.

How does Kubernetes handle 12-Factor configuration injection?

Kubernetes provides native primitives for 12-Factor Config Across Runtimes through ConfigMaps and Secrets. These objects decouple configuration data from pod specifications, allowing you to update settings without rebuilding images. However, the mechanism of injection matters significantly for security and observability.

Environment Variables vs. Volume Mounts

Pure 12-Factor advocates prefer environment variables because they are universally supported and language-agnostic. However, environments have limitations: they cannot hold large structured data, they are visible in process listings, and updating them requires a pod restart. Volume mounts solve the size and update problems but violate strict 12-Factor purity since the app reads files instead of ENV.

CriterionEnvironment VariablesVolume Mounts
12-Factor PurityHigh (Strict adherence)Medium (File-based override)
Hot ReloadNo (Requires restart)Yes (SubPath excepted)
Secret SafetyLow (Visible in /proc)Higher (File permissions)
Structured DataPoor (Flat key-value only)Excellent (JSON/YAML files)
Size Limit~128KB total env blockMBs to GBs

In my experience managing SOC 2 compliant infrastructure, the pragmatic approach is hybrid: use environment variables for simple scalars and connection strings, but mount complex configurations or high-sensitivity secrets as files with restricted permissions. Always enable encryption at rest for etcd if storing Secrets in Kubernetes.

Injecting Values Safely

Use the envFrom field to inject entire ConfigMaps cleanly, avoiding verbose individual env entries. For secrets, reference them explicitly to maintain audit trails.

apiVersion: v1
kind: Pod
metadata:
  name: payment-service
spec:
  containers:
    - name: app
      image: registry.example.com/payment-svc:v2.4.1
      envFrom:
        - configMapRef:
            name: payment-config
      env:
        - name: PAYMENT_API_KEY
          valueFrom:
            secretKeyRef:
              name: payment-secrets
              key: api-key

This pattern ensures that non-sensitive defaults live in version-controlled ConfigMaps while credentials remain in managed Secrets. For teams using GitOps, tools like ArgoCD can sync these resources declaratively, as detailed in setting up GitOps with ArgoCD.

Environment Variable PathConfigMapSecretKubelet ENV InjectionContainer ProcessVolume Mount PathConfigMapSecrettmpfs / Projected VolContainer File Read
Injection mechanisms compared: environment variables offer strict 12-Factor compliance while volume mounts enable hot reloads and better secret hygiene.

How do you manage 12-Factor config on bare metal and VMs?

Not every workload runs in Kubernetes. Many Nepali government systems and legacy enterprise applications still operate on standalone Ubuntu or RHEL servers. You can still achieve 12-Factor Config Across Runtimes without containers by leveraging systemd's powerful environment management features. Avoid placing .env files in application directories where they might be accidentally committed or exposed via web servers.

Systemd Drop-in Files

Use systemd drop-in directories to override service configurations without modifying the main unit file. This keeps vendor-provided units pristine while allowing site-specific configuration.

# Create override directory
sudo mkdir -p /etc/systemd/system/payment-service.service.d/

# Create environment override
sudo tee /etc/systemd/system/payment-service.service.d/env.conf <<EOF
[Service]
Environment="PAYMENT_GATEWAY_URL=https://api.npay.np"
Environment="LOG_LEVEL=info"
EnvironmentFile=-/run/secrets/payment-api-key
EOF

# Apply changes
sudo systemctl daemon-reload
sudo systemctl restart payment-service

The EnvironmentFile directive with the - prefix tells systemd to ignore the file if missing, preventing startup failures in development environments where secrets might be handled differently. For production, combine this with Ubuntu security hardening practices to restrict file permissions on secret files to root-only read access.

Docker Compose for Local Parity

Development environments must mirror production injection patterns to prevent "works on my machine" bugs. Use Docker Compose's env_file directive alongside explicit environment overrides to simulate Kubernetes behavior locally.

services:
  payment-service:
    image: registry.example.com/payment-svc:v2.4.1
    env_file:
      - .env.development
    environment:
      - LOG_LEVEL=debug
      - DB_HOST=postgres
    # Never put real secrets here; use .env.development for local mocks

This setup ensures developers test against the same configuration interface that production uses. The .env.development file contains safe defaults and mock endpoints, while CI pipelines inject real test credentials via runner environment variables.

How do you secure sensitive configuration across different runtimes?

Security is where 12-Factor Config Across Runtimes implementations most frequently fail. Storing secrets in plaintext environment variables exposes them to any process that can inspect /proc/<pid>/environ, including debugging tools, crash dumpers, and compromised sidecar containers. A defense-in-depth approach is mandatory for any system handling financial or personal data.

  • Never log configuration: Implement structured logging filters that redact keys matching patterns like *KEY*, *SECRET*, *TOKEN*, or *PASSWORD*. See structured logging best practices for implementation patterns.
  • Use short-lived credentials: Prefer dynamic secrets from HashiCorp Vault or AWS Secrets Manager over static API keys. Rotate automatically and tie credential lifetime to pod or instance lifecycle.
  • Encrypt at rest and in transit: Enable KMS encryption for Kubernetes Secrets, use TLS for all config retrieval calls, and ensure etcd encryption is active.
  • Restrict access via RBAC: Limit who can read Secrets in Kubernetes and who can access parameter store paths in cloud providers. Apply least-privilege principles rigorously.
  • Audit all access: Log every read of sensitive configuration. Unusual access patterns often indicate compromise before data exfiltration occurs.

For highly regulated environments, consider injecting secrets via mounted volumes with 0400 permissions rather than environment variables. While less pure from a 12-Factor perspective, this prevents leakage through process inspection and allows tighter filesystem-level access controls. The trade-off between theoretical purity and practical security always favors security.

Plaintext ENV Variables (Least Secure)Visible in /proc, logs, crash dumpsEncrypted Secrets / Mounted FilesKMS-backed, restricted fs permissions, audit loggedDynamic Short-Lived CredentialsVault/AWS SSM, auto-rotation, tied to workload identityZero-Secret Injection (Ideal)↑ Increasing Security Posture for 12-Factor Config Across Runtimes ↑
Security maturity model: progress from plaintext environment variables toward dynamic, short-lived credentials for production-grade 12-Factor Config Across Runtimes.

Implement Resilient Configuration Today

Adopting 12-Factor Config Across Runtimes is not about dogmatic purity; it is about building systems that survive personnel changes, infrastructure migrations, and security audits without catastrophic rewrites. Start by documenting your configuration contract, then migrate one service at a time to proper injection patterns. Validate at boot, secure your secrets, and treat configuration as a first-class engineering concern. If your team needs help auditing current practices or designing a migration path, reach out to discuss your infrastructure.

Frequently Asked Questions

It is applying the third factor of the Twelve-Factor App methodology to manage configuration strictly through environment variables across diverse execution environments like containers, serverless functions, and traditional VMs without code changes.

Use External Secrets Operator or Sealed Secrets to sync credentials from vaults into Kubernetes Secrets. Mount these as environment variables via pod specs, ensuring sensitive data never resides in container images or plaintext config files within the cluster.

Only for local development. Production runtimes must inject variables directly via orchestration tools. Loading dot-env files in production violates strict 12-factor principles by coupling configuration to the filesystem rather than the execution environment.

Laravel caches config via php artisan config:cache, requiring all env calls to exist only in config files. Node.js typically reads process.env directly at runtime, meaning Laravel requires stricter build-time validation to prevent missing variable errors in cached production containers.

Use conf-test or custom CI scripts to diff required variables against deployed environments. Tools like dotenv-linter check format validity, while Terraform or Helm validators ensure infrastructure definitions match application expectations before runtime failures occur in staging or production.

Yes, but it requires an adapter. Applications should not call AWS APIs directly. Instead, use init containers or sidecars to fetch parameters and inject them as standard environment variables, maintaining runtime agnosticism and keeping the application code free of cloud vendor dependencies.

Centralize configuration definitions in Git using Helm charts or Terraform modules. Automate synchronization pipelines that push identical variable sets to ECS, Lambda, and EC2 simultaneously, preventing manual overrides that cause inconsistent behavior between development, staging, and production environments.

Yes, if fetching remote secrets synchronously during initialization. Mitigate this by provisioning secrets via infrastructure-as-code during deployment or using Lambda SnapStart. Avoid runtime API calls for configuration to maintain millisecond-level startup performance compliant with 12-factor principles.

Hardcoding fallback values in application code for critical settings. This masks missing environment variables during deployment, causing silent failures or security gaps. Fail fast at startup if required variables are absent to ensure configuration integrity across all target runtimes.

Implement graceful signal handling to reload environment variables or restart workers. Since true 12-factor apps treat config as immutable per instance, use rolling deployments to replace old instances with new ones containing updated credentials, avoiding in-place mutation risks.

Static boolean toggles fit environment variables, but dynamic targeting rules do not. Complex feature flagging requires dedicated services like LaunchDarkly. Keep 12-factor env vars limited to deployment-specific settings, separating operational configuration from business logic state management.

BuildKit prevents secret leakage during image builds via --mount=type=secret. Never bake production config into layers. Ensure final images contain zero sensitive data, relying entirely on runtime injection to satisfy 12-factor portability requirements across different host environments.

Files create artifact-environment coupling. Environment variables provide a universal interface supported by every OS and orchestrator. This abstraction allows identical binaries to deploy anywhere without modification, which is the core value proposition of 12-factor portability.

Use direnv or containerized dev environments like DevContainers. These isolate project-specific variables from your global shell session, ensuring local testing mirrors production injection patterns without risking credential leakage or variable conflicts between multiple projects.

Primary costs are engineering time for pipeline automation and secret manager fees. HashiCorp Vault Enterprise or AWS Secrets Manager charge per secret or API call. Open-source alternatives reduce licensing costs but increase operational overhead for maintenance and security patching.