caddy run (foreground) or caddy start (background), caddy reload to apply changessetcap on Linux if not root./data directory stores certificates and must be persistent (mount in Docker)./config directory stores active config — also mount in Docker.lb_try_interval 0 with non-zero lb_try_duration.| Directive | Purpose | Example |
|---|---|---|
reverse_proxy | Proxy to backend | reverse_proxy localhost:3000 |
file_server | Serve static files | file_server or file_server browse |
root | Document root | root * /var/www/html |
tls | Configure TLS | tls [email protected] |
encode | Compression | encode gzip zstd |
header | Response headers | header X-Frame-Options DENY |
log | Access logs | log { output file /var/log/caddy/access.log } |
basicauth | HTTP basic auth | basicauth /admin/* { ... } |
redir | HTTP redirect | redir https://new.example.com{uri} |
handle | Exclusive route group | handle /api/* { ... } |
handle_path | Handle + strip prefix | handle_path /api/* { ... } |
import | Include snippets | import common_headers |
lb_policy | Load balancing | lb_policy least_conn |
php_fastcgi | PHP FastCGI | php_fastcgi localhost:9000 |
START
├── Need automatic HTTPS with zero config?
│ ├── YES → Use domain name as site address (auto-provisioned)
│ └── NO ↓
├── Reverse proxy to single backend?
│ ├── YES → example.com { reverse_proxy localhost:3000 }
│ └── NO ↓
├── Multiple backends?
│ ├── YES → reverse_proxy with multiple upstreams + lb_policy
│ └── NO ↓
├── Serving static files?
│ ├── YES → root * /path + file_server
│ └── NO ↓
├── PHP application?
│ ├── YES → php_fastcgi localhost:9000
│ └── NO ↓
└── DEFAULT → reverse_proxy + file_server combo
Install on your platform. [src6]
# Ubuntu/Debian
sudo apt install caddy
# macOS
brew install caddy
# Docker
docker run -d -p 80:80 -p 443:443 -v caddy_data:/data caddy:2
Verify: caddy version → v2.8.x
Write minimal reverse proxy. [src1]
example.com {
reverse_proxy localhost:3000
}
Verify: caddy validate → Valid configuration
Serve static assets alongside proxy. [src4]
example.com {
handle /static/* {
root * /var/www
file_server
}
handle {
reverse_proxy localhost:3000
}
encode gzip zstd
header {
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
-Server
}
}
Verify: caddy reload
Multiple backends with health checks. [src2]
api.example.com {
reverse_proxy {
to localhost:3001
to localhost:3002
lb_policy least_conn
health_uri /health
health_interval 10s
}
}
Verify: caddy reload, check balanced traffic
# Input: React/Vue SPA + backend API
# Output: SPA files served, /api proxied to backend
example.com {
handle_path /api/* {
reverse_proxy localhost:4000
}
handle {
root * /var/www/spa
try_files {path} /index.html
file_server
}
encode gzip
}
# Input: PHP app (Laravel, WordPress)
# Output: Caddy serving PHP via FastCGI
example.com {
root * /var/www/html
php_fastcgi localhost:9000
file_server
encode gzip
@blocked path /.env /.git/*
respond @blocked 404
}
# Input: Multiple sites
# Output: Shared security headers via snippet
(security) {
header {
X-Frame-Options "SAMEORIGIN"
Strict-Transport-Security "max-age=63072000"
-Server
}
}
app.example.com {
import security
reverse_proxy localhost:3000
}
api.example.com {
import security
reverse_proxy localhost:4000
}
# ❌ BAD — unnecessary manual TLS
example.com {
tls /etc/ssl/cert.pem /etc/ssl/key.pem
}
# ✅ GOOD — auto-provision, manage, and renew
example.com {
reverse_proxy localhost:3000
}
# ❌ BAD — ACME doesn't issue certs for IPs
192.168.1.100 {
reverse_proxy localhost:3000
}
# ✅ GOOD — domain triggers auto-HTTPS
app.example.com {
reverse_proxy localhost:3000
}
# ❌ BAD — certificates lost on restart
services:
caddy:
image: caddy:2
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
# ✅ GOOD — certificates survive restarts
services:
caddy:
image: caddy:2
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
dig example.com first. [src3]http_port/https_port global options. [src5]/data as named volume. [src6]handle preserves URI, handle_path strips prefix. Fix: use handle_path when backend expects stripped paths. [src4]route { } for explicit ordering. [src1]# Validate Caddyfile
caddy validate --config /etc/caddy/Caddyfile
# Start in foreground
caddy run --config /etc/caddy/Caddyfile
# Reload config (zero-downtime)
caddy reload --config /etc/caddy/Caddyfile
# List loaded modules
caddy list-modules
# Check if ports available
ss -tlnp | grep -E ':(80|443)'
# Test HTTPS
curl -I https://example.com
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Caddy 2.8.x | Current | None | Recommended version |
| Caddy 2.7.x | Supported | None | — |
| Caddy 2.0 | GA (2020) | Complete rewrite from v1 | Caddyfile syntax incompatible with v1 |
| Caddy 1.x | EOL | — | Must migrate to v2 |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Automatic HTTPS with zero config | Granular control over every setting | nginx |
| Quick reverse proxy | High-perf CDN (millions req/s) | nginx + CloudFront |
| Docker/K8s easy config | Extensive module ecosystem | nginx |
| PHP hosting with FastCGI | Raw TCP/UDP proxying | HAProxy |
| Dev HTTPS with local CA | Enterprise WAF | nginx + ModSecurity |
tls internal for private domains.route {} for explicit ordering.