Skip to content
Go back

Caddyfile Patterns That Actually Work

By SumGuy 5 min read
Caddyfile Patterns That Actually Work

The Caddyfile Cookbook

You’ve got Caddy running. You know it auto-handles HTTPS. Now you’re staring at a blank Caddyfile wondering how to do the thing you actually need to do.

This is that reference. Bookmark it, keep it open in a tab, steal from it freely.

Each pattern is minimal and working. Adapt the domain names and paths to your setup.


Multi-Site on One Server

Run multiple sites from a single Caddyfile — no virtual host gymnastics required.

Caddyfile
app.example.com {
reverse_proxy localhost:3000
}
files.example.com {
root * /srv/files
file_server browse
}
blog.example.com {
reverse_proxy localhost:4321
}

Each site block is independent. Caddy handles HTTPS for all three automatically.


Reverse Proxy with Custom Headers

Pass the real client IP upstream and strip headers you don’t want leaking to internal services.

Caddyfile
app.example.com {
reverse_proxy localhost:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
header_down -X-Powered-By
header_down -Server
}
}

header_up adds/sets request headers sent to the backend. header_down with a - prefix strips response headers from what the client sees. Hide the fact you’re running PHP 7.2 from 2019.


Basic Auth on a Specific Path

Protect /admin without locking down the whole site. Users hit the public site normally; /admin prompts for credentials.

Caddyfile
example.com {
reverse_proxy localhost:3000
basicauth /admin* {
# Generate hash: caddy hash-password
alice $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
}
}

Generate the password hash with caddy hash-password --plaintext yourpassword. The * glob means /admin, /admin/settings, /admin/users — all protected.


Redirects and Rewrites

301 Redirect (old slug → new slug)

Caddyfile
example.com {
redir /old-page /new-page 301
reverse_proxy localhost:3000
}

www to non-www

Caddyfile
www.example.com {
redir https://example.com{uri} 301
}
example.com {
reverse_proxy localhost:3000
}

Path rewrite (strip prefix before proxying)

Caddyfile
example.com {
handle /api/* {
uri strip_prefix /api
reverse_proxy localhost:8080
}
}

The backend sees /users instead of /api/users. Useful when your app doesn’t know it’s mounted at a subpath.


File Server with Directory Listing

Instant file share — drop files in a folder, browse them in a browser.

Caddyfile
files.example.com {
root * /srv/files
file_server browse
}

browse enables the directory listing UI. Remove it if you want file serving without listing (404 on directories).


Health Check Endpoint That Bypasses Auth

Your monitoring tool needs to hit /health but you’ve got basicauth on the site. Handle the health check route first.

Caddyfile
example.com {
handle /health {
respond "OK" 200
}
basicauth * {
alice $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
}
reverse_proxy localhost:3000
}

handle blocks are evaluated in order. The /health route matches first and short-circuits before auth runs.


Rate Limiting (rate_limit plugin)

Requires the caddy-ratelimit plugin. If you built Caddy with xcaddy or used a custom build, add it there. The community Docker image includes it.

Caddyfile
api.example.com {
rate_limit {
zone api_zone {
key {remote_host}
events 100
window 1m
}
}
reverse_proxy localhost:8080
}

100 requests per minute per IP. Exceed it and you get a 429. Tune events and window to your traffic patterns — don’t be surprised when your own health checks trip this.


Structured JSON Logging

Default Caddy logs are human-readable. For log aggregation (Loki, Elasticsearch, whatever you’re running), JSON is easier to parse.

Caddyfile
{
log {
output file /var/log/caddy/access.log
format json
}
}
example.com {
log {
output file /var/log/caddy/example.log
format json
}
reverse_proxy localhost:3000
}

The global log block sets the default. Per-site log blocks let you split logs by domain. Each line is a JSON object — pipe it to jq for sanity checks.


Serving a Single-Page App (SPA)

React, Vue, Svelte — they all need the server to return index.html for unknown paths so client-side routing works.

Caddyfile
app.example.com {
root * /srv/app
try_files {path} /index.html
file_server
}

try_files is Caddy’s equivalent of nginx’s try_files $uri $uri/ /index.html. It tries the exact path first, then falls back to index.html. Static assets (.js, .css) are served directly; routes like /dashboard/settings fall through to the SPA.


Environment Variables in Caddyfile

Stop hardcoding secrets and paths. Caddy reads from the environment with {env.VAR_NAME}.

Caddyfile
{env.MY_DOMAIN} {
reverse_proxy {env.BACKEND_HOST}:{env.BACKEND_PORT}
basicauth /admin* {
{env.ADMIN_USER} {env.ADMIN_HASH}
}
}

Set variables before starting Caddy or in your Docker Compose environment: block. Useful for sharing a Caddyfile across environments without modification.


Wildcard Certificates with DNS Challenge (Cloudflare)

When you need *.example.com covered by a single cert — useful for dynamic subdomains or internal services you don’t want to expose individually.

Requires the Cloudflare DNS provider plugin. Build with xcaddy:

Terminal window
xcaddy build --with github.com/caddy-dns/cloudflare

Then in your Caddyfile:

Caddyfile
{
acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
*.example.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
@app host app.example.com
handle @app {
reverse_proxy localhost:3000
}
@files host files.example.com
handle @files {
root * /srv/files
file_server browse
}
}

Named matchers (@app, @files) let you route within a wildcard block based on hostname. One cert, many subdomains, zero port 80 challenges.


One More Thing

Caddy reloads its config without restarting:

Terminal window
caddy reload --config /etc/caddy/Caddyfile

No downtime, no dropped connections. Make a change, reload, verify. It’s genuinely one of the better parts of working with Caddy — your 2 AM self running quick fixes will appreciate not having to restart a process that’s handling live traffic.

Combine these patterns freely. Caddy’s config model composes cleanly — site blocks, matchers, and handlers stack without fighting each other.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
TLS 1.3: Modern Encryption Without the Existential Dread
Next Post
IPFS: Peer-to-Peer File Storage for People Who've Seen Too Many 404s

Related Posts