Caddy’s reverse_proxy directive covers everything from “send this domain to port 3000” to multi-backend load balancing with health checks — with automatic HTTPS in front. These are the patterns you’ll use most. (New to Caddy? Start with installing Caddy.)
One domain, one app
app.example.com {
reverse_proxy 127.0.0.1:3000
}
Caddy terminates TLS, forwards the request, and sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host. The original Host header is passed through. WebSocket upgrades work automatically.
Bind your app to 127.0.0.1 so it’s only reachable through Caddy.
Several apps on one server
app.example.com {
reverse_proxy 127.0.0.1:3000
}
api.example.com {
reverse_proxy 127.0.0.1:8000
}
grafana.example.com {
reverse_proxy 127.0.0.1:3001
}
Each site gets its own certificate.
Path-based routing
example.com {
handle /api/* {
reverse_proxy 127.0.0.1:8000
}
handle_path /admin/* {
reverse_proxy 127.0.0.1:9000
}
handle {
root * /var/www/frontend
try_files {path} /index.html
file_server
}
}
handlekeeps the path (/api/users→ backend receives/api/users).handle_pathstrips the matched prefix (/admin/settings→ backend receives/settings).- The final
handleserves a single-page app with fallback toindex.html.
Load balancing
api.example.com {
reverse_proxy 10.0.0.11:8000 10.0.0.12:8000 10.0.0.13:8000 {
lb_policy least_conn
lb_retries 2
health_uri /health
health_interval 10s
health_timeout 3s
fail_duration 30s
}
}
- Active health checks (
health_uri) poll each backend and stop sending traffic to unhealthy ones. - Passive health checks (
fail_duration) mark a backend down after failed requests. - Policies:
random(default),round_robin,least_conn,ip_hash,cookie(sticky sessions),first(failover).
Headers
app.example.com {
reverse_proxy 127.0.0.1:3000 {
header_up X-Real-IP {remote_host}
header_down -Server
}
header {
Strict-Transport-Security "max-age=31536000"
X-Content-Type-Options nosniff
-X-Powered-By
}
}
header_up modifies the request to the backend; header_down the response from it; header sets response headers for the client.
Backends over HTTPS
reverse_proxy https://10.0.0.20:8443 {
transport http {
tls_server_name internal.example.com
# tls_insecure_skip_verify # only for self-signed test backends
}
}
Timeouts and large uploads
files.example.com {
request_body {
max_size 2GB
}
reverse_proxy 127.0.0.1:8080 {
transport http {
read_timeout 10m
write_timeout 10m
}
flush_interval -1 # stream responses immediately (SSE, progress)
}
}
Authentication and IP restrictions
admin.example.com {
@outside not remote_ip 203.0.113.0/24 10.8.0.0/24
respond @outside "Forbidden" 403
basic_auth {
alice $2a$14$… # caddy hash-password
}
reverse_proxy 127.0.0.1:9000
}
For SSO, forward_auth integrates with Authelia or Authentik — see Authelia.
Behind Cloudflare or another proxy
Tell Caddy which proxies to trust for client IPs:
{
servers {
trusted_proxies static 173.245.48.0/20 103.21.244.0/22
}
}
(List all of Cloudflare’s published ranges, or use a module that fetches them.)
Reusable snippets
(proxy_defaults) {
encode zstd gzip
header -Server
}
app.example.com {
import proxy_defaults
reverse_proxy 127.0.0.1:3000
}
Troubleshooting
- 502 — the backend isn’t listening where you said; test with
curl http://127.0.0.1:3000. - Redirect loops — the app forces HTTPS but doesn’t trust
X-Forwarded-Proto; configure its proxy settings. - Certificate errors — DNS not pointing to the server, or ports 80/443 blocked; check
journalctl -u caddy.
Related: Caddy with Docker, Nginx reverse proxy and HAProxy.