Modern web server with automatic HTTPS. Configures TLS certificates from Let's Encrypt without any setup — just specify domain names and Caddy handles the rest.
Setup
bash
# Debian/Ubuntu
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
# Docker
docker run -d -p 80:80 -p 443:443 \
-v caddy_data:/data -v caddy_config:/config \
-v $PWD/Caddyfile:/etc/caddy/Caddyfile \
caddy:latest
Caddyfile (config format)
Simple Static Site
caddyfile
example.com {
root * /var/www/html
file_server
encode gzip zstd
}
That's it. Caddy automatically obtains and renews a TLS certificate for example.com.
Reverse Proxy
caddyfile
# Single backend
app.example.com {
reverse_proxy localhost:3000
}
# With health checks and load balancing
api.example.com {
reverse_proxy localhost:3001 localhost:3002 localhost:3003 {
lb_policy round_robin
health_uri /health
health_interval 10s
health_timeout 5s
}
}
# WebSocket support (automatic — no special config needed)
ws.example.com {
reverse_proxy localhost:8080
}
Multi-Service Setup
caddyfile
# Main app
example.com {
# API routes → backend
handle /api/* {
reverse_proxy localhost:3000
}
# Static assets → file server with caching
handle /static/* {
root * /var/www/static
file_server
header Cache-Control "public, max-age=31536000, immutable"
}
# Everything else → frontend SPA
handle {
root * /var/www/app
try_files {path} /index.html
file_server
}
encode gzip zstd
}
# Admin panel — separate subdomain
admin.example.com {
reverse_proxy localhost:3001
# Basic auth protection
basicauth {
admin $2a$14$... # bcrypt hash: caddy hash-password
}
}
# Redirect www to non-www
www.example.com {
redir https://example.com{uri} permanent
}
Security Headers
caddyfile
example.com {
reverse_proxy localhost:3000
header {
# Security headers
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
# Remove server identification
-Server
}
}
Rate Limiting
caddyfile
example.com {
# Rate limit: 100 requests per minute per IP
rate_limit {
zone api_zone {
key {remote_host}
events 100
window 1m
}
}
reverse_proxy localhost:3000
}
CORS
caddyfile
api.example.com {
@cors_preflight method OPTIONS
handle @cors_preflight {
header Access-Control-Allow-Origin "https://app.example.com"
header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header Access-Control-Allow-Headers "Content-Type, Authorization"
header Access-Control-Max-Age "86400"
respond "" 204
}
header Access-Control-Allow-Origin "https://app.example.com"
reverse_proxy localhost:3000
}
JSON Config (API mode)
For programmatic configuration instead of Caddyfile:
json
{
"apps": {
"http": {
"servers": {
"main": {
"listen": [":443"],
"routes": [
{
"match": [{"host": ["api.example.com"]}],
"handle": [{
"handler": "reverse_proxy",
"upstreams": [
{"dial": "localhost:3000"},
{"dial": "localhost:3001"}
],
"load_balancing": {"selection_policy": {"policy": "round_robin"}},
"health_checks": {
"active": {"uri": "/health", "interval": "10s"}
}
}]
}
]
}
}
}
}
}
Config API
Caddy exposes a REST API for live configuration changes — no restarts needed:
bash
# Load full config
curl -X POST http://localhost:2019/load \
-H "Content-Type: application/json" \
-d @caddy-config.json
# Get current config
curl http://localhost:2019/config/
# Update a specific route without touching anything else
curl -X PATCH "http://localhost:2019/config/apps/http/servers/main/routes/0" \
-H "Content-Type: application/json" \
-d '{"handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": "localhost:3002"}]}]}'
Common Patterns
Local Development with HTTPS
caddyfile
# Caddyfile.dev — local HTTPS with self-signed cert
localhost {
tls internal # Auto-generate a local CA cert
reverse_proxy /api/* localhost:3000
reverse_proxy localhost:5173 # Vite dev server
}
bash
caddy run --config Caddyfile.dev
# App available at https://localhost with valid (local) TLS
Wildcard Subdomains
caddyfile
# Requires DNS challenge (e.g., Cloudflare DNS plugin)
*.example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
# Route subdomains to different backends
@app host app.example.com
handle @app {
reverse_proxy localhost:3000
}
@docs host docs.example.com
handle @docs {
reverse_proxy localhost:3001
}
# Catch-all: 404
handle {
respond "Not found" 404
}
}
Caching Proxy
caddyfile
api.example.com {
reverse_proxy localhost:3000
# Cache responses from the backend
@cacheable {
method GET
path /api/public/*
}
header @cacheable Cache-Control "public, max-age=300" # 5 min cache
}
Caddy vs Nginx
| Caddy | Nginx | |
|---|---|---|
| HTTPS | Automatic (Let's Encrypt) | Manual cert setup or certbot |
| Config format | Caddyfile (simple) or JSON | nginx.conf (complex) |
| Hot reload | API-based, zero downtime | nginx -s reload |
| HTTP/3 | Built-in | Requires compilation flags |
| Plugins | Go modules, xcaddy build | C modules, recompilation |
| Performance | Fast, slightly slower than Nginx | Fastest for static files |
| Use case | Modern apps, auto-HTTPS | Legacy, high-traffic static |
Commands
bash
caddy run # Foreground with Caddyfile in current dir
caddy start # Background daemon
caddy stop # Stop daemon
caddy reload # Reload config without downtime
caddy adapt --config Caddyfile # Convert Caddyfile to JSON (debugging)
caddy validate --config Caddyfile # Check for syntax errors
caddy hash-password # Generate bcrypt hash for basicauth
caddy fmt --overwrite Caddyfile # Format Caddyfile
Guidelines
- Automatic HTTPS is the default — don't disable it unless you have a specific reason. Caddy handles cert issuance, renewal, and OCSP stapling.
- Use
try_filesfor SPAs —try_files {path} /index.htmlserves the SPA shell for all routes, letting the frontend router handle them. - Caddyfile for humans, JSON for automation — Caddyfile is easier to read and maintain; JSON API is for programmatic management.
- Config API is live — changes via the admin API take effect immediately without restart or reload.
- DNS challenge for wildcards — wildcard certs require DNS-01 challenge. Install the appropriate DNS provider plugin via
xcaddy. encode gzip zstdon every site — compression is free performance. Zstandard is faster than gzip for modern clients.- Health checks on reverse proxy — always configure them for multi-backend setups to avoid routing to dead backends.
-Serverheader — remove it in production to avoid leaking server info.