The "subfolder problem", OR, "why can't I reverse proxy my app into a subfolder?"

Have you ever tried to reverse proxy an app into its own little subfolder of your domain name?

Makes things neat, doesn’t it? Using example.com/foo/ for one app, example.com/bar/ for another. If you’re coming here from one of the selfhosted communities, you might be thinking along the lines of example.com/sonarr/, example.com/radarr/ etc.

Chances are, you’ve tried some configuration along these lines:

example.com {
  redir /sonarr /sonarr/
  handle_path /sonarr/* {
    reverse_proxy localhost:8989
  }
}

But then disaster strikes, and you get a very poorly formatted page! Maybe links don’t work at all. Maybe you just get redirected straight away to a blank page. Images, style sheets, and scripts don’t work. If you’re struggling with this issue, read on!

Understanding the problem


Firstly, know that this isn’t something unique to Caddy. Every single reverse proxy server you’d care to name will have this inherent problem (unless they have some seriously advanced logic built in to handle it!). That’s because this is a logical problem, not strictly a bug or issue with Caddy or the app itself.

Almost all HTTP applications are written under the assumption that they are serving requests from the web root. A request to example.com/ would be a request for the web root - it just means the highest point in the path hierarchy, the first path element, i.e. not a subfolder.

When you request a website, you usually get a HTML document in response. The HTML directs your browser to additional elements; static assets like images, style sheets, and scripts it uses to further render out the web page. Because it’s assuming it has the web root, it might give your browser links like /images/stock.jpg and /css/site.css. When you open your browser and access your application directly, that’s fine.

But when we put a reverse proxy in the middle, and pen it into a subfolder, we might run into a problem. You’re proxying your application from /foo/. When you send the request to Caddy, and you’re browsing to example.com/foo/, you get a HTML document just like before. But the document still has links to /images/stock.jpg and /css/site.css. Your browser follows those links and makes those requests to Caddy. But Caddy doesn’t know how to handle requests for /images/ or /css/! You’ve only configured the reverse proxy for /foo/. And now our asset links are broken and the page is, too.

Redirects are another problem, as well. They don’t happen in HTML, usually; they happen in HTTP headers (specifically, Location). These, too, are unlikely to properly reference the subfolder you want.

Fixing the problem


Ultimately, to fix this issue, all links and redirects must match up to valid URLs that Caddy knows how to handle. Something’s gotta give; either the HTTP application must correct its URLs to add the subfolder you want, or you need to configure Caddy to let the app have the web root it wants.

Option 1: Upstream

Some apps, like Sonarr, are programmed with the assumption that they might be reverse proxied into a subfolder. There is a helpful option in the application’s settings - commonly referred to as a “URL Base” - which will add your subfolder as a prefix to all of its links/headers. If the app you’re proxying has a feature like this, hurrah! Set it and forget it. If not, this solution isn’t an option.

Option 2: In Caddy

A. By giving up the web root

You can use a subdomain instead, e.g. sonarr.example.com, and proxy the entire web root. This configuration precludes the issue described above and will work for absolutely any HTTP application.

B. By HTML filtering/header rewriting (advanced)

Alternately, you could try to use Caddy as a filtering layer and intercept those incorrect links as they come downstream from your HTTP application.

A Caddy 2 module has been completed and is in the process of being published that can perform replacements in response bodies: GitHub - caddyserver/replace-response: Caddy module that performs replacements in response bodies

Using that plugin involves defining rules that Caddy uses to alter the HTML document it receives from the upstream HTTP application before sending it to the client. In this way, the client would receive the correct links in HTML.

Rewriting headers from upstream can be achieved already using Caddy’s reverse_proxy directive which changes headers only to or from the backend (or, to change any/all headers, you can use the header directive). In this way, you can fix Location redirects as well. Combined, if carefully executed, these two strategies can produce a neatly contained HTTP application inside a subfolder without requiring the app to explicitly support this behaviour.

25 Likes

A post was split to a new topic: Alias directive like in NGINX?

A post was split to a new topic: Treat a domain and subfolder as one single domain for upstream

the subfolder mentioned by you actually concerns the Context Path of a backend web server. As long as all your backend web servers have unique Context-Path respectively, a reverse proxy such as Caddy, F5 BigIP has no problem to support context-path based routing.