Skip to main content
Every domain an app owns has its own set of traffic controls, enforced by the embedded Caddy ingress before a request reaches the container: None of these needs a second proxy or a container restart. A change is stored, audited, and applied on the next ingress reconcile pass (normally within a second). Deleting a section restores the default behavior for that domain.

Where to find it

On the Domains page, click a domain name. The domain page has tabs for Overview, Headers, Forwarding, Access, Cache, Redirects, Ports and Danger zone. Each editor keeps an unsaved draft per tab, shows the difference before you save, and a “rules preview” lists in order what happens to a sample request.

Order of evaluation

For a request to a domain with controls configured, the ingress runs, in order:
  1. Redirects (lower case host, force HTTPS, trailing slash)
  2. Geo rule
  3. Headers: request rules, CORS, then response rules and presets
  4. The existing WAF, rate limit and basic auth, if configured
  5. Cache lookup
  6. Forwarders, first match wins
  7. The domain’s own app
Maintenance mode and a whole-domain redirect still take precedence over everything above, exactly as before.

Headers

Rules are evaluated in order. Each rule has a side (request or response), an operation (set, add or remove), a name, and (except for remove) a value.
  • Request rules change what the app receives.
  • Response rules apply after the app answers, so they override the app’s own value. A later rule overrides an earlier one, and any rule overrides a preset.
  • Header names must be valid RFC 9110 tokens. Values cannot contain line breaks or control characters, and are capped by APP_DOMAIN_POLICY_MAX_HEADER_VALUE_LEN. Operator text is never expanded as a Caddy placeholder, so a value like {env.SECRET} is sent literally.
  • These names are reserved by the ingress and refused: Host, Content-Length, Transfer-Encoding, Connection, Keep-Alive, Upgrade, TE, Trailer, Proxy-Connection, Forwarded, X-Real-IP, Proxy-Authorization and every X-Forwarded-* header. X-Forwarded-Prefix has its own option, forwarded_prefix. X-Cache and Cache-Status are reserved for the cache.

Presets

HSTS is only accepted when the domain has a publicly trusted certificate: ACME enabled, an uploaded certificate, or TLS terminated by your own proxy. Sending HSTS with a self-signed certificate would lock visitors out, so the API refuses it. When the ingress terminates TLS, HSTS is only sent on HTTPS responses. CORS echoes the request’s Origin back only when it is in the list, and adds Vary: Origin. * allows any origin but cannot be combined with credentials.

Path forwarders

A forwarder sends requests that match a path somewhere other than the domain’s app. Rules are checked in order, the first match wins, and a request no rule matches goes to the app as usual. An app target uses the same upstream the app’s own domain routes to; if that app is not running, the rule answers 502 with a plain explanation rather than falling back to the default app.

Outbound safety

An external URL is resolved and every address it resolves to is checked, both when you save and again on every ingress pass (cached for a minute), so a DNS record changed later to point at an internal address is caught. Refused targets:
  • loopback, unless APP_FORWARDER_ALLOW_LOOPBACK=true
  • private and carrier grade NAT ranges, unless APP_FORWARDER_ALLOW_PRIVATE_NETWORKS=true
  • link-local and cloud metadata addresses (169.254.0.0/16, fe80::/10, fd00:ec2::/32, 100.100.100.200), always
The ingress dials the checked address directly (with the original name for TLS), so a lookup at request time cannot be steered elsewhere. A rule whose target is refused answers 502. A forwarder to an external URL whose host is the domain itself is refused, and so is a redirect to a path the same rule would match again.

Geolocation

A geo rule is an allow list (only these countries) or a deny list (everyone but these) of ISO 3166-1 alpha-2 codes, plus what to do with a blocked visitor: a plain 403 (or 404, 451), a redirect, or your own HTML page.
  • Private, loopback and link-local addresses are never blocked by country.
  • Addresses and ranges in exempt always pass.
  • unknown decides requests whose country cannot be determined: allow (default) or block.

Country sources

The ingress needs a country source. Without one, geo rules are saved but inactive, and the dashboard and GET .../policies say so. APP_GEOIP_SOURCE picks auto (default: the header when trusted proxies are configured, the database when it loads, header first), header, mmdb or none. Two databases work out of the box:
  • MaxMind GeoLite2 Country: free, but needs a MaxMind account and licence key to download, and its EULA applies (including keeping the file updated).
  • DB-IP IP to Country Lite: free under CC BY 4.0, no account. Attribution is required wherever you show the data, for example “IP geolocation by DB-IP”.
