.NET Core hosted on subdirectories in Nginx

 January 28, 2018


I have a few apps. running on subdirectories at https://labs.billbogaiv.com/. Each app. runs on a separate port and Nginx is setup to proxy requests to each respective app. The trouble begins when those apps. generate links for things like CSS or images. Those apps. don't inherently know they are running on a subdirectory, so the resulting links are broken from the viewpoint of the browser. Here's a simplified setup to demonstrate this behavior:

# nginx.conf
location /app/ {
    proxy_pass http://localhost:5000;
}
> dotnet run --urls=http://localhost:5000

Now, if I hit the app. directly for an image like GET http://localhost:5000/images/example.png, I'll get the image. But, if I use the public-facing URI of GET https://example.com/app/images/example.png, then I'll get a 404. This is because Nginx includes the app/ part of the URI when proxying the request to the app. But, we can get around this by adding a trailing / to our Nginx config: proxy_pass http://localhost:5000/;. This subtle change tells Nginx to remove the app/ from the request prior to passing to the proxy. However, the app. will still internally generate invalid links when requested by the browser (because the app. still doesn't know it's running on a subdirectory).

Example

Nginx's sub_filter is another potential option, but it's pretty fragile since you need to know all possible types of links on your page. And, it won't help in situations where a form-tag doesn't have an action-attribute.

.NET Core 2.0 and UsePathBase

Back in pre-2.0 days, you could tell the app. pretty easily that it should prepend a path when generating links via dotnet run --urls=http://localhost:5000/app/. However, with 2.0, this logic was pulled out of Kestrel and put into middleware. Trying that same command now results in an exeption: System.InvalidOperationException: A path base can only be configured using IApplicationBuilder.UsePathBase().

😠😠😠

Following the message's suggestion, adding the middleware to Startup will make the runtime happy:

// Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UsePathBase("/app"); // DON'T FORGET THE LEADING SLASH!
    app.UseStaticFiles();
}

Without the leading slash, expect to receive another exception: System.ArgumentException: The path in 'value' must start with '/'

The interesting thing about the middleware's current form is requests to both http://localhost:5000/images/example.png and http://localhost:5000/app/images/example.png work. But, hard-coding the path still feels weird to me because my app. shouldn't need to know about my server setup. To appease my inner critic, I'll use a config. setting that I can pass via the command-line:

// Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        // This is no longer needed in .NET 2.1
        // Ref. https://github.com/aspnet/MetaPackages/issues/221
        var config = new ConfigurationBuilder()
            .AddCommandLine(args)
            .Build();

        WebHost.CreateDefaultBuilder(args)
            .UseConfiguration(config)
            .UseStartup<Startup>()
            .Build()
            .Run();
    }
}
// Startup.cs
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        this.configuration = configuration;
    }

    private readonly IConfiguration configuration;

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UsePathBase(configuration["pathBase"]);
        app.UseStaticFiles();
    }
}
> dotnet run --urls=http://localhost:5000 --pathBase=/app

The middleware ignores empty parameters, so no problems for development-testing and not using the pathBase-argument.

Maybe .NET Core > 2.1 will handle this automatically?

There is an issue, https://github.com/aspnet/Hosting/issues/1120, which should just make things works similar to the pre-2.0 days. It has a 2.1 milestone, but I'm not sure it's ready or if it's still being discussed.