Configuration & Secrets in Prod
Rust services follow twelve-factor config: store settings in the environment, load once at startup, and never commit secrets to git or bake them into images.
Recipe
[dependencies]
config = "0.14"
serde = { version = "1.0", features = ["derive"] }use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Settings {
database_url: String,
port: u16,
}
impl Settings {
fn from_env() -> Result<Self, config::ConfigError> {
config::Config::builder()
.add_source(config::Environment::default().separator("__"))
.build()?
.try_deserialize()
}
}When to reach for this:
- Every deployable binary or Axum service
- Rotating API keys without rebuilds
- Multi-environment promotion (dev, staging, prod)
Working Example
# Kubernetes secret -> env
# env:
# - name: DATABASE_URL
# valueFrom:
# secretKeyRef:
# name: db-credentials
# key: url
export PORT=8080
export DATABASE_URL=postgres://user:pass@host/db
./my-service// Never log secrets
tracing::info!(port = settings.port, "listening");
// BAD: tracing::info!(?settings.database_url);What this demonstrates:
- Nested env keys via
APP__DATABASE__URLstyle separators - Secrets only in orchestrator secret stores
- Logs redact sensitive fields
Deep Dive
Layering
- Defaults in code (non-secret only)
- Config files for non-secret toggles (mounted ConfigMap)
- Environment overrides for prod
- Secrets from Vault / AWS SM / K8s Secrets at runtime
Rust Crates
| Crate | Role |
|---|---|
config | Layered file + env |
figment | Flexible sources |
dotenvy | Local dev only (not prod loader) |
Gotchas
.envin production - file leakage risk. Fix: dotenvy for dev; orchestrator env in prod.- Secrets in
RUST_LOGdebug dumps - accidental exposure. Fix: structured logging with allowlists. - Printing config at startup - passwords in journald. Fix: log keys only, mask values.
- Immutable config mid-flight - race on reload. Fix: restart pods or explicit watch API.
- Same secret all environments - blast radius. Fix: per-env credentials and rotation.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| HashiCorp Vault agent | Dynamic DB creds | Simple static API keys |
| SOPS-encrypted files | Git-stored non-K8s deploys | Pure 12-factor K8s |
| Hard-coded defaults | Local dev ergonomics | Production secrets |
FAQs
Where to put `SQLX_OFFLINE`?
CI only. Runtime uses live DATABASE_URL.
How to validate config early?
Parse in main before binding sockets; exit non-zero with clear message.
TLS certs as secrets?
Mount as files; read paths from env (TLS_CERT_PATH). Do not inline PEM in env vars.
Feature flags?
Non-secret flags can live in ConfigMap; sensitive rollout keys stay in secret store.
Related
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.