Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Layer defaults, a file, and environment variables

config-badge cat-config-badge

Most applications resolve their configuration from several places: defaults that ship with the binary, a config file an operator edits, and environment variables set at deploy time. config builds one typed value from those layers, where each source overrides the one before it.

ConfigBuilder collects sources in precedence order. set_default seeds the baseline values, File loads config.toml (marked required(false) so a missing file falls back to the defaults), and Environment reads APP_-prefixed variables. APP_PORT=9090 sets the port key. build merges them and try_deserialize produces the typed Settings.

use std::path::Path;

use config::{Config, Environment, File};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct Settings {
    host: String,
    port: u16,
    log_level: String,
}

fn load(config_path: &Path) -> Result<Settings, config::ConfigError> {
    Config::builder()
        .set_default("host", "127.0.0.1")?
        .set_default("port", 8080)?
        .set_default("log_level", "info")?
        .add_source(File::from(config_path).required(false))
        .add_source(Environment::with_prefix("APP"))
        .build()?
        .try_deserialize()
}

fn main() -> Result<(), config::ConfigError> {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("config.toml");
    let settings = load(&path)?;
    println!("listening on {}:{}", settings.host, settings.port);
    println!("log level: {}", settings.log_level);
    Ok(())
}

The bundled config.toml supplies the file layer:

host = "0.0.0.0"
port = 3000
log_level = "debug"