Reverse proxy via paths only shows a white page (but the title works)

1. The problem I’m having:

Trying to run a reverse proxy via handle_path leads to a white page, but the Title-tag is shown, so caddy at least gets some response. Using a subdomain works, but is not what I want.
The curl output further down is correct though, path and subdomain give the same result.

The proxied service is a docker container:

services:
tools:
image: corentinth/it-tools:latest
ports:
- 8000:80

2. Error messages and/or full log output:

> caddy run -w
2026/02/26 21:39:57.592	INFO	maxprocs: Leaving GOMAXPROCS=16: CPU quota undefined
2026/02/26 21:39:57.592	INFO	GOMEMLIMIT is updated	{"GOMEMLIMIT": 46308114432, "previous": 9223372036854775807}
2026/02/26 21:39:57.592	INFO	using adjacent Caddyfile
2026/02/26 21:39:57.592	INFO	adapted config to JSON	{"adapter": "caddyfile"}
2026/02/26 21:39:57.604	INFO	admin	admin endpoint started	{"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2026/02/26 21:39:57.604	INFO	tls.cache.maintenance	started background certificate maintenance	{"cache": "0x34fdf1345280"}
2026/02/26 21:39:57.604	INFO	http.auto_https	server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS	{"server_name": "srv0", "https_port": 443}
2026/02/26 21:39:57.604	INFO	http.auto_https	enabling automatic HTTP->HTTPS redirects	{"server_name": "srv0"}
2026/02/26 21:39:57.606	DEBUG	http.auto_https	adjusted config	{"tls": {"automation":{"policies":[{"subjects":["tools.localhost","localhost"]},{}]}}, "http": {"servers":{"remaining_auto_https_redirects":{"listen":[":80"],"routes":[{},{}]},"srv0":{"listen":[":443"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:8000"}]}]}]}],"terminal":true},{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"rewrite","strip_path_prefix":"/tools"}]},{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:8000"}]}]}]}],"match":[{"path":["/tools/*"]}]}]}],"terminal":true}],"tls_connection_policies":[{}],"automatic_https":{}}}}}
2026/02/26 21:39:57.619	INFO	tls	storage cleaning happened too recently; skipping for now	{"storage": "FileStorage:C:\\Users\\dustb\\AppData\\Roaming\\Caddy", "instance": "34045383-a7eb-4caf-96e9-35c91437047b", "try_again": "2026/02/27 21:39:57.619", "try_again_in": 86400}
2026/02/26 21:39:57.619	INFO	tls	finished cleaning storage units
2026/02/26 21:39:57.620	INFO	pki.ca.local	root certificate is already trusted by system	{"path": "storage:pki/authorities/local/root.crt"}
2026/02/26 21:39:57.621	DEBUG	http	starting server loop	{"address": "[::]:443", "tls": true, "http3": false}
2026/02/26 21:39:57.621	INFO	http	enabling HTTP/3 listener	{"addr": ":443"}
2026/02/26 21:39:57.621	INFO	http.log	server running	{"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2026/02/26 21:39:57.621	DEBUG	http	starting server loop	{"address": "[::]:80", "tls": false, "http3": false}
2026/02/26 21:39:57.621	WARN	http	HTTP/2 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/26 21:39:57.621	WARN	http	HTTP/3 skipped because it requires TLS	{"network": "tcp", "addr": ":80"}
2026/02/26 21:39:57.621	INFO	http.log	server running	{"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2026/02/26 21:39:57.621	INFO	http	enabling automatic TLS certificate management	{"domains": ["tools.localhost", "localhost"]}
2026/02/26 21:39:57.622	WARN	tls	stapling OCSP	{"identifiers": ["tools.localhost"]}
2026/02/26 21:39:57.622	DEBUG	tls.cache	added certificate to cache	{"subjects": ["tools.localhost"], "expiration": "2026/02/27 09:22:03.000", "managed": true, "issuer_key": "local", "hash": "9771157e0c6c91f75a324b6999612b74f6bd47783dcad94e2b8972e31aca8df8", "cache_size": 1, "cache_capacity": 10000}
2026/02/26 21:39:57.622	DEBUG	events	event	{"name": "cached_managed_cert", "id": "a276fc39-ab10-4d6b-a0b3-8ef63ea36a96", "origin": "tls", "data": {"sans":["tools.localhost"]}}
2026/02/26 21:39:57.622	WARN	tls	stapling OCSP	{"identifiers": ["localhost"]}
2026/02/26 21:39:57.623	DEBUG	tls.cache	added certificate to cache	{"subjects": ["localhost"], "expiration": "2026/02/27 06:06:27.000", "managed": true, "issuer_key": "local", "hash": "77c2a6805bfd86ed97f24c90e409b17c49aa9d2acb9410e82b7c0071c2182778", "cache_size": 2, "cache_capacity": 10000}
2026/02/26 21:39:57.623	DEBUG	events	event	{"name": "cached_managed_cert", "id": "b3254d5f-529b-4944-963d-49931f34c033", "origin": "tls", "data": {"sans":["localhost"]}}
2026/02/26 21:39:57.623	DEBUG	events	event	{"name": "started", "id": "467a79f1-fab0-4a9a-affa-206318ada342", "origin": "", "data": null}
2026/02/26 21:39:57.623	INFO	autosaved config (load with --resume flag)	{"file": "C:\\Users\\dustb\\AppData\\Roaming\\Caddy\\autosave.json"}
2026/02/26 21:39:57.623	INFO	serving initial configuration
2026/02/26 21:39:57.623	INFO	watcher	watching config file for changes	{"config_file": "Caddyfile"}
2026/02/26 21:40:01.165	DEBUG	events	event	{"name": "tls_get_certificate", "id": "147e7dc0-df80-4014-9915-4d0a5e62803b", "origin": "tls", "data": {"client_hello":{"CipherSuites":[47802,4865,4866,4867,49195,49199,49196,49200,52393,52392,49171,49172,156,157,47,53],"ServerName":"localhost","SupportedCurves":[2570,4588,29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[1027,2052,1025,1283,2053,1281,2054,1537],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[2570,772,771],"RemoteAddr":{"IP":"::1","Port":60137,"Zone":""},"LocalAddr":{"IP":"::1","Port":443,"Zone":""}}}}
2026/02/26 21:40:01.165	DEBUG	tls.handshake	choosing certificate	{"identifier": "localhost", "num_choices": 1}
2026/02/26 21:40:01.166	DEBUG	tls.handshake	default certificate selection results	{"identifier": "localhost", "subjects": ["localhost"], "managed": true, "issuer_key": "local", "hash": "77c2a6805bfd86ed97f24c90e409b17c49aa9d2acb9410e82b7c0071c2182778"}
2026/02/26 21:40:01.166	DEBUG	tls.handshake	matched certificate in cache	{"remote_ip": "::1", "remote_port": "60137", "subjects": ["localhost"], "managed": true, "expiration": "2026/02/27 06:06:27.000", "hash": "77c2a6805bfd86ed97f24c90e409b17c49aa9d2acb9410e82b7c0071c2182778"}
2026/02/26 21:40:01.168	DEBUG	http.handlers.rewrite	rewrote request	{"request": {"remote_ip": "::1", "remote_port": "60137", "client_ip": "::1", "proto": "HTTP/2.0", "method": "GET", "host": "localhost", "uri": "/", "headers": {"Sec-Fetch-Dest": ["document"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"], "Sec-Fetch-Mode": ["navigate"], "Sec-Fetch-User": ["?1"], "Accept-Encoding": ["gzip, deflate, br, zstd"], "Accept-Language": ["en-US,en;q=0.9,de-DE;q=0.8,de;q=0.7"], "Priority": ["u=0, i"], "Cache-Control": ["max-age=0"], "Sec-Ch-Ua-Platform": ["\"Windows\""], "Dnt": ["1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"], "Sec-Fetch-Site": ["none"], "Cookie": ["REDACTED"], "If-None-Match": ["\"67176216-ae3\""], "If-Modified-Since": ["Tue, 22 Oct 2024 08:28:06 GMT"], "Sec-Ch-Ua": ["\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Google Chrome\";v=\"144\""], "Sec-Ch-Ua-Mobile": ["?0"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "localhost", "ech": false}}, "method": "GET", "uri": "/"}
2026/02/26 21:40:01.168	DEBUG	http.handlers.reverse_proxy	selected upstream	{"dial": "localhost:8000", "total_upstreams": 1}
2026/02/26 21:40:01.172	DEBUG	http.handlers.reverse_proxy	upstream roundtrip	{"upstream": "localhost:8000", "duration": 0.0037794, "request": {"remote_ip": "::1", "remote_port": "60137", "client_ip": "::1", "proto": "HTTP/2.0", "method": "GET", "host": "localhost", "uri": "/", "headers": {"Sec-Ch-Ua": ["\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Google Chrome\";v=\"144\""], "Via": ["2.0 Caddy"], "Priority": ["u=0, i"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"], "If-Modified-Since": ["Tue, 22 Oct 2024 08:28:06 GMT"], "X-Forwarded-Proto": ["https"], "Accept-Encoding": ["gzip, deflate, br, zstd"], "Accept-Language": ["en-US,en;q=0.9,de-DE;q=0.8,de;q=0.7"], "Cache-Control": ["max-age=0"], "Sec-Ch-Ua-Platform": ["\"Windows\""], "If-None-Match": ["\"67176216-ae3\""], "Sec-Ch-Ua-Mobile": ["?0"], "X-Forwarded-For": ["::1"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36"], "Sec-Fetch-Mode": ["navigate"], "Dnt": ["1"], "Sec-Fetch-Site": ["none"], "Cookie": ["REDACTED"], "Sec-Fetch-Dest": ["document"], "X-Forwarded-Host": ["localhost"], "Upgrade-Insecure-Requests": ["1"], "Sec-Fetch-User": ["?1"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": "localhost", "ech": false}}, "headers": {"Date": ["Thu, 26 Feb 2026 21:40:01 GMT"], "Last-Modified": ["Tue, 22 Oct 2024 08:28:06 GMT"], "Connection": ["keep-alive"], "Etag": ["\"67176216-ae3\""], "Server": ["nginx/1.26.2"]}, "status": 304}
2026/02/26 21:40:01.321	DEBUG	events	event	{"name": "tls_get_certificate", "id": "40b5f926-ae57-422a-bbaa-1376f216f1de", "origin": "tls", "data": {"client_hello":{"CipherSuites":[23130,4865,4866,4867,49195,49199,49196,49200,52393,52392,49171,49172,156,157,47,53],"ServerName":"localhost","SupportedCurves":[56026,4588,29,23,24],"SupportedPoints":"AA==","SignatureSchemes":[1027,2052,1025,1283,2053,1281,2054,1537],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[2570,772,771],"RemoteAddr":{"IP":"::1","Port":64784,"Zone":""},"LocalAddr":{"IP":"::1","Port":443,"Zone":""}}}}
2026/02/26 21:40:01.321	DEBUG	tls.handshake	choosing certificate	{"identifier": "localhost", "num_choices": 1}
2026/02/26 21:40:01.321	DEBUG	tls.handshake	default certificate selection results	{"identifier": "localhost", "subjects": ["localhost"], "managed": true, "issuer_key": "local", "hash": "77c2a6805bfd86ed97f24c90e409b17c49aa9d2acb9410e82b7c0071c2182778"}
2026/02/26 21:40:01.321	DEBUG	tls.handshake	matched certificate in cache	{"remote_ip": "::1", "remote_port": "64784", "subjects": ["localhost"], "managed": true, "expiration": "2026/02/27 06:06:27.000", "hash": "77c2a6805bfd86ed97f24c90e409b17c49aa9d2acb9410e82b7c0071c2182778"}


> curl.exe -kvL localhost/tools/
* Host localhost:80 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:80...
* Established connection to localhost (::1 port 80) from ::1 port 62038
* using HTTP/1.x
> GET /tools/ HTTP/1.1
> Host: localhost
> User-Agent: curl/8.16.0
> Accept: */*
>
< HTTP/1.1 308 Permanent Redirect
< Connection: close
< Location: https://localhost/tools/
< Server: Caddy
< Date: Thu, 26 Feb 2026 21:47:40 GMT
< Content-Length: 0
<
* shutting down connection #0
* Clear auth, redirects to port from 80 to 443
* Issue another request to this URL: 'https://localhost/tools/'
* Host localhost:443 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:443...
* schannel: disabled automatic use of client certificate
* ALPN: curl offers http/1.1
* ALPN: server accepted http/1.1
* Established connection to localhost (::1 port 443) from ::1 port 62039
* using HTTP/1.x
> GET /tools/ HTTP/1.1
> Host: localhost
> User-Agent: curl/8.16.0
> Accept: */*
>
* schannel: remote party requests renegotiation
* schannel: renegotiating SSL/TLS connection
* schannel: SSL/TLS connection renegotiated
* Request completely sent off
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Alt-Svc: h3=":443"; ma=2592000
< Content-Length: 2787
< Content-Type: text/html
< Date: Thu, 26 Feb 2026 21:47:40 GMT
< Etag: "67176216-ae3"
< Last-Modified: Tue, 22 Oct 2024 08:28:06 GMT
< Server: nginx/1.26.2
< Via: 1.1 Caddy
<
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>IT Tools - Handy online tools for developers</title>
    <meta itemprop="name" content="IT Tools - Handy online tools for developers" />
    <meta
      name="description"
      content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
    />
    <meta
      itemprop="description"
      content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
    />
    <link rel="author" href="humans.txt" />
    <link rel="canonical" href="https://it-tools.tech" />

    <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
    <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
    <link rel="mask-icon" href="safari-pinned-tab.svg" color="#18a058" />
    <meta name="msapplication-TileColor" content="#da532c" />
    <meta name="theme-color" content="#ffffff" />

    <meta property="og:url" content="https://it-tools.tech/" />
    <meta property="og:type" content="website" />
    <meta property="og:title" content="IT Tools - Handy online tools for developers" />
    <meta
      property="og:description"
      content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
    />
    <meta property="og:image" content="https://it-tools.tech/banner.png?v=2" />

    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:site" content="@ittoolsdottech" />
    <meta name="twitter:creator" content="@cthmsst" />

    <meta name="twitter:title" content="IT Tools - Handy online tools for developers" />
    <meta
      name="twitter:description"
      content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
    />
    <meta name="twitter:image" content="https://it-tools.tech/banner.png?v=2" />
    <meta name="twitter:image:alt" content="IT Tools - Handy online tools for developers" />
    <script type="module" crossorigin src="/assets/index-f8ba620c.js"></script>
    <link rel="stylesheet" href="/assets/index-f0f4dd3b.css">
  <link rel="manifest" href="/manifest.webmanifest"></head>
  <body>
    <div id="app"></div>

  </body>
</html>
* Connection #1 to host localhost:443 left intact

3. Caddy version:

v2.11.1 h1:C7sQpsFOC5CH+31KqJc7EoOf8mXrOEkFyYd6GpIqm/s=

4. How I installed and ran Caddy:

winget add caddy

a. System environment:

Win11

b. Command:

caddy run -w

c. Service/unit/compose file:

d. My complete Caddy config:

{
	debug
}

localhost {
	handle_path /tools/* {
		reverse_proxy localhost:8000
	}
}

tools.localhost {
	reverse_proxy localhost:8000
}

5. Links to relevant resources:

See this article

You’re stripping /tools off of the path passed to your app, but your app tries to load JS files from /assets which is not being proxied, so those files get blank responses and cannot load the content of your page.