On this page
Deploying applications or static assets in subfolders (such as /blog or /docs) is a common pattern for web architects. However, a frequent pitfall when configuring Caddy is encountering unexpected 404 Not Found errors. This happens because search engine crawlers and regular users request paths that include the subfolder prefix, but the upstream application or file server expects a relative path without that prefix.
This guide explains how to solve this issue by comparing handle and handle_path directives, focusing on path prefix stripping and practical configuration steps.
Prerequisites and Scope
- A running server with Caddy installed and a valid
Caddyfile. - An application or static site deployed under a non-root path prefix (e.g.,
/blog). - Scope is strictly limited to Caddyfile routing and path handling directives.
1. Problem Diagnosis: Why Subfolder Deployments Return 404
When you use a standard handle block in your Caddyfile, Caddy matches the incoming request path but does not modify the request URI.
For instance, if you configure a block for /blog/*, a request to https://example.com/blog/about retains the full URI /blog/about when passed to your upstream application or internal file handler. If your Node.js, Python, or static file directory only knows about /about, it will fail to match the route and return a 404 Not Found error. This broken link prevents search engines from properly indexing your subfolder content.
2. Core Mechanism: handle_path vs uri strip_prefix
To bridge this gap, Caddy provides the handle_path directive. According to the official documentation, handle_path works the same as the handle directive, but it implicitly uses `uri strip_prefix` to strip the matched path prefix from the request URI before passing it to inner blocks handle_path Caddy Documentation.
Key behavioral rules for handle_path:
- It accepts a single path matcher directly in its definition.
- You cannot use named matchers inside a
handle_pathdeclaration. - The path left after stripping always starts with `/`. For example, matching
/prefix/*for a request to/prefixresults in the internal path/handle_path Caddy Documentation.
3. Scenario One: Fixing file_server Static Asset 404s
When serving static files from a subfolder, combining file_server with the site's global root directive can lead to incorrect file lookups if paths aren't stripped. The file_server directive forms file paths by appending the request's URI path to the site's root path file_server Caddy Documentation.
If you deploy a static site in /var/www/blog under the /blog/ prefix, use handle_path to strip the prefix so the file server looks for /index.html instead of /blog/index.html inside your root folder:
example.com {
handle_path /blog/* {
root * /var/www/blog
file_server
}
}Verifying the File Server
Run the following verification command:
curl -I https://example.com/blog/index.htmlExpected Result: HTTP status 200 OK, confirming that Caddy successfully mapped /blog/index.html to /var/www/blog/index.html.
4. Scenario Two: Fixing Reverse Proxy Application 404s
For upstream applications running behind Caddy (such as a backend service listening on port 8080), receiving /blog/api/users instead of /api/users often triggers framework-level routing failures.
Use handle_path to seamlessly strip the /blog prefix before forwarding the traffic:
example.com {
handle_path /blog* {
reverse_proxy localhost:8080
}
}Verifying the Reverse Proxy
Send a test request to the upstream application route:
curl -i https://example.com/blog/statusExpected Result: The upstream application logs and response headers must show it received /status, not /blog/status. The HTTP status code should be 200 OK.
5. Configuration Comparison and Verification Steps
To ensure your routing behaves correctly, follow this step-by-step verification checklist:
- Draft the configuration using
handle_pathinstead ofhandlefor any non-root path matcher. - Reload Caddy using
caddy reload --config /etc/caddy/Caddyfile. - Test with curl to inspect response headers and payload.
- Check upstream logs to verify that the prefix has been cleanly stripped and requests start with
/.
6. Risks, Side Effects, and Rollback
While handle_path offers great convenience, be aware of its side effects:
- Over-stripping risk: Because the directive automatically strips the matched prefix, writing overly broad matchers (like
/*) will strip your entire path down to/, breaking multi-level routing. - Fallback option: If you need granular control over which parts of the URI are stripped or need to preserve specific query parameters alongside complex rewrites, fall back to a standard
handleblock paired with an explicituri strip_prefix /custom-prefixdirective.
Verifying Path Stripping and Static File Resolution
When troubleshooting 404s in subfolder deployments, it is critical to verify that the path prefix is being stripped correctly before the request reaches the static file server or reverse proxy. The handle_path directive implicitly uses uri strip_prefix to remove the matched path prefix, ensuring the remaining path always starts with /. For example, with handle_path /prefix*, a request for /prefix is handled with the path /. This behavior is distinct from handle, which requires explicit uri strip_prefix configuration. To validate this, you can inspect the request path received by your backend or file server. If you are serving static files, ensure that the file_server directive is correctly resolving the stripped path against the site root. Note that file_server enforces canonical URIs, issuing redirects for directories without trailing slashes or files with trailing slashes, unless an internal rewrite modifies the last path element. For a deeper understanding of how handle_path simplifies this process compared to manual prefix stripping, refer to the official documentation: Caddy handle_path directive. Additionally, if your 404 errors persist after verifying path stripping, consider reviewing upstream connection issues or body limits, as detailed in our guide on Fixing Caddy Reverse Proxy 404 and 502 Errors.
Sources
Next steps
Continue with the next useful task; you do not need to read everything at once.