Caddy: The Easiest Self-Hosted Web Server with Automatic HTTPS
Caddy is a modern web server that does something no other server does out of the box: it automatically provisions and renews HTTPS certificates for every site you configure. No certbot cron jobs, no manual Let's Encrypt setup, no expired certificate surprises at 3 AM.
For self-hosters running multiple services behind a reverse proxy, Caddy can be a dramatically simpler alternative to Nginx or Apache.
Why Caddy?
The self-hosting reverse proxy landscape has several strong options. Here's where Caddy fits:
- Automatic HTTPS — Caddy obtains and renews Let's Encrypt (or ZeroSSL) certificates automatically, with zero configuration
- Simple configuration — The Caddyfile format is readable and concise
- HTTP/3 support — Built-in QUIC/HTTP3 support
- Single binary — No dependencies, easy to deploy
- API-driven — Full REST API for dynamic configuration changes
The trade-off compared to Nginx: Caddy uses slightly more memory and has fewer third-party modules. Compared to Traefik: Caddy doesn't auto-discover Docker containers (though plugins exist for this).
Installation with Docker
# docker-compose.yml
services:
caddy:
image: caddy:2
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
volumes:
caddy_data: # Stores certificates
caddy_config: # Stores configuration state
The caddy_data volume is critical — it stores your TLS certificates. Don't delete it or you'll re-request certs (and potentially hit rate limits).
Basic Caddyfile Configuration
Static File Server
yourdomain.com {
root * /srv/www
file_server
}
That's it. Caddy will:
- Obtain a TLS certificate for
yourdomain.com - Redirect HTTP to HTTPS
- Serve files from
/srv/www - Renew the certificate automatically before it expires
Reverse Proxy
The most common self-hosting use case — proxying to internal services:
grafana.yourdomain.com {
reverse_proxy grafana:3000
}
nextcloud.yourdomain.com {
reverse_proxy nextcloud:80
}
jellyfin.yourdomain.com {
reverse_proxy jellyfin:8096
}
Each subdomain automatically gets its own HTTPS certificate. No certificate configuration needed.
Reverse Proxy with Path-Based Routing
If you prefer paths over subdomains:
yourdomain.com {
handle /grafana/* {
uri strip_prefix /grafana
reverse_proxy grafana:3000
}
handle /jellyfin/* {
reverse_proxy jellyfin:8096
}
handle {
root * /srv/www
file_server
}
}
Advanced Configuration
Headers and Security
yourdomain.com {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
-Server # Remove server header
}
reverse_proxy app:8080
}
Basic Authentication
admin.yourdomain.com {
basicauth {
admin $2a$14$... # bcrypt hash
}
reverse_proxy admin-panel:8080
}
Generate the hash with:
docker exec -it caddy caddy hash-password
Rate Limiting
api.yourdomain.com {
rate_limit {
zone api {
key {remote_host}
events 100
window 1m
}
}
reverse_proxy api:3000
}
Wildcard Certificates
For many subdomains, a wildcard cert avoids individual certificate requests:
*.yourdomain.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
@grafana host grafana.yourdomain.com
handle @grafana {
reverse_proxy grafana:3000
}
@nextcloud host nextcloud.yourdomain.com
handle @nextcloud {
reverse_proxy nextcloud:80
}
}
Wildcard certs require DNS challenge validation. Caddy supports DNS providers through plugins — the Cloudflare plugin is the most popular.
Using Caddy with Cloudflare DNS Plugin
Build a custom Caddy image with the Cloudflare DNS module:
FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
Then in your Caddyfile:
{
acme_dns cloudflare {env.CF_API_TOKEN}
}
Caddy vs Nginx vs Traefik
| Feature | Caddy | Nginx | Traefik |
|---|---|---|---|
| Auto HTTPS | Built-in | Manual (certbot) | Built-in |
| Config format | Caddyfile (simple) | nginx.conf (verbose) | YAML/TOML/labels |
| Docker integration | Manual/plugin | Manual | Native (labels) |
| Performance | Good | Excellent | Good |
| Memory usage | ~30 MB | ~10 MB | ~50 MB |
| HTTP/3 | Built-in | Experimental | Built-in |
| Community size | Growing | Massive | Large |
| Middleware | Plugins | Modules | Extensive |
| Dashboard | No (API only) | No (paid Plus) | Yes (built-in) |
| Learning curve | Low | Medium | Medium-High |
When to Choose Caddy
- You want automatic HTTPS without thinking about it
- You prefer simple, readable configuration
- You're setting up a small-to-medium number of services
- You value simplicity over maximum performance
When to Choose Nginx
- You need maximum performance and minimal resource usage
- You're already familiar with nginx.conf
- You need specific Nginx modules (e.g., ngx_pagespeed)
- You're running a high-traffic production site
When to Choose Traefik
- You run many Docker containers and want auto-discovery
- You need a visual dashboard
- You want label-based configuration in docker-compose
- You're in a Kubernetes environment
Common Patterns for Self-Hosters
All-in-One with Docker Compose
services:
caddy:
image: caddy:2
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
restart: unless-stopped
networks:
- proxy
whoami:
image: traefik/whoami
networks:
- proxy
networks:
proxy:
name: proxy
volumes:
caddy_data:
caddy_config:
Put all your services on the proxy network and reference them by container name in the Caddyfile.
Local HTTPS for Development
Caddy can issue locally-trusted certificates for development:
localhost {
reverse_proxy app:3000
}
Caddy installs a local CA and issues certificates that your browser trusts. No more "your connection is not private" warnings during development.
Migrating from Nginx
The translation is usually straightforward:
Nginx:
server {
listen 80;
server_name app.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Caddy:
app.example.com {
reverse_proxy localhost:8080
}
Caddy handles the HTTP→HTTPS redirect, certificate provisioning, and proxy headers automatically.
Verdict
Caddy is the best choice for self-hosters who want a reverse proxy that just works. The automatic HTTPS alone saves hours of certificate management headaches. The Caddyfile format is the most readable of any web server configuration.
If you're starting fresh with a self-hosted setup and don't need Traefik's Docker auto-discovery or Nginx's raw performance, Caddy is the easiest path to a secure, well-configured reverse proxy. The five minutes you spend writing a Caddyfile will save you hours of debugging certificate renewals and nginx configuration.