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

Caddy: HTTPS with two lines — The easier Nginx + Certbot.

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

TaskWhat
Config fileCaddyfile
A static siteexample.com { root * /srv/site file_server }
A reverse proxystatus.example.com { reverse_proxy 127.0.0.1:3001 }
Apply changesdocker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
Needsports 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.

Call us