🪝 yoink

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 308

JSON:

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." 503

JSON:

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

On this page