What Changed in Hosting from .NET 6 to .NET 10: Port 8080, HTTP_PORTS, KnownIPNetworks and More
Since .NET 8, ASP.NET Core images listen on 8080 instead of 80 and ship a ready-made app user, but the default user is still root; on .NET 10 the images are Ubuntu-based and KnownNetworks and WebHost are obsolete. With real runs we see the old -p 8000:80 command give a connection reset, why a non-root port 80 works on Docker but may not elsewhere, the traps in old tutorials and the .NET 11 breaking changes that touch hosting.
Version note: The examples in this post were run with the .NET 10 SDK (Software Development Kit) 10.0.402 and the 6.0, 8.0 and 10.0 tags of the mcr.microsoft.com/dotnet/aspnet images on Docker 29. .NET 11's GA (General Availability) date is 10 November 2026; it wasn't released yet when this post was written, and the .NET 11 items rely only on the breaking change pages Microsoft has published.
Guess First
The Bookstore team uses the Dockerfile and docker run command from a blog post written in 2021: docker run -p 18581:80 .... The only difference is that the image is now mcr.microsoft.com/dotnet/aspnet:10.0. The container comes up and there's no error in the log.
Guess: what does curl http://localhost:18581/api/books/1 return? And if the team adds --user app to close a "container runs as root" finding from a security scan and configures the app to listen on 80, what happens?
From 80 to 8080 in Containers
The change that left the deepest mark on hosting from .NET 6 to 10 is one environment variable. The defaults of three images:
$ for v in 6.0 8.0 10.0; do docker run --rm mcr.microsoft.com/dotnet/aspnet:$v sh -c '...'; done
== aspnet:6.0
ASPNETCORE_URLS=http://+:80
user: root (0)
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
id: 'app': no such user
== aspnet:8.0
APP_UID=1654
ASPNETCORE_HTTP_PORTS=8080
user: root (0)
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
uid=1654(app) gid=1654(app) groups=1654(app)
== aspnet:10.0
APP_UID=1654
ASPNETCORE_HTTP_PORTS=8080
user: root (0)
PRETTY_NAME="Ubuntu 24.04.5 LTS"
uid=1654(app) gid=1654(app) groups=1654(app)
Since .NET 8 the images come with ASPNETCORE_HTTP_PORTS=8080 instead of ASPNETCORE_URLS=http://+:80. The answer to the guess:
$ docker run -d -p 18581:80 ... mcr.microsoft.com/dotnet/aspnet:10.0 dotnet Bookstore.Api.dll
$ curl -sS http://localhost:18581/api/books/1
curl: (56) Recv failure: Connection reset by peer
$ docker run -d -p 18581:8080 ... mcr.microsoft.com/dotnet/aspnet:10.0 dotnet Bookstore.Api.dll
$ curl -sS http://localhost:18581/api/books/1
{"id":1,"title":"Dune","author":"Frank Herbert","price":12.50}
Nobody inside the container listens on 80; Docker accepts the connection and closes it. The example in Microsoft's breaking change document is exactly this. One more detail: the old WebHost.CreateDefaultBuilder API (Application Programming Interface) doesn't recognise the new ASPNETCORE_HTTP_PORTS, and since ASPNETCORE_URLS isn't set any more, such apps fall back to http://localhost:5000 in a container. The same as the localhost trap from the second post: up inside the container, unreachable from outside.
The Non-Root app User: Opt-In
The second line that stands out in the output is APP_UID=1654 and the app user. A common misreading is that since .NET 8 containers run without root by default. As the output shows, the default user is root in all three images; .NET 8 only added a ready-made app user, and using it is your decision (USER $APP_UID in the Dockerfile or docker run --user app). That's also one of the reasons for moving to 8080: traditionally only root can open ports below 1024.
$ docker run -d --user app ... mcr.microsoft.com/dotnet/aspnet:10.0 dotnet Bookstore.Api.dll
$ docker exec webhwork-port ps -o user,pid,args
USER PID COMMAND
app 1 dotnet Bookstore.Api.dll
$ docker logs webhwork-port
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
Storing keys in a directory '/home/app/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. ...
Now listening on: http://[::]:8080
The second half of the guess came out unexpectedly: --user app with ASPNETCORE_HTTP_PORTS=80 worked on Docker. The reason is Docker itself; in containers it sets net.ipv4.ip_unprivileged_port_start to 0, so non-root processes can open 80 too:
$ docker run -d --user app -e ASPNETCORE_HTTP_PORTS=80 ... mcr.microsoft.com/dotnet/aspnet:10.0 dotnet Bookstore.Api.dll
Now listening on: http://[::]:80
$ docker exec webhwork-port cat /proc/sys/net/ipv4/ip_unprivileged_port_start
0
$ docker run -d --user app --sysctl net.ipv4.ip_unprivileged_port_start=1024 -e ASPNETCORE_HTTP_PORTS=80 ...
fail: Microsoft.Extensions.Hosting.Internal.Host[11]
Hosting failed to start
System.Net.Sockets.SocketException (13): Permission denied
The same image may fail to start with "Permission denied" on Kubernetes, Podman or a runtime with a different sysctl. Staying on 8080 removes that difference entirely; mapping the external port is the job of the ingress or -p anyway. The rest of container hardening is in the upcoming Docker series.
ASPNETCORE_HTTP_PORTS and Port Precedence
HTTP_PORTS and HTTPS_PORTS came with .NET 8 and sit at the bottom of the precedence order we measured in the second post: the urls key (--urls, ASPNETCORE_URLS) overrides them, and Kestrel:Endpoints and Listen* in code override urls. If an ENV ASPNETCORE_URLS=http://+:80 line is left in an old Dockerfile, it silently overrides the image's 8080 and the app listens on 80 again; the only clue in the log is the Overriding HTTP_PORTS '8080' and HTTPS_PORTS '' warning. Don't mix the two worlds: either delete the old line and move to 8080, or keep it deliberately and write the port mappings accordingly.
.NET 10: Ubuntu-Based Images and Obsolete APIs
In .NET 10 the base of the default images moved from Debian to Ubuntu 24.04; the PRETTY_NAME line above shows it. According to Microsoft's documentation no Debian image is published for .NET 10. For teams installing packages with apt-get install in their Dockerfile, package names and versions may change; in the libmsquic example in the second post we used the Microsoft repository address for Ubuntu 24.04.
On the code side there are two obsolete warnings. The first is KnownNetworks, which we saw in the third post:
Program.cs(9,5): warning ASPDEPR005: 'ForwardedHeadersOptions.KnownNetworks' is obsolete: 'Please use KnownIPNetworks instead. For more information, visit https://aka.ms/aspnet/deprecate/005.' (https://aka.ms/aspnet/deprecate/005)
Program.cs(9,35): warning ASPDEPR005: 'IPNetwork' is obsolete: 'Please use System.Net.IPNetwork instead. For more information, visit https://aka.ms/aspnet/deprecate/005.' (https://aka.ms/aspnet/deprecate/005)
The second is the old hosting model itself. I compiled a Program.cs from the .NET Core 3.1 and .NET 5 era with .NET 10:
// .NET Core 3.1 / .NET 5 era hosting, compiled with .NET 10
using Microsoft.AspNetCore;
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://localhost:5000")
.UseStartup<Startup>()
.Build()
.Run();
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapGet("/api/books/1", () => "Dune"));
}
}
Program.cs(4,1): warning ASPDEPR008: 'WebHost' is obsolete: 'WebHost is obsolete. Use HostBuilder or WebApplicationBuilder instead. For more information, visit https://aka.ms/aspnet/deprecate/008.' (https://aka.ms/aspnet/deprecate/008)
Program.cs(4,1): warning ASPDEPR008: 'IWebHostBuilder.Build()' is obsolete: 'IWebHost is obsolete. Use IHost instead. For more information, visit https://aka.ms/aspnet/deprecate/008.' (https://aka.ms/aspnet/deprecate/008)
$ curl -s http://localhost:5000/api/books/1
Dune
The code compiles and runs; .NET 10 marked WebHostBuilder, IWebHost and WebHost obsolete (code that writes new WebHostBuilder() directly gets ASPDEPR004). Without the using Microsoft.AspNetCore; line the compiler bound WebHost to another, inaccessible type and gave a CS0122 error; that can be confusing when moving old projects. The steps to move this code to WebApplication.CreateBuilder are in the "Reading Old .NET Core 3.1 and .NET 5 Projects: Migrating from Startup.cs to .NET 6" post of the ASP.NET Core Web API series. What matters for hosting is that UseUrls is also subject to the precedence order from the second post, and binding to localhost in a container leads into the same trap.
What Changed on the ANCM and IIS Side
The changes on the IIS (Internet Information Services) side are quieter. In-process hosting has been the default since ASP.NET Core 3.0. Shadow copy arrived experimentally in .NET 6 with the experimentalEnableShadowCopy handler setting and has been supported as enableShadowCopy since .NET 7; if you see the experimental name in a web.config from the .NET 6 era, update it as in the tenth post. ANCM (ASP.NET Core Module) is forward and backward compatible with supported .NET versions; a single current Hosting Bundle carries old and new apps together.
There's also a behaviour change that came with a servicing release: since ASP.NET Core 8.0.17 and 9.0.6, the forwarded headers middleware ignores X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Prefix from unknown proxies too. Arriving without a major version change, through an apt upgrade or an image update, this change showed up as an HTTPS (HTTP Secure) redirection loop in apps whose trusted proxy list was incomplete. The "Unknown proxy" log from the third post is the first place to look here too.
Reading Old Tutorials
Hosting guides live long; the first search result is often from the .NET Core 2.x or 3.1 era. The list of old leftovers we saw in this series:
| In an old tutorial | Today | Post |
|---|---|---|
docker run -p 8000:80 | -p 8000:8080; the image default is 8080 | This post |
ENV ASPNETCORE_URLS=http://+:80 | Unnecessary; ASPNETCORE_HTTP_PORTS is already 8080 | 2 and this post |
Startup.cs, WebHost.CreateDefaultBuilder, UseUrls | WebApplication.CreateBuilder; the old APIs are obsolete | This post, Web API 32 |
KnownNetworks.Add(new IPNetwork(...)) | KnownIPNetworks.Add(System.Net.IPNetwork.Parse(...)) | 3 |
proxy_http_version 1.1 and keepalive required in Nginx | The default since 1.29.7; an upstream block is enough | 5 |
listen 443 ssl http2; | listen 443 ssl; and http2 on; | 7 |
KillSignal=SIGINT | KillSignal=SIGTERM (the systemd default) | 8 |
ExecStart=/usr/local/bin/dotnet ... | /usr/bin/dotnet with the Ubuntu package | 8 |
| "Add the Microsoft package repository first" | On Ubuntu 24.04 and later .NET is in the distribution's repository | 8 |
experimentalEnableShadowCopy | enableShadowCopy (.NET 7+) | 10 |
The quickest way to tell a guide's age is to look for Startup.cs, port 80 and ASPNETCORE_URLS. If one of them is there, compare the port, user and signal settings in the rest of the guide with this table too.
What's Coming with .NET 11
.NET 11 goes GA two days after this post is published; the breaking change list Microsoft has published for .NET 11 has three items that touch hosting. Kestrel now rejects HTTP (Hypertext Transfer Protocol) 2 and 3 requests containing the headers RFC (Request for Comments) 9113 and 9114 consider connection-specific (Connection, Keep-Alive, Transfer-Encoding, Upgrade and others), and closes the connection after an HTTP/1.1 request that contained both Content-Length and Transfer-Encoding; the documentation says the latter also ships in servicing updates to earlier supported versions. Kestrel processing the first HTTP/3 request without waiting for the control stream reduces first-request latency. The response compression middleware adds Vary: Accept-Encoding to responses it doesn't compress too; in apps with an Nginx or CDN (Content Delivery Network) cache in front, that can affect cache keys. Upgrading Bookstore once .NET 11 is GA and measuring these items again with the experiments in this series is a topic for a separate post.
In Production
Test a major version upgrade from the hosting angle too. The app code may compile and the tests may pass; but the image base moving from Debian to Ubuntu, the default port changing, or a forwarded headers tightening in a servicing release only show up with a real proxy and a real container. Make the check script from the eleventh post part of the upgrade pipeline.
Don't ignore obsolete warnings. ASPDEPR005 and ASPDEPR008 are warnings today, but Microsoft can remove obsolete APIs in later versions; .NET 11's breaking change list has items where previously obsoleted APIs were removed. In a project with TreatWarningsAsErrors on, these warnings will stop an upgrade one day; clean them up early.
Common Pitfalls
Using the old docker run -p ...:80 command with the new image. The container is up, the log is clean, the client gets "connection reset".
Thinking .NET 8 runs containers as non-root by default. The default is still root; USER $APP_UID is your job.
Assuming a non-root port 80 that works on Docker will work everywhere. Docker's sysctl default doesn't exist on other runtimes; stay on 8080.
Leaving the old ASPNETCORE_URLS line in the Dockerfile. It overrides the image's 8080 and two different pieces of port information live in the same image.
Assuming servicing updates contain no behaviour changes. The forwarded headers change in 8.0.17 arrived without a major upgrade.
A Small Experiment
★☆☆ Write a Dockerfile based on mcr.microsoft.com/dotnet/aspnet:10.0, add USER $APP_UID and run the app. Where are the Data Protection keys written? What happens when the container is recreated, and how would you apply the fix from the third post in a container?
★★☆ Run the old WebHost.CreateDefaultBuilder sample in a container without setting ASPNETCORE_URLS and look at which address it listens on. Then move the same app to WebApplication.CreateBuilder and verify that it uses the image's ASPNETCORE_HTTP_PORTS.
★★★ When .NET 11 is released, upgrade Bookstore.Api to net11.0 and rerun three experiments from this series: the port precedence from the second post, the upstream keepalive count from the fifth and the HTTP/3 request from the seventh. Which outputs changed?
The series ends here. Bookstore now runs on a Linux VM (virtual machine) behind Nginx with two Kestrel instances under systemd; Angular comes from the same origin; TLS (Transport Layer Security), headers, limits and the health check are in place and all verified with a check script. The next steps are other series' topics: certificate lifecycle in the SSL (Secure Sockets Layer)/TLS Certificates and HTTPS series, zero-downtime deploys in Deployment Strategies, and moving the same app to containers in the Docker and Kubernetes series.