Configuration and Limits in a Container
Environment variables, the port the app listens on, how .NET reads a CPU or memory limit, and what happens when it goes over.
docker run -e ASPNETCORE_HTTP_PORTS=8080 -m 512m --cpus 1.5 -p 8080:8080 inventory-api:1.4.0An image is fixed. Everything that differs between staging and production arrives as an environment variable, a named value handed to the process when it starts. ASP.NET Core reads them as configuration, with a double underscore standing in for the colon in a nested key: ConnectionStrings__Inventory is the key ConnectionStrings:Inventory. ASPNETCORE_ENVIRONMENT selects the environment name.
The port
Since .NET 8 the official images listen on port 8080, not 80, because a non-root user cannot bind below 1024. ASPNETCORE_HTTP_PORTS=8080 changes it. That is shorthand for ASPNETCORE_URLS=http://*:8080/, and if both are set, ASPNETCORE_URLS wins.
CPU and memory limits
A limit is a kernel setting on the container, and .NET reads it rather than the host's figures. Environment.ProcessorCount returns the CPU limit rounded up to a whole number, fixed at startup. The garbage collector treats the memory limit as the machine's memory: with no explicit setting, its heap hard limit becomes 75% of that limit, or 20 MB, whichever is larger.
Going over
Two different failures. Past the GC's heap hard limit, .NET throws OutOfMemoryException and you get a stack trace. Past the container's memory limit, the kernel kills the process outright.
Example
// Program.cs - what a container's settings look like from inside the app.
// `docker run -e` and Kubernetes both deliver these as environment
// variables. They are set in code here so the output is the same when you
// run the file on your own machine.
Environment.SetEnvironmentVariable("ASPNETCORE_HTTP_PORTS", "8080");
Environment.SetEnvironmentVariable("ConnectionStrings__Inventory", "Host=db");
// Without the images' default, an ASP.NET Core app would use port 5000.
string port = Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS") ?? "5000";
// A double underscore is how an environment variable spells a nested
// configuration key: this is ConnectionStrings:Inventory.
string conn = Environment.GetEnvironmentVariable("ConnectionStrings__Inventory")
?? "(none)";
Console.WriteLine($"listening port: {port}");
Console.WriteLine($"connection: {conn}");
// Output:
// listening port: 8080
// connection: Host=dbWhen to use it
- Run one image in staging and production, with only the environment variables different.
- Diagnose a service that is killed with exit code 137 under load and logs nothing.
- Right-size a memory limit by logging the heap figures the runtime actually sees.
More examples
Reading the limits the runtime sees
The numbers differ per machine, so there is no fixed output here. Use it to catch the common mismatch where a manifest sets a 512 MiB limit but the container was started without one, and the runtime sizes its heap for a 64 GB host.
// Program.cs - log the limits once at startup. Print this on a new
// deployment and you find out immediately whether the limits you wrote in
// the manifest are the limits the process actually got.
using System.Runtime;
GCMemoryInfo info = GC.GetGCMemoryInfo();
// Inside a container with a memory limit, this is the container's limit,
// not the host's RAM. Divide twice to get mebibytes.
long availableMiB = info.TotalAvailableMemoryBytes / 1024 / 1024;
// A CPU limit of 1.5 gives 2 here: the limit, rounded up. Fixed at startup.
int cpus = Environment.ProcessorCount;
Console.WriteLine($"available memory: {availableMiB} MiB");
Console.WriteLine($"processor count: {cpus}");The same settings in a Kubernetes manifest
The production form of the docker run command in the syntax line. Quote the port value: YAML would otherwise read 8080 as a number, and a container environment variable must be a string.
spec:
containers:
- name: inventory-api
image: ghcr.io/example/inventory-api:1.4.0
ports:
- containerPort: 8080
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
- name: ASPNETCORE_HTTP_PORTS
value: "8080"
# Double underscore for the nested key ConnectionStrings:Inventory.
- name: ConnectionStrings__Inventory
valueFrom:
secretKeyRef:
name: inventory-db
key: connection-string
resources:
# requests: what the scheduler reserves for this container.
requests:
cpu: "250m"
memory: "256Mi"
# limits: the ceiling the kernel enforces, and what .NET reads.
limits:
cpu: "1"
memory: "512Mi"Shorthand and longhand for the listening port
You will meet both. ASPNETCORE_URLS is older and appears in most existing deployment scripts; the port list is easier to get right because there is no scheme, wildcard or trailing slash to mistype. Knowing they are the same setting saves an afternoon when a stale ASPNETCORE_URLS silently overrides a new port.
# Shorthand. A semicolon-separated list of port numbers.
-e ASPNETCORE_HTTP_PORTS=8080
-e ASPNETCORE_HTTP_PORTS="8080;8081"
# Longhand. The same two endpoints written as full URLs.
-e ASPNETCORE_URLS="http://*:8080/"
-e ASPNETCORE_URLS="http://*:8080/;http://*:8081/"
# ASPNETCORE_HTTP_PORTS was added in ASP.NET Core 8.0. If both are set,
# ASPNETCORE_URLS takes effect and the port list is ignored.
Discussion