
- Use handle_path rather than manual rewrites to strip subpath prefixes automatically.
- Upstream apps need their public base URL configured properly.
- Rewrite Location headers with header_down.
Caddy reverse proxy subpath routing basics
Deploying a caddy reverse proxy subpath lets teams route internal applications behind one unified domain name. For example, route public calls for /api to an internal app. Send root traffic to a web frontend. This setup saves IP addresses. It also cuts down TLS certificate needs. Yet basic setups often fail. Backends rarely expect a path prefix in front of their routes.
Request routing relies on strict directive order and path matchers. You must know how path prefixes work. That knowledge keeps upstreams healthy and stops bad loops.
Using handle_path instead of manual prefix rewrites
The cleanest way to forward traffic from a subpath is the built-in handle_path directive. In older web servers, removing a prefix takes complex rewrite rules. It often needs tricky slash syntax. The modern server makes this easy. It puts path matching and prefix stripping into a single block.
With handle_path, the server tests the URL. It cuts off the matched prefix right away. Then it runs any directives placed inside the block. The target app sees the call as if it hit the root path directly.
example.com {
# Strip /api and pass remaining path to upstream
handle_path /api/* {
reverse_proxy localhost:8080
}
# Default fallback for all other requests
handle {
respond "Frontend root application" 200
}
}
Say a client visits /api/users. When configured with handle_path, the reverse proxy immediately drops /api and forwards the remaining request path /users directly to port 8080. What if you use standard handle instead? The proxy forwards /api/users intact. Most root-based apps return an instant 404 error when that happens.
Comparing Caddy subpath routing directives
Picking the right block stops routing bugs early. Each directive handles URI updates in its own way. The table below shows how standard routing blocks treat subpath traffic.
| Routing directive pattern | Prefix stripping | Mutual exclusivity | Best operational use case |
|---|---|---|---|
| handle_path /prefix/* | Automatic and implicit | Yes, mutually exclusive with other handles | Routing independent services that expect root paths |
| handle /prefix/* | None (path preserved) | Yes, mutually exclusive with other handles | Services designed to run under an explicit subpath |
| route { uri strip_prefix /prefix ... } | Manual via directive | No, executes sequentially in defined order | Complex pipelines requiring ordered middleware |
| reverse_proxy /prefix/* upstream:port | None (path preserved) | No, standard directive ordering rules apply | Quick single-purpose proxies without fallback handlers |
Fixing broken relative assets and base URL issues
Even with good proxy rules, web dashboards may still break. Styles fail. Images vanish. Screens turn white. Why does this happen? Web pages often use absolute asset links like /static/bundle.js.
A browser opens /app/ in a tab. An absolute link tells it to load /static/bundle.js from the site root. But your proxy only sends /app/* to the backend app. As a result, the root server gives a 404 error for the asset. Rewriting HTML responses dynamically on the edge is fragile and slow. You must configure the backend app directly.
Most web frameworks have an option for this. Look for BASE_URL, URL_PREFIX, or PUBLIC_URL. Adding that value makes the app add /app/ to all asset paths and links.
Preventing redirect loops with Location header rewrites
Another common trap is the infinite redirect loop. Many backend apps want a trailing slash on directories. This behavior triggers problems. When visitors request /app without a slash, upstream applications send 301 redirects to the root. Backends lack awareness of the edge proxy. They send a Location header pointing straight to root. Clients follow this redirect blindly. The subpath drops. Use header_down in your proxy block to stop this loop. It intercepts redirect headers before delivery.
example.com {
handle_path /dashboard/* {
reverse_proxy localhost:9000 {
# Rewrite upstream root redirects to preserve the subpath
header_down Location ^/$ /dashboard/
header_down Location ^/(.*) /dashboard/$1
}
}
}
It inspects root paths in Location headers and rewrites them so users stay inside /dashboard.
Production Docker Compose subpath example
Running app stacks behind a central proxy often means linking several containers together. Below is a tested configuration for an API service and a metrics dashboard.
{
email admin@example.com
}
example.com {
# Route API service with automatic prefix stripping
handle_path /api/* {
reverse_proxy api-service:5000 {
header_up X-Forwarded-Prefix /api
}
}
# Route metrics service preserving full path
handle /grafana/* {
reverse_proxy grafana:3000
}
# Serve static assets or frontend from root
handle {
root * /var/www/html
file_server
}
}
Check the X-Forwarded-Prefix header on the API proxy. This header helps frameworks like FastAPI build valid links without changing internal routes.
For more architectural guidance on proxy choices, read our comparison of Caddy vs Nginx reverse proxy performance. If you work with edge access control, see our guide on basic auth vs bearer tokens. You can also explore our analysis of DNS over HTTPS vs DNS over TLS for secure internal name resolution. For transport security, review our overview of TLS 1.3 vs TLS 1.2 handshake speed, and inspect our checklist on proving TLS certificate renewal on production edge nodes.
Frequently asked questions about Caddy subpath proxies
What is the difference between handle and handle_path in Caddy?
The handle directive preserves the request URI path exactly as received. By contrast, handle_path strips prefixes.
Why do web applications return 404 for assets when proxied under a subpath?
Most upstream applications generate absolute asset URLs starting with a root slash. When proxied under a subpath without an updated base URL, the browser requests assets from the root domain.
How do you rewrite upstream redirect Location headers in Caddy?
You use the header_down response modifier inside reverse_proxy to replace root redirect paths with your target subpath prefix.
Do you need trailing slashes on Caddy subpath matchers?
Yes, standard Caddy path matchers are exact matches. Using a wildcard like /api/* allows Caddy to match all child paths reliably.
Sources and technical documentation
- Caddy Server: reverse_proxy Directive Documentation.
- Caddy Server: handle_path Directive and Prefix Stripping.
- Caddy Server: URI Rewrite and strip_prefix Documentation.