Skip to content

Reverse proxy

A proxy host maps one or more domains onto one or more upstreams. It is the thing this product is mostly for, and most of the other features attach to it.

Every host, HTTP or L4, can carry notes: free text for whatever the next person needs to know, up to 2000 characters. They show as an icon beside the host’s name in the list, with the text on hover, and the search box finds them. Anyone who can see the host can read them.

The proxy host listSort by domain or status, switch to the enabled or disabled hosts, or hover the note on staging. Grafana is in maintenance mode. Tick a few rows to disable them together.DemoNothing you change here is saved
Proxy Hosts
4
3 enabled, 1 disabled
Requests, 24h
24,954
0.4% blocked
Certificates
3
3 hosts on this page carry one
Agents
3one or more offline
2 connected
Row status
TLSAgentsRequests, 24h
app.example.com
http://app-1:8080 +1
WAFLB
app.example.comEvery agent
18,41296 blocked
Active
grafana.example.com
http://grafana:3000
Authentik
grafana.example.comedge-fra
5,730
Maintenance
staging.example.com
http://staging:8080
Wildcard *.example.comlab-nuc—
Paused
vpn.example.com
http://headscale:8080
mTLSTailnet
—edge-fra, edge-ams
8124 blocked
Active

Tick hosts in the list and a bar replaces the filters: enable, disable or delete them, turn maintenance mode on or off, or give them all one certificate (automatic included) or one access list (none included). Every action confirms with the names of the hosts it will touch.

A batch is all or nothing. If one host could not take the change - a wildcard domain asked to use automatic certificates with no DNS provider, say - nothing is written and the dialog says why. Each host gets its own row in the audit log, and Caddy is reloaded once for the lot. Selection covers the page you are looking at and clears when you page, filter, search or sort; it is not offered on a phone. An operator can tick only the hosts their groups let them manage. The API has the same actions as the bulkProxyHosts mutation.

Each host has switches at the top of its editor:

  • Force HTTPS answers plain HTTP with a 308 redirect to the same URL over HTTPS.
  • HSTS tells browsers to use only HTTPS for the host for two years, optionally including its subdomains. It needs Force HTTPS, because a browser pinned to HTTPS never tries HTTP again.
  • WebSocket support lets upgrade requests through. The WAF checks the handshake like any other request, but not the messages after it. With it off, an upgrade is refused with a 403.
  • Preserve Host header sends the domain the client asked for rather than the upstream’s own address.
  • Discourage search engines adds X-Robots-Tag: noindex, nofollow to every response, replacing any the upstream sent, and answers /robots.txt with a file that disallows everything. Both come before access lists and sign-in, so a crawler sees them on a protected host too. Crawlers that follow the rules stay away; it keeps nobody out.
Host optionsTurn off Force HTTPS and HSTS follows.DemoNothing you change here is saved
Proxy Host Enabled
This host is active and routing traffic
Advanced Options
Redirect plain HTTP requests to HTTPS
Tell browsers to only ever use HTTPS for this host. Requires Force HTTPS
Include subdomains in the Strict-Transport-Security header
Allow WebSocket upgrades. The WAF checks the handshake but not the messages after it
Send the original Host header to the upstream instead of the upstream's own address
Skip SSL certificate hostname verification for backend connections
Ask search engines not to index this host: every response gets X-Robots-Tag: noindex, nofollow, and /robots.txt disallows everything, even behind a sign-in. Well-behaved crawlers honour it; it doesn't keep anyone out
CompressionCompress text responses with zstd or gzip. Default follows Settings → Network → Compression

HTTP/2 and HTTP/3 are offered on every host. Settings → Network → HTTP Versions turns either off, for all hosts at once: Caddy decides this per listening port, and every host shares one. HTTP/1.1 always stays on. Hosts served on a tailnet get HTTP/2 from the same switch, but HTTP/3 only when Serve HTTP/3 on tailnet listeners is on as well.

Responses are compressed with zstd or gzip, whichever the browser prefers, and turned off for every host in Settings → Network → Compression. Only text formats are compressed - HTML, CSS, JavaScript, JSON, XML, SVG and fonts - and only above 512 bytes; images, video, archives and anything the upstream already compressed pass through unchanged. A host’s Compression option can follow that setting (Default) or force it On or Off for that host alone, say for an upstream that compresses better itself. Server-sent events and WebSockets keep streaming.

A host can have any number of upstreams. Twelve selection policies are available, including round-robin, least-connections, weighted, and hashing by IP, query, header or cookie.

UpstreamsAdd one, or switch a scheme.DemoNothing you change here is saved
Upstreams
Backend servers to proxy requests to

