Copy of the legacy ~/.pi/agent/docs tree rebuilt as the command-center knowledge base: concept files promoted to path identity, index listings stripped of frontmatter, all wikilinks/relative links resolved, every checkable claim verified against the live fleet (Dankserver = Ryzen 7 7700X + RTX 3090; Strix Halo is Dankclaw), unverifiable claims tagged. kb-lint exits 0.
7.4 KiB
type, title, description, resource, tags, verified
| type | title | description | resource | tags | verified | |||
|---|---|---|---|---|---|---|---|---|
| reference | Caddyfile Syntax | Caddy Caddyfile reference — directive and matcher syntax, named matchers, directive sorting, and the handle/route order rule. Grounded in caddyserver.com (fetched 2026-08-28). | https://caddyserver.com/docs/caddyfile |
|
2026-08-28 |
Caddyfile Syntax
A Caddyfile defines sites. Each site block starts with an address (a host or a port). Directives inside the block control how Caddy serves requests. Matchers classify requests. This page records the syntax that trips people up, grounded in the official docs.
Sources: caddyfile-tutorial, matchers, rewrite, handle, forward_auth, reverse_proxy, concepts.
Directive and matcher syntax
A directive takes an optional matcher as its first argument:
directive [<matcher>] <args...>
The matcher token is one of three forms:
*— the wildcard (matches all requests)./path— a path (must start with/).@name— a named matcher.
The matcher follows the directive name. A leading @name before a directive
name is parsed as a matcher definition, not a use (see below).
Named matchers
A named matcher is defined outside any directive and reused:
@name {
<matchers...>
}
If the set has one matcher, a one-liner works:
@post method POST
Use the matcher as the first argument of a directive:
reverse_proxy @post localhost:6001
Pitfall: @name <directive> (the @name before the directive) is parsed
as a one-liner definition of @name. Writing it twice re-defines the
matcher and Caddy rejects the config with matcher is defined more than once.
Always put the matcher after the directive: rewrite @name /to, not
@name rewrite /to.
Path matching
Path matching is an exact match by default. Append * for a prefix match:
/foo*matches/foo,/foo/, and/foobar./foo/*matches/foo/and/foo/anything, but not/fooor/foobar.
A handle inline matcher is a single token. To match several paths, use a
named matcher:
@data path /a.json /b.js
handle @data { ... }
Directive sorting (the key rule)
Caddy sorts top-level directives by a fixed canonical order. It does not
run them in file order. The relevant order (partial): vars, root,
redir, then rewrite, try_files, uri, then basic_auth, then
file_server.
Consequence: a top-level rewrite runs before a top-level
basic_auth. If the rewrite changes the path, the path-based auth matcher sees
the rewritten path, not the original. This silently applies the wrong
auth block.
Fix: wrap the ordered directives in handle or route. These blocks run
their contents in file order (no re-sort).
handle vs route
handle [<matcher>] { ... }— mutually exclusive. Only the first matchinghandleruns. Ahandlewith no matcher is the catch-all.route [<matcher>] { ... }— runs its contents as one unit, in file order, and the request continues to later top-level directives.
Use handle for mutually exclusive branches (for example, per-path pages). Use
route when several directives must all run in order.
rewrite
rewrite [<matcher>] <to>
<to> is the new URI (path or query). If <to> starts with /, Caddy 2.8.0+
parses it directly. Before 2.8.0, a leading / was confused with a path
matcher, so a * disambiguator was needed: rewrite * /index.html.
try_files <files...> rewrites to the first listed file that exists. It is the
clean way to serve a named file for a path prefix:
try_files /admin.html
file_server
file_server and root
root * /srv/site
file_server
root sets the site root. file_server serves files from the root by
appending the request path. The * is the wildcard matcher (it disambiguates
the path argument).
basic_auth
basic_auth [<matcher>] {
<username> <bcrypt_hash>
...
}
The default hash algorithm is bcrypt. A block may list several users; any of
them is accepted. basicauth is deprecated in favor of basic_auth, but both
work.
forward_auth
forward_auth is Caddy's auth gate for an external identity provider (OIDC,
Authelia, Authentik, Tailscale, ...). It clones the incoming request, sends it
to an auth upstream, and only proxies to the app if the upstream answers 2xx.
forward_auth [<matcher>] [<upstreams...>] {
uri <to>
copy_headers <fields...>
trusted_proxies [private_ranges] <ranges...>
}
<upstreams...>— the auth gateway address (positional, required).uri <to>— the path the cloned request hits on the gateway (the verification endpoint, e.g./outpost.goauthentik.io/auth/caddy).copy_headers <fields...>— header fields to copy from the gateway's2xxresponse onto the proxied request (e.g.X-Authentik-Username).- All
reverse_proxysubdirectives pass through, includingtrusted_proxies. trusted_proxiesaccepts CIDRs or the keywordprivate_ranges(all private ranges). It controls which sources Caddy trusts forX-Forwarded-*.
Pitfall — the outpost path: an embedded outpost (Authentik) serves its own
login UI at a path (e.g. /outpost.goauthentik.io/*). That path must be
proxied to the gateway before forward_auth runs, or the login page is
itself gated. Wrap both in a route (file order), outpost first:
example.com {
route {
reverse_proxy /outpost.goauthentik.io/* authentik:9000
forward_auth authentik:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Authentik-Username X-Authentik-Groups
trusted_proxies private_ranges
}
reverse_proxy app:8080
}
}
The reverse_proxy on the outpost path is terminal: a matched request proxies
to the gateway and the route stops. Non-outpost paths fall through to
forward_auth, then the app.
Worked example: per-path auth with shared data
Serve /admin/ (operator only) and / (family), where both pages fetch the
same data files. The browser reuses the page's credential for the data
fetches, so the data block must accept both users.
example.com {
root * /srv/site
# Operator-only page. Auth is checked on the original path, before any
# rewrite, so the path matcher is reliable.
handle /admin* {
basic_auth {
frank $2a$14$<operator_hash>
}
try_files /admin.html
file_server
}
# Shared data + assets: the family OR the operator.
@data path /services.json /status.json /app.js /style.css
handle @data {
basic_auth {
dank $2a$14$<family_hash>
frank $2a$14$<operator_hash>
}
file_server
}
# Everything else: the family.
handle {
basic_auth {
dank $2a$14$<family_hash>
}
try_files /index.html
file_server
}
}
handle blocks run in file order and are mutually exclusive (first match
wins). Put the specific handle /admin* before the catch-all handle.
Validate before you deploy
Run caddy validate --config <file> --adapter caddyfile before loading a
Caddyfile. A placeholder hash (not a real bcrypt hash) fails validation with
base64-decoding password. Render the real hashes first, then validate.