Configuration and Environment Basics
Configuration Is a Runtime Concern
In production systems, configuration must not be hardcoded. Ports, database URLs, API keys, timeouts, and feature toggles change between environments. A Rust service must separate build-time decisions from runtime configuration and fail fast if required configuration is missing.
Environment Variables as the Baseline
The most portable and container-friendly configuration mechanism is environment variables. They work across Linux servers, Docker, Kubernetes, and CI systems.
use std::env;
fn main() {
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
println!("Connecting to {}", database_url);
}
Production principle: if a required variable is missing, crash early during startup. Do not allow partially configured services to run.
Strongly Typed Configuration Struct
A production service should parse configuration once at startup and expose a typed struct to the rest of the application.
use std::env;
pub struct Config {
pub database_url: String,
pub port: u16,
pub request_timeout_ms: u64,
}
impl Config {
pub fn from_env() -> Result<Self, String> {
let database_url = env::var("DATABASE_URL")
.map_err(|_| "DATABASE_URL missing".to_string())?;
let port = env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse()
.map_err(|_| "PORT must be a valid number".to_string())?;
let request_timeout_ms = env::var("REQUEST_TIMEOUT_MS")
.unwrap_or_else(|_| "5000".to_string())
.parse()
.map_err(|_| "REQUEST_TIMEOUT_MS must be a number".to_string())?;
Ok(Self {
database_url,
port,
request_timeout_ms,
})
}
}
This ensures:
- Parsing happens once
- Validation is centralized
- Business logic never reads environment variables directly
Using dotenv in Development Only
In local development, it is convenient to load environment variables from a .env file. This should not replace real environment configuration in production.
# .env DATABASE_URL=postgres://localhost/dev PORT=8080
dotenvy = "0.15"
fn main() {
dotenvy::dotenv().ok();
let config = Config::from_env().expect("Invalid configuration");
}
Rule: do not rely on .env in production containers. Use real environment injection.
Fail Fast, Not Lazily
Never lazily read environment variables deep inside request handlers. That makes configuration errors appear under load instead of at startup.
Correct approach:
- Parse configuration at startup
- Validate it completely
- Pass Config via dependency injection
Configuration vs Secrets
Not all configuration is equal. Secrets like database passwords or API tokens must never be logged.
Guidelines:
- Never print secrets in debug logs
- Keep secret values out of panic messages
- Use separate secret management in Kubernetes or cloud environments
Build-Time vs Runtime Configuration
Rust supports compile-time configuration using env! and option_env! macros, but these embed values into the binary.
const BUILD_VERSION: &str = env!("CARGO_PKG_VERSION");
Acceptable use cases:
- Embedding build metadata
- Feature flags decided at compile time
Not acceptable:
- Embedding database URLs
- Embedding API keys
Container-Aware Configuration
In containerized deployments:
- Environment variables are injected by Docker or Kubernetes
- Config maps hold non-sensitive config
- Secrets are injected separately
Your Rust service should assume it is stateless and externally configured.
Production Checklist
- All required env vars validated at startup
- No lazy env reads inside handlers
- Strongly typed Config struct
- .env used only in development
- Secrets never logged
- Clear separation between build-time and runtime config
Configuration discipline prevents subtle runtime failures and makes your Rust service predictable across environments. Production reliability starts at process startup.