Health checks come in both forms:

  • Active - CPM asks the upstream on an interval and takes failures out of rotation.
  • Passive - failures on real requests count against an upstream, with a configurable number of failures and a duration to keep it out.

Retries let a failed request try another upstream rather than surfacing the error.

Load balancing and health checksChange the policy to see what each one asks for.DemoNothing you change here is saved
Load BalancerConfigure load balancing and health checks for multiple upstreams
Retry Settings
How long to try upstreams
Wait between attempts
Maximum retry attempts
Periodically probe upstreams to check health
Path to probe for health
Override upstream port
How often to check
Timeout for health probe
Expected HTTP status
Expected response body
Consecutive successful probes before an upstream is used again.
Consecutive failed probes before an upstream is taken out.
GET when unset.
Sent with the probe. Distinct from the expected response body.
Mark upstreams unhealthy based on response failures
How long to remember failures
Failures before marking unhealthy
Comma-separated status codes
Latency above which an upstream is marked unhealthy
Concurrent requests to one upstream before it is considered unhealthy.

Caddy’s defaults suit most upstreams: three seconds to connect and no limit on anything after that. Upstream timeouts change them per host, as durations such as 30s, 1m30s, 2h or 1d, and an empty field keeps the default:

  • Connect - opening a connection, name lookup included.
  • Response headers - from sending the request to the upstream answering.
  • Read and Write - the longest wait for a single read from, or write to, the upstream.
  • Idle connection - how long an unused connection is kept for reuse (two minutes by default).
  • Stream lifetime - closes WebSockets and other upgraded connections after this long.
  • Stream close delay - how long upgraded connections survive a configuration reload, rather than being closed at once.

Location rules use their host’s timeouts. A host that reaches its upstream through a Tailscale node keeps only the two stream settings, since that transport takes no others. The server-side read, write and idle timeouts belong to the listener rather than a host.

Upstream timeoutsType something that is not a duration to see it refused.DemoNothing you change here is saved
Upstream timeoutsHow long to wait on the upstream before giving up. A field left empty keeps Caddy's default, and location rules use the same values
Opening a connection, name lookup included. Caddy's default is 3s
From sending the request to receiving the response headers. No limit by default
The longest wait for the next read from the upstream. No limit by default
The longest wait for a write to the upstream. No limit by default
How long an unused connection is kept open for reuse. Caddy's default is 2m
Closes WebSockets and other upgraded connections after this long. No limit by default
How long upgraded connections survive a config reload. Closed at once by default
Through a Tailscale node only the two stream settings apply
The host editor's raw-config fieldsDemoNothing you change here is saved
Caddyfile directives for this host, adapted by Caddy and inserted before the reverse proxy. Rejected on save if Caddy cannot parse them.
Tab indents; press Escape then Tab to leave the fieldCaddyfile
JSON array of Caddy handlers, run before the reverse proxy.
Tab indents; press Escape then Tab to leave the fieldJSON
Deep-merged into the reverse_proxy handler itself (proxy mode only).
Tab indents; press Escape then Tab to leave the fieldJSON