The database is read with oschwald/maxminddb-golang (ISC licence). Check what a lookup returns:
The lookup tool only consults the database; the header source depends on the visitor’s proxy and cannot be simulated.

Caching

Caching is per domain and off by default. Each rule matches a path (prefix, exact or regex) and says how long to keep a response.
  • Only GET and HEAD are cached; a HEAD can be answered from a stored GET.
  • Never cached: requests with Authorization, requests with a Cookie (unless the rule sets cache_with_cookies), range requests, upgrades, and Cache-Control: no-store requests. Responses with Set-Cookie, Cache-Control: private, no-store or no-cache, Vary: *, a Vary on a header the rule does not list, a partial content range, or a status outside the rule’s list (default 200).
  • TTL: the app’s s-maxage, then max-age, then the rule’s ttl_seconds. With override_upstream, the rule’s TTL always wins. A TTL of 0 means “do not store”. TTLs are capped by APP_INGRESS_CACHE_MAX_TTL.
  • Stale while revalidate: after an entry expires, the first request fetches a fresh copy from the app, and requests arriving while that fetch is running get the expired copy, marked STALE, for up to stale_while_revalidate_seconds.
  • Key: the request host, path and query, Accept-Encoding, and the rule’s vary headers. Entries from one host are never served for another.
  • Every response on a cached path carries X-Cache: HIT, MISS, STALE or BYPASS, and a hit has an Age header. A hit answers If-None-Match with 304 when the ETag matches.
The cache lives in the control plane’s memory: a least recently used store capped by APP_INGRESS_CACHE_MAX_BYTES (default 64 MiB) across every domain, with single responses capped by APP_INGRESS_CACHE_MAX_OBJECT_BYTES (default 2 MiB) or the domain’s own lower max_object_bytes. It is per instance, starts empty after a restart, and is not shared between nodes.

Redirects

Whole-domain redirects (one host to a target URL) still use domain redirects; the controls here build on them.

Force HTTPS

How plain HTTP is handled depends on where TLS ends, and the Redirects tab says which case applies: The redirect goes to the public HTTPS port (public_https_port), so an internal port such as :8443 never leaks into a URL. The status is 308 by default, which keeps the method and body, or 301.

www and apex

Three presets: Redirect www to apex, Redirect apex to www, or Serve both. Choosing a redirect preset:
  1. Adds the other host to the app if it is missing (this part needs the write ability, like adding any domain), and shows the DNS record to create for it.
  2. Installs a redirect from the non canonical host to the canonical one that keeps path and query.
A preset is refused when the other host belongs to another app, when either host is a wildcard, when it would create a loop, or when either host already redirects somewhere else (pass replace to overwrite). A host in maintenance mode keeps answering 503 before any redirect, and the page warns about it.

Aliases

Pick one of the app’s domains as the primary and mark others as aliases: each alias redirects to the primary with path and query kept, using 301, 302, 307 or 308. Domains you unmark stop redirecting to the primary.

Normalisation

  • Trailing slash: add (paths without a file extension get a slash), remove, or off (default). Uses 308.
  • Lower case host: redirects Example.COM to example.com. Off by default.
The tab walks http://www.example.com/a?x=1, http://example.com/a and https://www.example.com/a through your settings and shows each hop with its status code before you save.

Ports

Raw TCP and UDP port forwards are app streams (raw TCP streams); the Ports tab and domains ports show list them for the domain’s app with their target, who may reach them, and conflicts: another stream on the same port, the HTTP ingress port, or a container that already publishes the port. “Open this port only to these IPs” writes firewall rules: one allow rule per address or range, then a deny for everyone else, all labelled domain-ports:PORT/PROTO. Removing the restriction deletes exactly those rules.

Limits

Every cap has a default and an environment override on the control plane.

Permissions and audit

Every change is recorded in the audit log like any other API write.

Next steps

Domains and ingress

TLS, WAF, maintenance mode and the rest of the ingress.

Load balancing

Balance traffic across an app’s replicas.
Last modified on October 12, 2026