
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Failing to properly manage secrets and config in Elixir apps is the most common security vulnerability I encounter during infrastructure audits. While Elixir’s compile-time configuration system is powerful for performance, it becomes dangerous when developers accidentally bake API keys or database passwords into release artifacts. The solution requires shifting from static files to a strict runtime evaluation strategy that separates immutable code from mutable, sensitive data. This guide covers the exact patterns needed to secure your Phoenix or OTP applications in production environments.
runtime.exs for all environment-specific values and never commit credentials to version control. Inject secrets via environment variables or external providers like AWS Secrets Manager at boot time, ensuring releases remain immutable and audit-compliant across staging and production.For teams operating in regulated environments or handling payments, this separation isn't just best practice—it's mandatory. If you are building financial technology or handling user data, aligning your Elixir configuration with broader data protection and security basics ensures you meet compliance requirements without sacrificing developer velocity. The following architecture illustrates how configuration flows safely through an Elixir release lifecycle.
How do you manage secrets and config in Elixir apps using runtime.exs?
The introduction of runtime.exs was a pivotal moment for Elixir operations. Before this, many teams used prod.secret.exs, which was evaluated at compile time. This meant changing a database password required rebuilding the entire release—a violation of modern immutable infrastructure principles. Today, runtime.exs executes on the target machine every time the application boots, allowing the same binary artifact to serve dev, staging, and production simply by changing the environment context.
Structuring runtime configuration correctly
Your runtime.exs should be the single source of truth for anything that varies between environments. A common mistake is leaving static defaults in config/prod.exs that silently override runtime values. Keep prod.exs for structural settings (logger formats, telemetry pipelines) and reserve runtime.exs strictly for connection details and credentials.
# config/runtime.exs
import Config
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
"""
config :my_app, MyApp.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
ssl: true
config :my_app, MyAppWeb.Endpoint,
server: true,
http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")],
secret_key_base: secret_key_base
end This pattern guarantees fail-fast behavior. If a required secret is missing, the node refuses to start rather than running in a degraded state. In my experience auditing SOC 2 environments, this explicit failure mode is preferred over silent fallbacks that mask misconfigurations until a customer triggers an error. For deeper context on handling these variables safely at the OS level, review Ubuntu environment variables explained.
What are the safest methods to inject secrets into Elixir releases?
Once your code is structured to read from the environment, you must decide how those values reach the container or VM. Direct environment variables are the baseline, but they have limitations: they appear in process lists, crash dumps, and shell histories. For higher security postures, integrate with dedicated secrets managers.
- Environment Variables: Simplest approach; suitable for non-sensitive config and low-risk internal tools. Use Kubernetes Secrets or Docker Compose env_file for local parity.
- AWS Secrets Manager / SSM Parameter Store: Best for AWS-native stacks. Requires IAM roles instead of long-lived access keys. Adds ~200ms to boot time but provides automatic rotation and audit trails.
- HashiCorp Vault: Industry standard for multi-cloud. Supports dynamic database credentials and PKI. Higher operational complexity but unmatched policy granularity.
- SOPS / Age Encryption: Encrypt secrets directly in Git repositories. Good for GitOps workflows where external vaults are overkill. Decryption happens at deploy time, not runtime.
When integrating external providers, wrap the fetch logic in a custom config provider module. This keeps runtime.exs clean and testable. Never make HTTP calls directly inside the config file itself; use the Config.Provider behaviour introduced in Elixir 1.9 to handle initialization before the supervision tree starts.
How does Elixir runtime config compare to compile-time configuration?
Understanding the boundary between compile-time and runtime is critical for reliability. Compile-time config (config.exs, prod.exs) is baked into the Erlang BEAM files. Changing it requires a full rebuild and redeploy. Runtime config (runtime.exs) is evaluated fresh on every boot. Mixing these up causes subtle bugs where staging values leak into production builds because they were captured during CI compilation.
| Aspect | Compile-Time (config.exs) | Runtime (runtime.exs) |
|---|---|---|
| Evaluation | During mix release | On application boot |
| Mutability | Immutable in artifact | Changes per environment |
| Use Case | Logger backends, OTP app structure | DB URLs, API keys, hostnames |
| Secret Safety | Unsafe (embedded in binary) | Safe (injected externally) |
| Restart Required | Yes (rebuild needed) | No (just restart service) |
In practice, I recommend treating config.exs as documentation of your application's structural dependencies, while runtime.exs serves as the operational interface. This mental model prevents accidental credential leakage and aligns with twelve-factor app principles. For teams managing complex Kubernetes deployments, this distinction maps directly to Kubernetes secrets management done right, where ConfigMaps hold structure and Secrets hold sensitive data.
What are common pitfalls when configuring Elixir for production?
Even experienced teams stumble on Elixir-specific configuration traps. These issues rarely surface in development but cause outages or security incidents in production. Avoiding them requires discipline and automated validation.
- Hardcoding fallback secrets: Writing
System.get_env("KEY", "default-dev-key")in production config. If the env var is unset, your app runs with a known insecure key. Always raise on missing required secrets in prod. - Ignoring SSL/TLS verification: Setting
ssl: truewithout specifyingtransport_opts: [verify: :verify_peer]. Many Postgres/Ecto drivers default to no verification even with SSL enabled, exposing you to MITM attacks. - Logging sensitive config: Accidentally printing the full config struct during startup debugging. Use
Logger.metadata_filteror redaction libraries to scrub keys matching*secret*,*password*, or*token*. - Missing config providers in releases: Forgetting to add custom providers to the
releasesblock inmix.exs. Your code compiles fine, but the provider never initializes at boot, leaving secrets nil. - Assuming env vars persist: Relying on shell-exported vars that aren't passed through systemd, Docker entrypoints, or Kubernetes pod specs. Always verify the execution environment, not just your interactive shell.
These pitfalls compound when teams lack observability. Misconfigured secrets often manifest as cryptic connection timeouts or auth failures. Correlating these with deployment events requires structured logging and tracing. Integrating OpenTelemetry instrumentation early helps distinguish between genuine infrastructure failures and configuration drift.
How do you rotate secrets in Elixir without downtime?
Rotation is where most "secure" setups fail operationally. Elixir applications are long-lived; they don't automatically pick up new environment variables after boot. To rotate a database password or API key without restarting nodes, you need either a graceful restart strategy or dynamic credential support.
For standard env-var-based secrets, implement rolling restarts. In Kubernetes, update the Secret object and trigger a rollout. Elixir's OTP supervision trees handle individual process restarts gracefully, but the top-level config won't refresh without a node restart. For zero-downtime rotation of high-value secrets like database credentials, use Vault's dynamic secrets engine. Your Elixir app requests a short-lived lease at boot and periodically renews it. When rotation occurs, the old lease remains valid until expiry, preventing mid-query disconnections.
Always test rotation procedures in staging first. Verify that your connection pooling library (e.g., DBConnection, Finch) handles stale connections gracefully. Monitor error rates during rotation windows using golden signals. If you see spikes in 5xx errors or connection timeouts during rotation, your pool configuration or retry logic needs tuning before touching production.
Next Steps for Secure Elixir Configuration
To effectively manage secrets and config in Elixir apps, treat configuration as code that evolves separately from application logic. Audit your current runtime.exs for hardcoded fallbacks, verify SSL settings on all external connections, and implement automated secret scanning in your CI pipeline today. Security is not a feature you add later; it is the foundation your users trust. If your team needs help designing an audit-ready secrets architecture or migrating legacy Elixir configs to a secure runtime model, reach out to discuss your infrastructure.