Path-based routing inside one host: send /api/* to one backend and /ws/* to another, each with its own upstreams, load balancing and health checks. A rule can also use an access list of its own, or none, instead of the host’s.

Location rulesExpand a rule to give it its own load balancer, or change the access list it uses.DemoNothing you change here is saved
Location Rules
Upstreams
Load BalancerHealth checks & balancing for this path's upstreams
Upstreams
Load BalancerHealth checks & balancing for this path's upstreams

Per-host redirect rules with a status of 301, 302, 307 or 308 - 307 and 308 preserve the request method, which matters for anything but a GET. Keep path carries the rest of the request along: Full path appends the whole path and query to the target, and After prefix drops the part of the pattern before its * first, so /blog/* sends /blog/2024/post to https://blog.example.com/2024/post. A path prefix can be prepended before the request reaches the upstream.

Redirects and path rewritesDemoNothing you change here is saved
Redirects
Path Rewrites
Internally rewrite the request URI before proxying. The client URL is unchanged; the upstream sees the target URI.

Cache assets covers stylesheets, scripts, images and fonts, matched by file extension. Pages and API responses are never cached. The max age runs from a minute to a year, a day by default.

  • Browser sets Cache-Control: max-age=<n> on successful asset responses whose upstream sent no Cache-Control of its own, so an application that already manages caching keeps its policy, and a 404 or 500 during a deploy is not kept for a day.
  • Caddy cache also keeps a shared copy in Caddy, so repeat requests skip the upstream. It needs the opt-in HTTP Cache module (see Caddy build); until that is compiled in, the host falls back to browser caching. The cache lives in memory unless another storage is chosen, such as Redis to share it between Caddys. A request carrying a cookie or an Authorization header always goes to the upstream, so an asset an application serves per user never reaches the next visitor.

Either way, a response that sets a cookie gets private added to its Cache-Control, alongside anything stricter the upstream sent, and nothing marked private or no-store is stored. The cache sits after any forward auth or access check, just before the upstream, so an asset is never served to a caller who could not fetch it.

Maintenance mode answers every request to a host with a 503 Service Unavailable while its upstream is being worked on. It is not the same as disabling the host: a disabled host stops existing as far as Caddy is concerned and falls through to the default response, while a host in maintenance keeps its certificate and tells visitors, and crawlers, that it will be back.

  • Bypass addresses - IP addresses or CIDR ranges, one per line, that still reach the upstream, so you can check the site before reopening it. The client address is the one Caddy resolves through Settings → Network → Trusted Proxies. Access lists, sign-in and the WAF still apply to a bypassing request.
  • Retry-After - seconds, sent as the Retry-After header when set.
  • Page - the HTML visitors see. Left empty, the host’s own custom error page for a 503 is used, then the global one from Settings → Responses → Error Pages, then a short built-in page.

The 503 comes before everything else in the host’s configuration but the WebSocket refusal - geo blocking, the WAF, forward auth and location rules included - and is sent with Cache-Control: no-store, so no cache keeps it past the maintenance window. Its settings are kept while it is off. The host list shows a Maintenance status, and the row’s menu turns it on or off in one click; either way the change is recorded in the audit log.

It never applies to the dashboard host, which would lock out the administrator who has to turn it off.

Rate limiting answers a client that sends too many requests with 429 Too Many Requests and a Retry-After header saying when to try again. It needs the opt-in Rate Limit module (see Caddy build); until that is compiled in, the host is served without limits and Caddy’s log says so.

A host has one or more zones, and each counts on its own:

  • Paths - Caddy path patterns such as /login or /api/*. Empty covers every request.
  • Requests and Per - how many requests one client may make within a sliding window, such as 100 per 1m.
  • Count per - the client IP, or the client IP and path together, so each path gets its own allowance.
  • IPv6 prefix - counts a whole IPv6 network, such as a /64, as one client, since one household or server usually holds far more addresses than it needs. Only with the client IP.

The client IP is the one Caddy resolves through Settings → Network → Trusted Proxies, so a load balancer in front does not make every visitor look like one client. The limit is checked before geo blocking and the WAF, so a flood never costs a WAF inspection. The 429 is rendered by the host’s custom error page for it, or the global one from Settings → Responses → Error Pages. Counters are kept per Caddy and survive a configuration reload; each agent counts on its own.

Rate limitingSwitch a zone to client IP and path to see the IPv6 prefix step aside.DemoNothing you change here is saved
Rate limitingAnswer clients that send too many requests with 429 Too Many Requests, before geo blocking and the WAF see them.
Zone 1
Caddy path patterns separated by spaces, such as /login or /api/*. Empty covers every request.
Zone 2
Caddy path patterns separated by spaces, such as /login or /api/*. Empty covers every request.
Each zone allows a number of requests per client in a sliding window. A request over the limit gets a 429 with Retry-After, rendered by this host's error pages. The client IP honours trusted proxies. An IPv6 prefix such as 64 counts a whole network as one client.

Upstream DNS pinning resolves upstream hostnames when the configuration is applied and writes the addresses into Caddy’s config, rather than leaving Caddy to resolve them per request. You can choose IPv4, IPv6 or both. Custom resolvers can be set per host; their connect timeout bounds the lookup and the connection together, and a Connect timeout under Upstream timeouts wins over it.

Upstream DNS pinningOpen it to override the global default.DemoNothing you change here is saved
Inherit uses the global setting. Enabled/Disabled overrides per host.
Both resolves AAAA + A with IPv6 preferred ordering.
Hostname upstreams are resolved at config-apply time
When enabled, hostname upstreams are written to Caddy as concrete IP dials. If this handler has multiple different HTTPS upstream hostnames, HTTPS pinning is skipped for those HTTPS upstreams to avoid SNI mismatch.

Pinning is skipped for HTTPS upstreams when one handler has several different HTTPS hostnames, because a single pinned address cannot carry more than one SNI name.

The host header is forwarded by default, which is what most applications behind a proxy expect; the Preserve Host header switch turns that off. Other request and response headers are set through the host’s advanced configuration fields, which take raw Caddy JSON and are admin-only.

Custom pre-handlers must be a JSON object or an array of objects, and the reverse proxy override a JSON object; anything else is refused on save with a 400, rather than dropped from the config without a word. A stored value left unchanged still saves, so an older host is never stuck.