ASPNETCORE_HTTP_PORTS

Make Kestrel listen on given ports on every interface with ASPNETCORE_HTTP_PORTS, and know what overrides it inside and outside containers.

Signature
ASPNETCORE_HTTP_PORTS=8080;8081  |  ASPNETCORE_HTTPS_PORTS=8443  |  --http_ports 8080
  • Added in .NET ASP.NET Core 8.0

Parameters

ParameterTypeDescription
$ASPNETCORE_HTTP_PORTSoptionalstring (ports separated by ;)= unset (8080 in the official .NET 8+ container images)Plain HTTP ports bound on all interfaces. Host config key http_ports.
$ASPNETCORE_HTTPS_PORTSoptionalstring (ports separated by ;)= unsetHTTPS ports bound on all interfaces using the default certificate. Host config key https_ports.

Return value

stringA semicolon-separated list of ports. Kestrel binds each one on all IPv4 and IPv6 interfaces (shown as http://[::]:8080). HTTPS_PORTS does the same for HTTPS and needs a certificate.

Errors

A port already in use fails StartAsync with System.IO.IOException (address already in use). HTTPS ports without a usable certificate fail startup with InvalidOperationException.

Changelog

  • PHP ASP.NET Core 8.0Added HTTP_PORTS and HTTPS_PORTS; the official images switched from ASPNETCORE_URLS=http://+:80 to ASPNETCORE_HTTP_PORTS=8080 so they can run as the non-root app user.

ASPNETCORE_HTTP_PORTS is the simplest way to say where Kestrel listens: a list of port numbers, nothing else. Each port is bound on every interface, IPv4 and IPv6, which is what a container needs because traffic arrives on the pod or container IP, never on localhost. ASPNETCORE_HTTPS_PORTS is the HTTPS twin. Both map to host configuration keys (http_ports, https_ports), so --http_ports 8080 on the command line works too.

Why containers listen on 8080

Since .NET 8 the official mcr.microsoft.com/dotnet/aspnet images set ASPNETCORE_HTTP_PORTS=8080 and ship a non-root user app (UID 1654, $APP_UID). Ports below 1024 need root or a capability on Linux, so the old default of port 80 conflicted with running as non-root. If you upgrade an image from .NET 6 or 7 and keep a Service or compose file that targets port 80, the container starts fine and every request fails with connection refused.

Precedence

SettingWins overWarning logged when it overrides
Kestrel:Endpoints config or ConfigureKestrel(o => o.Listen...)Everything belowOverriding address(es) ... Binding to endpoints defined via IConfiguration and/or UseKestrel() instead.
ASPNETCORE_URLS, --urls, UseUrls, app.UrlsHTTP_PORTS, HTTPS_PORTSOverriding HTTP_PORTS '8080' and HTTPS_PORTS ''. Binding to values defined by URLS instead.
ASPNETCORE_HTTP_PORTS / HTTPS_PORTSThe default
Nothing setListens on http://localhost:5000

The common production mistake is a leftover ASPNETCORE_URLS=http://+:80 in a Dockerfile or Helm chart: it silently beats the image's HTTP_PORTS=8080. What happens next depends on the runtime. Docker (20.10 and later) lets unprivileged processes bind low ports inside the container, so the app quietly listens on 80 while the port mapping or Service targets 8080, and every request gets connection refused. On container runtimes that keep the privileged-port restriction, the non-root process cannot bind port 80 and startup fails with a permission error. Search the logs for the "Overriding" warnings above; they tell you exactly which setting won.

Choosing

Use ASPNETCORE_HTTP_PORTS when the only question is the port: containers, Kubernetes, App Service. Use ASPNETCORE_URLS or Kestrel endpoints when you need a specific interface (bind 127.0.0.1 for a sidecar-only admin port) or per-endpoint protocols and certificates. Terminating TLS at the ingress and serving plain HTTP on 8080 inside the cluster is the usual choice; then leave HTTPS_PORTS unset. With no HTTPS port known, UseHttpsRedirection only logs "Failed to determine the https port for redirect" and does nothing. Once an HTTPS port is configured (for example ASPNETCORE_HTTPS_PORT=443), it redirects every request it sees as HTTP, and without forwarded headers that is every request, so clients loop. Listing two ports does not split routes either: every endpoint answers on every port unless you restrict it.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Run the API as the non-root app user on port 8080 in the official aspnet:10.0 image without any URL configuration
  • Bind traffic on 8080 and metrics on 9090, reject /metrics unless Connection.LocalPort is 9090, and let a NetworkPolicy admit only 8080 from the ingress
  • Find why a .NET 6 to .NET 10 upgrade broke the Service: the image moved from port 80 to 8080 while targetPort stayed 80

Frequently asked questions

Why does my container listen on 8080 and not 80?

Since .NET 8 the official images set ASPNETCORE_HTTP_PORTS=8080 so the app can run as the non-root user app; ports below 1024 need root. Point your Service targetPort or compose mapping at 8080.

Does ASPNETCORE_URLS override ASPNETCORE_HTTP_PORTS?

Yes. Any URLS value (environment variable, --urls, UseUrls) replaces HTTP_PORTS and HTTPS_PORTS and logs a warning, and Kestrel endpoint configuration replaces both.

Can HTTP_PORTS bind only to localhost?

No. It always binds all interfaces. Use ASPNETCORE_URLS=http://127.0.0.1:8080 or a Kestrel endpoint when you need a specific address.

More examples

Dockerfile that keeps the image default

<code>EXPOSE</code> is documentation only; what matters is that nothing sets <code>ASPNETCORE_URLS</code>. Running as <code>$APP_UID</code> (1654) works because 8080 is above 1024. To change the port, set <code>ASPNETCORE_HTTP_PORTS</code> rather than adding a URL.

Example · dockerfile
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish src/Orders.Api/Orders.Api.csproj -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
# The base image already sets ASPNETCORE_HTTP_PORTS=8080. Do not add ASPNETCORE_URLS.
EXPOSE 8080
ENTRYPOINT ["dotnet", "Orders.Api.dll"]

Compose and Kubernetes agreeing on the port

Two ports are bound here, 8080 for traffic and 9090 for health and metrics, but binding a port does not partition routes: every endpoint answers on both, so the API is reachable on 9090 and the health endpoint on 8080. To make a network policy meaningful, reject internal routes unless <code>HttpContext.Connection.LocalPort</code> is 9090 (<code>RequireHost("*:9090")</code> matches the client-supplied Host header, so it is not a security boundary). Quote the port list because it is a string. None of the official runtime, aspnet or chiseled images ship curl or wget: install curl in a derived image, ship a small health-check executable, or on Kubernetes use <code>httpGet</code> probes, which need no tools in the image.

Example · yaml
services:
  orders-api:
    image: registry.example.com/orders-api:1.8.2
    environment:
      ASPNETCORE_ENVIRONMENT: Staging
      ASPNETCORE_HTTP_PORTS: "8080;9090"
    ports:
      - "5000:8080"
    # curl is not in any official aspnet runtime image: this image
    # installs it in its Dockerfile (apt-get install curl on noble)
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9090/health/ready"]
      interval: 10s

Reading which addresses were bound

<code>IServerAddressesFeature</code> holds the addresses Kestrel actually bound, after all precedence rules ran. Logging it once at startup makes a wrong port obvious in the first log line instead of after a failed readiness probe.

Example · csharp
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/orders/{id:int}", (int id) => TypedResults.Ok(new { id, status = "shipped" }));

app.Lifetime.ApplicationStarted.Register(() =>
{
    var server = app.Services.GetRequiredService<IServer>();
    var addresses = server.Features.Get<IServerAddressesFeature>()?.Addresses ?? [];
    app.Logger.LogInformation("Listening on {Addresses}", string.Join(", ", addresses));
});

app.Run();

Discussion

  • Be the first to comment on this lesson.