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.
| Row status | TLS | Agents | Requests, 24h | |||
|---|---|---|---|---|---|---|
app.example.com http://app-1:8080 +1WAFLB | app.example.com | Every agent | 18,41296 blocked | Active | ||
grafana.example.com http://grafana:3000Authentik | grafana.example.com | edge-fra | 5,730 | Maintenance | ||
staging.example.com http://staging:8080 | Wildcard *.example.com | lab-nuc | — | Paused | ||
vpn.example.com http://headscale:8080mTLSTailnet | — | edge-fra, edge-ams | 8124 blocked | Active |
Changing many hosts at once
Section titled “Changing many hosts at once”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.
HTTPS and connection options
Section titled “HTTPS and connection options”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, nofollowto every response, replacing any the upstream sent, and answers/robots.txtwith 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.
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.
Upstreams and load balancing
Section titled “Upstreams and load balancing”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.
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.
Timeouts
Section titled “Timeouts”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.
Location rules
Section titled “Location rules”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.
Redirects and rewrites
Section titled “Redirects and rewrites”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.
Cache assets
Section titled “Cache assets”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 noCache-Controlof 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
Authorizationheader 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
Section titled “Maintenance mode”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-Afterheader 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
Section titled “Rate limiting”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
/loginor/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.
DNS controls
Section titled “DNS controls”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.
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.
Headers
Section titled “Headers”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.