Skip to content

Override settings with environment variables

Environment variables are the top layer: they beat both config files and embedded defaults, and they are re-read on every build() and every reload().

Register the prefix

let cfg = Config::<MyConfig>::builder()
    .embedded_default(include_str!("../defaults.yaml"))
    .user_file("/etc/mytool/config.yaml")
    .env_prefixed("MYTOOL_")
    .build()?;

Include the trailing underscore. env_prefixed("MYTOOL") looks equivalent and is not: the separator stays on the front of every key, nothing matches, and no error is reported. This is the single most common reason an environment override appears to do nothing.

Name the variable for the field you want to set

The prefix is stripped, the rest is lower-cased, and each remaining underscore becomes a level of nesting.

Field in your struct Variable to set
port: u16 MYTOOL_PORT
http.port (a nested struct) MYTOOL_HTTP_PORT
http.tls.enabled MYTOOL_HTTP_TLS_ENABLED

Matching on the variable name is case-insensitive, so mytool_port works as well as MYTOOL_PORT.

Set a boolean, a number or a list

Values are parsed rather than taken as raw strings:

MYTOOL_PORT=8080            # number
MYTOOL_DEBUG=true           # boolean
MYTOOL_HOSTS='[a.example,b.example]'   # list of two strings
MYTOOL_HOST=127.0.0.1       # string, dots and all

A list replaces the whole list from the layer below; there is no append syntax.

A value that does not fit the field's type fails the build with ConfigError::Parse, naming both the key and the prefix it came from:

configuration error: invalid type: found string "eighty", expected u16 for key "PORT" in `MYTOOL_` environment variable(s)

An empty variable is not the same as an unset one. MYTOOL_HOST= sets the field to the empty string.

Work around a field name that contains an underscore

A flat field like max_retries cannot be set from the environment. MYTOOL_MAX_RETRIES is read as the nested key max.retries, which your struct does not have, so it is ignored — no error, no warning, and the value silently stays as the file left it.

There is no flag to turn that off. Pick one of:

  • Nest it. retry: RetrySection { max: u32 } is reachable as MYTOOL_RETRY_MAX.
  • Rename it to a single word. MYTOOL_RETRIES reaches a field called retries.
  • Leave it file-only and document that max_retries is set in the config file rather than the environment.

Choosing between these is a design decision about which settings an operator must be able to override without editing a file — worth making deliberately when you name the field.

Apply a change without restarting

Environment variables are read at parse time, not cached at start-up, so a variable exported after the process started takes effect at the next reload:

// somewhere in the process, after MYTOOL_PORT changed
cfg.reload()?;
assert_eq!(cfg.get().port, 9999);

In practice a process cannot change its own environment from outside, so this matters for two cases: a supervisor that sets variables before an exec, and code inside the process that sets a variable and then reloads deliberately.

Accept more than one prefix

Register several. Later registrations win when the same key is set under both:

let cfg = Config::<MyConfig>::builder()
    .env_prefixed("LEGACYTOOL_")
    .env_prefixed("MYTOOL_")
    .build()?;

That is the shape to use when renaming a tool without breaking the old variables.

Next