Kestrel and Hosting

Kestrel is the server inside your process: how it binds ports, and how ForwardedHeaders restores the caller's real scheme and address.

Syntaxapp.Run("http://0.0.0.0:8080") // or ASPNETCORE_URLS / ASPNETCORE_HTTP_PORTS

Kestrel is the web server ASP.NET Core runs on. It is a library inside your own process, not a separate program you install. WebApplication.CreateBuilder selects it for you. It handles HTTP/1.1, HTTP/2, HTTP/3 and WebSockets, on Windows, Linux and macOS.

Binding

With nothing configured, Kestrel listens on http://localhost:5000. The project templates write development ports into Properties/launchSettings.json, which is why a new project starts on something like 5143. You can override the address with dotnet run --urls, with the ASPNETCORE_URLS environment variable, with ASPNETCORE_HTTP_PORTS for a bare port number, with a Kestrel:Endpoints section in appsettings.json, or in code.

localhost means loopback only. Inside a container that makes the service unreachable from outside, so bind 0.0.0.0 or + instead. The official mcr.microsoft.com/dotnet/aspnet images set ASPNETCORE_HTTP_PORTS=8080; it changed from 80 in .NET 8 so that a non-root user can bind it.

Behind a reverse proxy

In production something usually sits in front: nginx, a Kubernetes ingress, a cloud load balancer. It terminates TLS and forwards plain HTTP. Kestrel then sees the proxy as the client: Request.Scheme is http and Connection.RemoteIpAddress is the proxy's address. The proxy records the truth in the X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host headers, and UseForwardedHeaders copies those values back onto the request.

Example

Example · csharp
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

// Without this, ForwardedHeaders is None and the middleware reads nothing.
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders =
        ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;

    // The defaults trust only loopback: KnownNetworks holds 127.0.0.0/8 and
    // KnownProxies holds the IPv6 loopback address. A proxy in another
    // container or pod is neither, so clear both lists. Only safe when
    // nothing but the proxy can reach this port.
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
});

var app = builder.Build();

// Must run before anything that reads the scheme or the client address.
app.UseForwardedHeaders();

app.MapGet("/whoami", (HttpContext context) => new
{
    Scheme = context.Request.Scheme,
    Host = context.Request.Host.ToString(),
    ClientIp = context.Connection.RemoteIpAddress?.ToString()
});

// Bind every interface. localhost would be unreachable from outside a container.
app.Run("http://0.0.0.0:8080");

When to use it

  • Make a containerised service reachable, instead of it listening on loopback only.
  • Recover the caller's real IP address for rate limiting or audit logs behind an ingress.
  • Stop generated links coming out as http:// when the public site is https://.
  • Pick a fixed port per environment without rebuilding the image.

More examples

Endpoints from configuration

This appsettings.json section tells Kestrel where to listen with no code change at all, which is what you want when the port differs between environments. The endpoint name, Http here, is yours to choose and is only a label. A Kestrel:Endpoints section replaces any address given by the Urls setting rather than adding to it, which is a common surprise when both are present.

Example · json
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://0.0.0.0:8080"
      }
    }
  }
}

The same endpoint written in code

Use the code form when the choice is not a plain address: limits on request body size, a certificate loaded from somewhere unusual, or a Unix socket. Endpoints added with Listen or ListenAnyIP are cumulative with those in the Kestrel configuration section, so the application would listen on both. They do override an address given by UseUrls, ASPNETCORE_URLS or --urls, so hard-coding a port here removes the operator's ability to move the service with an environment variable, which is usually a mistake in a containerised service.

Example · csharp
var builder = WebApplication.CreateBuilder(args);

// The same binding as the appsettings.json above, expressed in C#.
// Endpoints added here are cumulative with any in the Kestrel
// configuration section, but they do override ASPNETCORE_URLS.
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    // ListenAnyIP binds every network interface.
    // ListenLocalhost(8080) would bind loopback only.
    serverOptions.ListenAnyIP(8080);
});

var app = builder.Build();

app.MapGet("/", () => "ok");

app.Run();

Ports from the outside

This is the fix for the most common first container failure: mapping to port 80 and getting connection reset. Before .NET 8 the images defaulted to 80; they now default to 8080 because 80 is privileged and the images stopped running as root. Prefer passing the port in from outside, so the same image can run on any port.

Example · bash
# The official aspnet images set ASPNETCORE_HTTP_PORTS=8080 and run as a
# non-root user, so 8080 is the port inside the container, not 80.
docker run --rm -p 8000:8080 my-api

# Override it for a single run.
docker run --rm -p 8000:5000 -e ASPNETCORE_HTTP_PORTS=5000 my-api

# Locally, dotnet run takes the address on the command line.
dotnet run --urls "http://0.0.0.0:8080"

Discussion

  • Be the first to comment on this lesson.