An ASP.NET Core app can be told where to listen in at least five places, and when two of them disagree the winner is not always the one you expect. This post creates an empty web app on .NET 10, runs every way of setting the port one at a time, then sets them against each other, and quotes the "Now listening on" line from each run. Read it once and the "why is it on 5000?" question goes away for good.

Do it yourself
  1. dotnet new web -n PortLab and open Properties\launchSettings.json to see the ports the template chose for you
  2. dotnet run, then dotnet run --no-launch-profile: the port and the environment both change
  3. dotnet run --urls http://localhost:15700, then the same with ASPNETCORE_URLS set in the shell
  4. Set both at once and see which wins; then add a Kestrel:Endpoints section to appsettings.json and watch the warning
  5. Bind to localhost, 0.0.0.0 and * and test each with curl on 127.0.0.1, [::1] and your LAN address
  6. dotnet publish -c Release -o out and run the dll: no launchSettings, port 5000, Production

The template's random ports

dotnet new web wrote a launchSettings.json with two profiles, and the ports in it are picked at random when the project is created; the documentation says HTTP from 5000 to 5300 and HTTPS from 7000 to 7300. This project got:

      "applicationUrl": "http://localhost:5216",
      "applicationUrl": "https://localhost:7129;http://localhost:5216",

Both profiles also set ASPNETCORE_ENVIRONMENT to Development. That file is for dotnet run and the IDE only: the documentation says it is used on the local development machine and is not deployed, and the publish step at the end confirms it.

Five ways, one at a time

CommandNow listening onHosting environment
dotnet runhttp://localhost:5216Development
dotnet run --launch-profile httpshttps://localhost:7129 and http://localhost:5216Development
dotnet run --no-launch-profilehttp://localhost:5000Production
dotnet run --urls http://localhost:15700http://localhost:15700Development
$env:ASPNETCORE_URLS='http://localhost:15701'; dotnet run --no-launch-profilehttp://localhost:15701Production

Two things in that table surprise people. Without a profile the app is in Production, not Development, because the environment came from the profile too; that is the whole story of the no-launch-profile post. And --urls replaced the profile's port but kept its environment: the profile's variables still applied, only its URL was overridden.

When they disagree

With ASPNETCORE_URLS set to 15701 in the shell for every run:

  • --urls http://localhost:15702 with the profile: 15702 listened. The switch beat both the variable and the profile.
  • --urls with --no-launch-profile: 15702 again.
  • The profile and no switch: 5216 listened. The profile's applicationUrl beat the variable set in the shell, which matches the documentation's line that environment values in launchSettings.json override values set by the system environment.

Then appsettings.json got a Kestrel section with an Endpoints:Http:Url of 15703. It won in all four runs, against nothing, against the variable, against --urls and against the profile, and whenever another source was present Kestrel said so:

warn: Microsoft.AspNetCore.Server.Kestrel[0]
      Overriding address(es) 'http://localhost:15701'. Binding to endpoints defined via IConfiguration and/or UseKestrel() instead.

The quoted address changed to match whatever lost. Finally, builder.WebHost.UseUrls("http://localhost:15704") in Program.cs beat --urls and beat the profile, and printed no warning at all; the other value was simply not used. The pair not tested against each other is code versus the Kestrel configuration section; the documentation says endpoints configured in code with Listen override UseUrls, and that the configuration section replaces the top-level URLs rather than adding to them. The practical ladder from the runs: code first, then the Kestrel section with its warning, then --urls, then the launch profile, then ASPNETCORE_URLS, then the 5000 default.

localhost, 0.0.0.0 and *

URL givenSockets127.0.0.1[::1]LAN address
http://localhost:15707127.0.0.1 and ::1200200refused
http://0.0.0.0:157060.0.0.0 only200refused200
http://*:15708one IPv6 any-address socket200200200

The wildcard is logged as the IPv6 any-address, not as a star:

Now listening on: http://[::]:15708

localhost is the safe default for development: reachable from the machine, refused from the network. 0.0.0.0 is IPv4 only, which is why [::1] refused it; the documentation says anything not recognised as an address or localhost binds every IPv4 and IPv6 address, which is what * did. Binding to the network on Windows also brings up the "Windows Security Alert" dialog the first time; answering it creates inbound firewall rules for that executable that stay behind after the test, so look in Windows Security, Firewall, "Allow an app" when you are done experimenting. The LAN tests here ran from the same PC; a second device was not used.

After publish

dotnet publish -c Release -o out produced a folder without launchSettings.json. Running dotnet out\PortLab.dll from the project folder listened on http://localhost:5000 in Production, and one more thing worth knowing: the content root was the current directory, not the out folder. --urls still works on the published dll, and in production the usual choice is ASPNETCORE_URLS in the service definition or the Kestrel section in appsettings.Production.json.

Two apps on one port

A second copy of the published app on a port the first one holds does not start:

Unhandled exception. System.IO.IOException: Failed to bind to address http://127.0.0.1:15710: address already in use.

The message names 127.0.0.1 although the URL said localhost, because Kestrel binds the loopback interfaces one by one. The process exited on its own and the first copy kept serving. The Fixes post for it shows how to find the process that owns the port.

Frequently asked

Why does my ASP.NET Core app run on port 5000?
Because no endpoint was configured. The template's launchSettings.json gives dotnet run a random port between 5000 and 5300, but that file is used only by dotnet run and the IDE; with --no-launch-profile, or after publish, Kestrel falls back to http://localhost:5000, and the environment falls back to Production for the same reason.
How do I change the port of an ASP.NET Core app?
For a one-off run, dotnet run --urls http://localhost:15700, or set ASPNETCORE_URLS before starting the app. For a project, edit applicationUrl in Properties/launchSettings.json. For a deployment, set ASPNETCORE_URLS in the service definition or add a Kestrel:Endpoints section to appsettings.json; that section overrides every other source and logs a warning when it does.
Which wins: ASPNETCORE_URLS, --urls or launchSettings?
Run on .NET 10: a UseUrls call in code beat everything tested, the Kestrel section in appsettings beat the rest with a warning, --urls beat both the launch profile and the variable, and the launch profile's applicationUrl beat ASPNETCORE_URLS set in the shell, because values in launchSettings.json override the system environment.

The HTTPS side of the same question, a missing development certificate, is in its own post, and the series From Prompt to Production deploys an app where these settings start to matter.