Caddy snippets cookbook
Copy-paste Caddyfile snippets for forward auth, basic auth, IP allowlist, custom headers, redirects, and maintenance pages.
caddy_extra_json: and caddy_extra_caddyfile: are the escape hatch for Caddy features that aren't deploy primitives: auth, rate limiting, headers, redirects, IP allowlists, body limits.
Each snippet shows both the Caddyfile form (Caddy's docs use this) and the JSON equivalent (admin API consumes this). Both work on vanilla caddy:2 unless noted.
For the proxy block schema, see the Reverse proxy guide.
Caddyfile vs JSON: caddy_extra_caddyfile: shells out to caddy adapt at render time (needs docker on the operator's machine). caddy_extra_json: is parsed inline. Mutually exclusive on a single service.
Adding authentication
Gates every request through an auth-decision endpoint on another service. Standard SSO pattern for self-hosted apps.
Caddyfile:
caddy_extra_caddyfile: |
forward_auth authelia:9091 {
uri /api/verify?rd=https://auth.example.com
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}JSON:
caddy_extra_json: |
[{
"handler": "forward_auth",
"upstreams": [{"dial": "authelia:9091"}],
"uri": "/api/verify?rd=https://auth.example.com",
"copy_headers": ["Remote-User", "Remote-Groups", "Remote-Email", "Remote-Name"]
}]Service:
- name: internal-app
image: ghcr.io/me/internal
domain: app.example.com
caddy_extra_caddyfile: |
forward_auth authelia:9091 { ... }
run: { port: 3000 }Basic auth (single user)
For an admin page or dashboard. Caddy bcrypt-hashes the password at config-load.
Caddyfile:
caddy_extra_caddyfile: |
basic_auth /* {
admin $2a$14$ABCDEFG... # bcrypt hash from `caddy hash-password`
}JSON:
caddy_extra_json: |
[{
"handler": "authentication",
"providers": {
"http_basic": {
"accounts": [
{"username": "admin", "password": "$2a$14$ABCDEFG..."}
]
}
}
}]Generate the hash:
docker run --rm caddy:2 caddy hash-password --plaintext 'your-password'IP allowlist
Restrict access to a set of IPs (CIDR ranges OK). Tailnet-only routes use this.
Caddyfile:
caddy_extra_caddyfile: |
@internal client_ip 100.64.0.0/10 192.168.1.0/24
handle @internal {
# request continues to reverse_proxy below
}
handle {
respond "Forbidden" 403
}JSON:
caddy_extra_json: |
[{
"match": [{"not": [{"client_ip": {"ranges": ["100.64.0.0/10", "192.168.1.0/24"]}}]}],
"handle": [{"handler": "static_response", "status_code": 403, "body": "Forbidden"}],
"terminal": true
}]Custom request / response headers
Strip a sensitive header before it reaches the upstream, or add one to the response.
Caddyfile:
caddy_extra_caddyfile: |
request_header -X-Internal-Token
header X-Frame-Options "DENY"
header X-Content-Type-Options "nosniff"
header Referrer-Policy "strict-origin-when-cross-origin"JSON:
caddy_extra_json: |
[
{
"handler": "headers",
"request": {"delete": ["X-Internal-Token"]}
},
{
"handler": "headers",
"response": {
"set": {
"X-Frame-Options": ["DENY"],
"X-Content-Type-Options": ["nosniff"],
"Referrer-Policy": ["strict-origin-when-cross-origin"]
}
}
}
](Yoink already emits Strict-Transport-Security for TLS sites by default; see the hsts: field in the proxy guide.)
Request body size limit
Block oversized request bodies before they reach the upstream.
Caddyfile:
caddy_extra_caddyfile: |
request_body {
max_size 10MB
}JSON:
caddy_extra_json: |
[{
"handler": "request_body",
"max_size": 10485760
}]Redirect a specific path
Permanent redirect of a specific URL to another. (For full canonical-domain redirects use the canonical_domain: field instead.)
Caddyfile:
caddy_extra_caddyfile: |
redir /old-path /new-path 308JSON:
caddy_extra_json: |
[{
"match": [{"path": ["/old-path"]}],
"handle": [{
"handler": "static_response",
"status_code": 308,
"headers": {"Location": ["/new-path"]}
}],
"terminal": true
}]Maintenance page
Force every request to a holding page for the duration of a maintenance window.
Caddyfile:
caddy_extra_caddyfile: |
respond "This service is undergoing scheduled maintenance. Back at 14:00 UTC." 503JSON:
caddy_extra_json: |
[{
"handler": "static_response",
"status_code": 503,
"body": "This service is undergoing scheduled maintenance. Back at 14:00 UTC.",
"headers": {"Content-Type": ["text/plain; charset=utf-8"]}
}]Plugins via proxy.xcaddy:
For directives outside stock Caddy (rate-limit, l4, redis-storage, third-party DNS providers), list plugins under proxy.xcaddy:. Yoink builds Caddy on each proxy host with those modules baked in; no registry, no operator-side Dockerfile. See proxy.xcaddy:.
Rate limiting (caddy-ratelimit)
proxy:
email: [email protected]
xcaddy:
plugins:
- github.com/mholt/caddy-ratelimit
services:
- name: public-api
domain: api.example.com
caddy_extra_json: |
[{
"handler": "rate_limit",
"zones": {"api": {"key": "{remote_host}", "events": 100, "window": "1m"}}
}]
run: { port: 8080 }Multi-host LE certs (caddy-storage-redis)
See the Multi-host Redis storage how-to — same proxy.xcaddy: story, different plugin.
See also
- Reverse proxy guide — the schema for
caddy_extra_json:,caddy_extra_caddyfile:,proxy.xcaddy:,proxy.tls:, and the auto-injection model. - Cloudflare Origin Certificates — skip Let's Encrypt with sealed Cloudflare certs.
- Caddy JSON config reference — for
caddy_extra_json:snippets. - Caddyfile docs — for
caddy_extra_caddyfile:syntax.
Caddy plugins (xcaddy, no registry)
Build a custom Caddy binary with rate-limit, l4, redis-storage, and third-party DNS providers on each host — no registry required.
Volume backups with restic
Nightly restic snapshots to any S3-compatible backend plus postgres WAL archiving for point-in-time recovery.