Skip to content

Instantly share code, notes, and snippets.

@hallojoe
Last active August 5, 2026 11:18
Show Gist options
  • Select an option

  • Save hallojoe/017981ebb292a4984fcb01b6796f1c0d to your computer and use it in GitHub Desktop.

Select an option

Save hallojoe/017981ebb292a4984fcb01b6796f1c0d to your computer and use it in GitHub Desktop.
Set up a reverse proxy for your .NET 10 projects

Set up a reverse proxy for your .NET 10 projects

Use YARP to expose multiple local ASP.NET applications under friendly hostnames instead of localhost ports.

For example, your solution might contain several applications running on different ports:

  • https://localhost:37101
  • https://localhost:37102
  • https://localhost:37103

Instead of accessing each application by port, you can expose them through local hostnames:

  • https://localhost:37101https://cm.dev.localhost
  • https://localhost:37102https://cd1.dev.localhost
  • https://localhost:37103https://cd2.dev.localhost

Why use .dev.localhost?

Starting with .NET 10, the ASP.NET Core development certificate includes support for *.dev.localhost. This means any hostname ending with .dev.localhost is trusted by the default development certificate after it has been installed and trusted on your machine.

This allows you to use friendly HTTPS hostnames such as https://cm.dev.localhost without creating custom certificates or configuring a local certificate authority.

Prerequisites

Ensure the ASP.NET Core development certificate is installed and trusted:

dotnet dev-certs https --trust

This only needs to be done once per machine.

Configure local hostnames

Domains ending with .localhost normally resolve to the local loopback address automatically, so this step is often unnecessary. If your environment requires it, add the following entries to your hosts file (C:\Windows\System32\drivers\etc\hosts):

127.0.0.1 cm.dev.localhost
127.0.0.1 cd1.dev.localhost
127.0.0.1 cd2.dev.localhost

Create the reverse proxy

Create a new empty ASP.NET project and install YARP:

dotnet package add Yarp.ReverseProxy

Configure the reverse proxy to listen on HTTPS port 443 (https://localhost:443) and forward incoming requests to your local applications.

The example includes the following files:

  • ReverseProxy.Program.cs
  • ReverseProxy.launchSettings.json
  • ReverseProxy.appsettings.json

Configure the destination applications

Each application behind the proxy must be configured to process forwarded headers so it can determine the original host and HTTPS scheme.

The example includes:

  • AspNetSites.Program.cs

That's it

Your local applications can now be accessed using friendly hostnames instead of localhost ports.

For example, if these were separate Umbraco instances:

  • https://cm.dev.localhost/umbraco/https://localhost:37101/umbraco/
  • https://cd1.dev.localhost/page-1/https://localhost:37102/page-1/
  • https://cd2.dev.localhost/page-2/https://localhost:37103/page-2/

The request flow looks like this:

flowchart TD
    A["Browser<br/>https://cm.dev.localhost/page-1"]
    B["YARP Reverse Proxy<br/>HTTPS :443"]

    C{"Match Host header"}

    D["cm.dev.localhost<br/>→ https://localhost:37101"]
    E["cd1.dev.localhost<br/>→ https://localhost:37102"]
    F["cd2.dev.localhost<br/>→ https://localhost:37103"]

    A --> B
    B --> C
    C --> D
    C --> E
    C --> F
Loading

YARP accepts the incoming request on port 443, matches the request hostname (cm.dev.localhost) against its configured routes, and forwards the request to the appropriate local application while preserving the original host and protocol through forwarded headers.

Bonus: Umbraco configuration

If you're using this setup with Umbraco, you should also configure the public URLs that Umbraco should use when generating links and handling authentication.

{
  "Umbraco": {
    "CMS": {
      "Security": {
        "BackOfficeHost": "https://cm.dev.localhost"
      },
      "WebRouting": {
        "UmbracoApplicationUrl": "https://cd1.dev.localhost/"
      }
    }
  }
}

BackOfficeHost

BackOfficeHost tells Umbraco which public URL hosts the backoffice.

This is especially important when using a reverse proxy, as developers often access the site through https://cm.dev.localhost, while YARP forwards requests internally to something like https://localhost:37101.

Without this setting, authentication can fail with errors such as:

error: invalid_request
error_description: The specified 'redirect_uri' is not valid for this client application.

UmbracoApplicationUrl

UmbracoApplicationUrl defines the public URL that Umbraco uses when generating absolute URLs, redirects, canonical links, and other application URLs.

When running behind a reverse proxy, this should point to the public hostname that users access—not the internal localhost address.

Also see: UmbracoApplicationUrl

Multi-site example

A common setup is:

Purpose Public URL Internal URL
Backoffice https://cm.dev.localhost https://localhost:37101
Website 1 https://cd1.dev.localhost https://localhost:37102
Website 2 https://cd2.dev.localhost https://localhost:37103

YARP handles routing from the public hostnames to the internal applications, while Umbraco continues to generate URLs and authentication callbacks using the public hostnames.

Further reading

If you'd like to learn more about YARP and ASP.NET reverse proxies, these resources are a great place to start:

var builder = WebApplication.CreateBuilder(args);
...
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedHost |
ForwardedHeaders.XForwardedProto;
options.KnownProxies.Add(IPAddress.Loopback);
options.KnownProxies.Add(IPAddress.IPv6Loopback);
});
...
app.UseForwardedHeaders();
...
await app.RunAsync();
127.0.0.1 cm.dev.localhost
127.0.0.1 cd1.dev.localhost
127.0.0.1 cd2.dev.localhost
{
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"cm": {
"ClusterId": "cm",
"Match": {
"Hosts": [
"cm.dev.localhost"
],
"Path": "{**catch-all}"
}
},
"cd1": {
"ClusterId": "cd1",
"Match": {
"Hosts": [
"cd1.dev.localhost"
],
"Path": "{**catch-all}"
}
},
"cd2": {
"ClusterId": "cd2",
"Match": {
"Hosts": [
"cd2.dev.localhost"
],
"Path": "{**catch-all}"
}
}
},
"Clusters": {
"cm": {
"Destinations": {
"default": {
"Address": "https://localhost:37101/"
}
}
},
"cd1": {
"Destinations": {
"default": {
"Address": "https://localhost:37102/"
}
}
},
"cd2": {
"Destinations": {
"default": {
"Address": "https://localhost:37103/"
}
}
}
}
}
}
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"LocalReverseProxy": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:443",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
// Program.cs for reverse proxy project
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddReverseProxy()
.LoadFromConfig(
builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Yarp.ReverseProxy" />
</ItemGroup>
</Project>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment