Configuration in Containers
How settings and secrets reach a containerised app: double-underscore environment variables, mounted files, and secret stores.
builder.Configuration.AddKeyPerFile(directoryPath: "/run/secrets", optional: true);A container image should be identical in every environment. Only its configuration changes. That rules out editing appsettings.Production.json and rebuilding, so the values have to arrive from outside the image at start-up.
Environment variables
This is the common case. A configuration key uses a colon between levels, but a colon is not usable in an environment variable name in a Linux shell. So the provider accepts two underscores instead and converts them to a colon. ConnectionStrings__Default becomes ConnectionStrings:Default, and Logging__LogLevel__Default becomes Logging:LogLevel:Default. Both spellings are the same key; only the double-underscore form works everywhere.
Mounted files
An environment variable is visible to anything that can inspect the process, which is why Docker secrets and Kubernetes Secrets can instead be mounted as a directory holding one file per value. AddKeyPerFile, from the Microsoft.Extensions.Configuration.KeyPerFile package, reads such a directory: the file name is the key, the file contents is the value, and a double underscore in the file name again becomes a colon. Docker mounts these at /run/secrets.
A secret store as a provider
A vault such as Azure Key Vault plugs in as one more provider. After AddAzureKeyVault, secrets are read as ordinary configuration keys, so no service in the app knows a vault exists.
Locally
Never commit a secret to appsettings.json. Use dotnet user-secrets, which stores values outside the project and which CreateBuilder loads only in the Development environment.
Calling curl http://localhost:8080/config against the example returns JSON with the host and the length of the mounted password, never the password itself.
Example
var builder = WebApplication.CreateBuilder(args);
// Docker and Kubernetes can mount secrets as one file per value.
// A file named Database__Password becomes the key Database:Password.
// AddKeyPerFile comes from Microsoft.Extensions.Configuration.KeyPerFile.
if (Directory.Exists("/run/secrets"))
{
builder.Configuration.AddKeyPerFile(directoryPath: "/run/secrets", optional: true);
}
var app = builder.Build();
// Report that the secret arrived without printing it.
app.MapGet("/config", (IConfiguration config) => new
{
host = config["Database:Host"] ?? "(unset)",
passwordLength = config["Database:Password"]?.Length ?? 0
});
app.Run();When to use it
- Ship one image to staging and production and change only the environment variables passed to it.
- Read a database password from a Kubernetes Secret mounted at /run/secrets instead of an environment variable.
- Keep a local connection string off the repository with dotnet user-secrets while the deployed app reads the same key from a vault.
More examples
Passing settings to a container
This is the shape of almost every containerised deploy: plain settings as environment variables, the password as a mounted file. The secret name is also the file name, so naming it Database__Password is what makes AddKeyPerFile produce the key Database:Password. Name it db-password and the key becomes db-password, which nothing reads.
# compose.yaml
services:
api:
image: shop-api:1.4.0
ports:
- "8080:8080"
environment:
# Two underscores stand in for the colon in the key name.
ASPNETCORE_ENVIRONMENT: Staging
ConnectionStrings__Default: "Host=db;Database=shop;Username=app"
Logging__LogLevel__Default: Warning
secrets:
# Mounted at /run/secrets/Database__Password inside the container.
- Database__Password
secrets:
Database__Password:
file: ./secrets/db-password.txtReading a vault as configuration
A vault earns its place when secrets must be rotated or audited without a redeploy. The point of adding it as a configuration provider is that the rest of the app is unchanged: services still read Database:Password. DefaultAzureCredential picks up the identity the container already runs as, so there is no secret needed to fetch the secrets.
// Requires: Azure.Extensions.AspNetCore.Configuration.Secrets and Azure.Identity
using Azure.Identity;
var builder = WebApplication.CreateBuilder(args);
// Locally the same keys come from user secrets, so only add the vault elsewhere.
if (!builder.Environment.IsDevelopment())
{
// A vault secret named Database--Password becomes the key Database:Password,
// because a vault secret name may not contain a colon.
builder.Configuration.AddAzureKeyVault(
new Uri("https://shop-prod.vault.azure.net/"),
new DefaultAzureCredential());
}
var app = builder.Build();
app.MapGet("/ready", () => Results.Ok("ready"));
app.Run();Secrets on your own machine
Use this instead of putting a real value in appsettings.Development.json, which is checked in. CreateBuilder adds the user secrets provider only when the environment is Development, and it sits above both appsettings files, so these values win locally and are simply absent in a container. Note the key uses a colon here: this is a configuration key, not an environment variable name.
# Once per project. Writes a UserSecretsId into the .csproj file.
dotnet user-secrets init
# Stored outside the project directory, so it cannot be committed.
dotnet user-secrets set "ConnectionStrings:Default" "Host=localhost;Database=shop"
dotnet user-secrets set "Database:Password" "local-only-password"
# Check what is there.
dotnet user-secrets list
Discussion