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.
250 lines
7.4 KiB
Markdown
250 lines
7.4 KiB
Markdown
---
|
|
type: reference
|
|
title: Caddyfile Syntax
|
|
description: 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).
|
|
resource: https://caddyserver.com/docs/caddyfile
|
|
tags:
|
|
- caddy
|
|
- caddyfile
|
|
- reverse-proxy
|
|
verified: 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](https://caddyserver.com/docs/caddyfile-tutorial),
|
|
[matchers](https://caddyserver.com/docs/caddyfile/matchers),
|
|
[rewrite](https://caddyserver.com/docs/caddyfile/directives/rewrite),
|
|
[handle](https://caddyserver.com/docs/caddyfile/directives/handle),
|
|
[forward_auth](https://caddyserver.com/docs/caddyfile/directives/forward_auth),
|
|
[reverse_proxy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy),
|
|
[concepts](https://caddyserver.com/docs/caddyfile/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 `/foo` or `/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** matching
|
|
`handle` runs. A `handle` with 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's `2xx`
|
|
response onto the proxied request (e.g. `X-Authentik-Username`).
|
|
* All `reverse_proxy` subdirectives pass through, including `trusted_proxies`.
|
|
* `trusted_proxies` accepts CIDRs or the keyword `private_ranges` (all private
|
|
ranges). It controls which sources Caddy trusts for `X-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.
|