How to Use Caddy for Automatic HTTPS (The Easier Nginx Alternative)

Nginx plus Certbot (tutorials 26 to 28) is the classic setup, and it works. Caddy does the same job with a fraction of the configuration: it fetches certificates from Let's Encrypt by itself, renews them, redirects http to https, and a reverse proxy is one line. If you're starting fresh or tired of server blocks, it's hard to argue with.
Quick reference
| Task | What |
|---|---|
| Config file | Caddyfile |
| A static site | example.com { root * /srv/site file_server } |
| A reverse proxy | status.example.com { reverse_proxy 127.0.0.1:3001 } |
| Apply changes | docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile |
| Needs | ports 80 and 443 reachable from the internet, DNS pointing at you |
1. Run it in Docker
If Nginx is installed, stop it first (sudo systemctl disable --now nginx) — only one thing can own ports 80 and 443.
mkdir -p ~/docker/caddy && cd ~/docker/caddy
nano compose.yaml
services:
caddy:
image: caddy:2
container_name: caddy
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./data:/data
- ./config:/config
- /srv/www:/srv/www:ro
restart: unless-stopped
./data holds the certificates — keep it in backups so you don't hit Let's Encrypt rate limits after a rebuild.
2. The Caddyfile
nano Caddyfile
example.com {
root * /srv/www/example.com
file_server
}
status.example.com {
reverse_proxy 127.0.0.1:3001
}
cloud.example.com {
reverse_proxy 127.0.0.1:8080
request_body {
max_size 10GB
}
}
That's three sites, all with HTTPS, http→https redirects, and the forwarding headers apps expect, with nothing else written down. WebSockets work without being asked.
docker compose up -d
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp
docker compose logs -f
Watch for "certificate obtained successfully". Browse to each name: padlock.
3. Changing things
Edit the Caddyfile, then:
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
No restart, no dropped connections.
4. Apps on the same Docker network
If Caddy and the app are in the same compose file (or a shared network), proxy by container name and skip the host port entirely:
reverse_proxy uptime-kuma:3001
5. Internal-only names
For services you only reach over the VPN, use Caddy's internal certificate authority:
nas.home.lan {
tls internal
reverse_proxy 127.0.0.1:9443
}
Devices will warn once about the local CA; install Caddy's root certificate (./data/caddy/pki/authorities/local/root.crt) to stop that. Or use the DNS challenge with a Cloudflare-enabled Caddy image for real certificates on private names.
Useful one-liners
basicauth { john $2a$14$...hash... } # password-protect (hash from: caddy hash-password)
redir https://www.example.com{uri} # redirect a bare domain
encode gzip # compress responses
log # access log to stdout
Troubleshooting
"challenge failed" in the logs. Port 80 or 443 isn't reachable from outside — router forward, firewall, or DNS pointing elsewhere. Same checks as Certbot (tutorial 27).
Hitting the rate limit while experimenting. Add acme_ca https://acme-staging-v02.api.letsencrypt.org/directory in a global { } block at the top until the config is right.
App behind the proxy shows a redirect loop. The app has its own https redirect. Turn it off; Caddy already does it.
Site serves "404 Not Found" for everything. root path doesn't match the volume mount inside the container.
Wrapping up
A compose file, a Caddyfile with one block per name, up -d. Certificates, redirects and headers are handled before you've finished reading the log. If you've never set up Nginx, start here; if you have, you'll see why people switch.
