Caddy Server Configuration Reference
Caddy server configuration reference
TL;DR
- Bottom line: Caddy is a modern web server with automatic HTTPS out of the box — a production Caddyfile is typically 15-25% the size of an equivalent nginx config.
- Key tool/command:
caddy run(foreground) orcaddy start(background),caddy reloadto apply changes - Watch out for: DNS must point to your server BEFORE Caddy obtains certificates — otherwise the ACME challenge fails.
- Works with: Caddy 2.8+, Let's Encrypt / ZeroSSL (automatic), any backend (HTTP, gRPC, WebSocket, FastCGI).
Constraints
- Caddy needs ports 80 and 443 for automatic HTTPS — use
setcapon Linux if not root. - DNS must resolve to the server before enabling HTTPS.
- The
/datadirectory stores certificates and must be persistent (mount in Docker). - The
/configdirectory stores active config — also mount in Docker. - Do not set
lb_try_interval 0with non-zerolb_try_duration.
Quick Reference
| 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 |
Decision Tree
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
Step-by-Step Guide
1. Install Caddy
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
2. Create basic Caddyfile
Write minimal reverse proxy. [src1]
example.com {
reverse_proxy localhost:3000
}
Verify: caddy validate → Valid configuration
3. Add static files and security headers
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
4. Configure load balancing
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
Code Examples
SPA with API proxy
# 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
}
PHP with FastCGI
# 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
}
Multi-site with shared snippets
# 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
}
Anti-Patterns
Wrong: Manual TLS when auto-HTTPS works
# ❌ BAD — unnecessary manual TLS
example.com {
tls /etc/ssl/cert.pem /etc/ssl/key.pem
}
Correct: Let Caddy handle HTTPS automatically
# ✅ GOOD — auto-provision, manage, and renew
example.com {
reverse_proxy localhost:3000
}
Wrong: IP address expecting HTTPS
# ❌ BAD — ACME doesn't issue certs for IPs
192.168.1.100 {
reverse_proxy localhost:3000
}
Correct: Use domain name
# ✅ GOOD — domain triggers auto-HTTPS
app.example.com {
reverse_proxy localhost:3000
}
Wrong: Missing Docker volumes
# ❌ BAD — certificates lost on restart
services:
caddy:
image: caddy:2
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
Correct: Persist /data and /config
# ✅ 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:
Common Pitfalls
- DNS not pointing before first run: ACME fails, no HTTPS. Fix: verify DNS with
dig example.comfirst. [src3] - Port 80/443 already in use: Another server blocking Caddy. Fix: stop it or use
http_port/https_portglobal options. [src5] - Missing Docker /data volume: Certs re-requested on restart, hitting rate limits. Fix: mount
/dataas named volume. [src6] - handle vs handle_path:
handlepreserves URI,handle_pathstrips prefix. Fix: usehandle_pathwhen backend expects stripped paths. [src4] - Directive ordering: Caddy has implicit fixed order. Fix: use
route { }for explicit ordering. [src1] - Let's Encrypt rate limits: >50 certs/domain/week. Fix: use staging CA for testing. [src3]
Diagnostic Commands
# 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 History & Compatibility
| 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 |
When to Use / When Not to Use
| 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 |
Important Caveats
- Caddy 2.x Caddyfile syntax is completely incompatible with Caddy 1.x.
- Automatic HTTPS only works for publicly resolvable domains — use
tls internalfor private domains. - Losing the /data directory forces certificate re-issuance, which can hit rate limits.
- Directive ordering is implicit and fixed — use
route {}for explicit ordering.