HTTPS for all your apps with Docker and Caddy
I used nginx with certbot for years. Then I tried Caddy and stopped writing proxy configs. It fetches and renews Let's Encrypt certificates on its own, and a whole site config is three lines.
This setup runs Caddy in front of Uptime Kuma, but the pattern works for any app.
Before you start
- Docker with the compose plugin installed
- A domain with an A record pointing at your server, e.g.
status.example.com - Ports 80 and 443 open (see the first 15 minutes post)
The compose file
mkdir -p ~/stack && cd ~/stack nano compose.yaml
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
uptime-kuma:
image: louislam/uptime-kuma:1
restart: unless-stopped
volumes:
- kuma:/app/data
volumes:
caddy_data:
caddy_config:
kuma:
Uptime Kuma has no ports: section at all. Only Caddy can reach it, over the internal Docker network. That also gets around the "Docker ignores ufw" problem: if a port isn't published, there's nothing to leak.
The Caddyfile
status.example.com {
reverse_proxy uptime-kuma:3001
}
That's all. uptime-kuma is the service name from the compose file, and Docker's DNS resolves it for you.
Start it
docker compose up -d docker compose logs -f caddy
Watch the log for a line saying the certificate was obtained. If it's stuck, it's almost always DNS (the record isn't pointing at the server yet) or port 80 being blocked.
Adding the next app
Add the service to compose.yaml without publishing any ports, then add a block to the Caddyfile:
vault.example.com {
reverse_proxy vaultwarden:80
}
docker compose up -d docker compose exec -w /etc/caddy caddy caddy reload
Don't lose the caddy_data volume. Your certificates live there, and if you wipe it Caddy has to request them all again. Do that too many times and Let's Encrypt will rate-limit you for a while.