Environment Variables and the Double Underscore
Override any configuration key from a shell, Docker or Kubernetes using double-underscore names, prefixes and array indexes.
Logging__LogLevel__Default=Warning
ConnectionStrings__Shop="Host=orders-db;Database=shop"
Orders__AllowedCurrencies__0=EUR
builder.Configuration.AddEnvironmentVariables(prefix: "ORDERSAPI_");Environment variables are how containers receive configuration, so every key an operator may need to change must be reachable through one. The environment variable provider reads the process environment once, when configuration is built, and turns each variable into a configuration key.
Name mapping
A double underscore becomes the section separator: Logging__LogLevel__Default=Warning sets Logging:LogLevel:Default, and ConnectionStrings__Shop is what GetConnectionString("Shop") reads. A colon is legal in Windows variable names but not in bash or in most container tooling, so use __ everywhere. Keys are case-insensitive, while Linux variable names are not: ORDERS__REGION and Orders__Region are two variables that map to the same key, and which one wins is not something to rely on.
Arrays and dictionaries
An index is just another segment: Orders__AllowedCurrencies__0=EUR. An override replaces only that index. If appsettings.json lists four currencies and the deployment sets indexes 0 and 1, the bound list still has four entries, the last two from the file. There is no way to delete a key from a lower provider, so a list that must shrink per environment should not have a default in the file. Dictionary entries work the same way: Payments__MerchantAccounts__EU=ShopEU_ECOM.
Prefixes
| Variables | What happens |
|---|---|
ASPNETCORE_*, DOTNET_* | Read first as host settings with the prefix removed (ASPNETCORE_ENVIRONMENT becomes environment) |
| Everything, unprefixed | Read by the default provider, after the JSON files |
AddEnvironmentVariables("ORDERSAPI_") | Only matching variables, prefix removed; added last, so it wins |
SQLCONNSTR_, SQLAZURECONNSTR_, MYSQLCONNSTR_, CUSTOMCONNSTR_ | Mapped to ConnectionStrings:{name}; the first three also set {name}_ProviderName |
The connection-string prefixes exist for Azure App Service, which injects connection strings that way.
Containers and Kubernetes
Values come from ENV in the Dockerfile, -e or --env-file on docker run, and env/envFrom in a pod spec, where env beats envFrom for the same name. Because the provider never reloads, editing a ConfigMap does nothing to running pods: restart them with a rollout. Kubernetes also injects service link variables such as ORDERS_PORT=tcp://10.96.14.3:8080 for every Service in the namespace. An app that reads ORDERS_-prefixed variables then finds a key Port holding a URL. Set enableServiceLinks: false on the pod, or choose a prefix no Service name produces.
Failure modes
A misspelled variable is simply an unused key: Loging__LogLevel__Default raises no error. A single underscore (Orders_Region) is a different key too. Validate options at start-up so missing values fail loudly, and print the effective keys with their source when a container behaves differently from your laptop.
Example
When to use it
- Set ConnectionStrings__Shop from a Kubernetes Secret with secretKeyRef so the connection string never appears in the manifest.
- Turn on Debug logging for one pod with Logging__LogLevel__Orders.Api=Debug while the rest stay at Information.
- Diagnose a pod whose prefixed settings read tcp://10.96.14.3:8080 because a Service named orders injected ORDERS_PORT.
More examples
Kubernetes: env, envFrom and secret references
<code>envFrom</code> turns every key of the ConfigMap into a variable, so ConfigMap keys should already use the <code>Section__Key</code> form. Entries under <code>env</code> override <code>envFrom</code> entries with the same name. <code>secretKeyRef</code> keeps the connection string out of the manifest. <code>enableServiceLinks: false</code> stops Kubernetes from adding <code>{SERVICE}_PORT</code> style variables that can collide with prefixed configuration. None of these values change in a running pod; a <code>kubectl rollout restart</code> is needed after editing the ConfigMap.
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
spec:
replicas: 3
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
spec:
enableServiceLinks: false
containers:
- name: api
image: registry.example.com/shop/orders-api:1.14.2
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: orders-api-settings
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
- name: Logging__LogLevel__Default
value: Warning
- name: Orders__AllowedCurrencies__0
value: USD
- name: Orders__AllowedCurrencies__1
value: EUR
- name: ConnectionStrings__Shop
valueFrom:
secretKeyRef:
name: orders-api-db
key: connection-stringShells and docker run
Most "my setting is ignored" reports come down to the variable never reaching the process: a typo, a single underscore, or quotes kept as part of the value. <code>printenv</code> inside the container shows the truth. Filter the output to non-secret prefixes, because a full <code>printenv</code> also prints connection strings and keys into your terminal history and CI logs.
# Double underscores work in every shell; a colon is not valid in a bash variable name.
export Orders__MaxItemsPerOrder=100
dotnet run --project src/Orders.Api
# Individual variables, or a file of KEY=value lines.
# docker run --env-file keeps quotes literally, so do not quote values in that file.
docker run -d --rm --name orders-api -p 8080:8080 \
-e ASPNETCORE_ENVIRONMENT=Staging \
-e Orders__Region=eu-west \
--env-file ./orders-api.env \
registry.example.com/shop/orders-api:1.14.2
# Check what the running container really received (non-secret keys only).
docker exec orders-api printenv | grep -E '^(ASPNETCORE_|Orders__|Logging__)'Reading only prefixed variables
By default every variable in the process, including <code>PATH</code>, <code>HOSTNAME</code> and Kubernetes service links, becomes a configuration key. Swapping the source for a prefixed one keeps configuration to variables meant for this service, and the operator sets <code>ORDERSAPI_Payments__Provider</code>. The cost is that conventional names such as <code>ConnectionStrings__Shop</code> stop working, so document the prefix. Setting an entry in <code>Sources</code> makes the <code>ConfigurationManager</code> reload its providers immediately.
using Microsoft.Extensions.Configuration.EnvironmentVariables;
var builder = WebApplication.CreateBuilder(args);
// Replace the unprefixed provider in place, so precedence stays the same
// but only ORDERSAPI_* variables become configuration.
var sources = builder.Configuration.Sources;
var index = sources.ToList().FindIndex(s => s is EnvironmentVariablesConfigurationSource { Prefix: null });
if (index >= 0)
{
sources[index] = new EnvironmentVariablesConfigurationSource { Prefix = "ORDERSAPI_" };
}
var app = builder.Build();
app.MapGet("/payments/provider", (IConfiguration config) =>
TypedResults.Ok(new { provider = config["Payments:Provider"] ?? "Stripe" }));
app.Run();
Discussion