Reverse proxy with automatic HTTPS: Caddy for self-hosters
Once you're running more than one service (see
self-hosting for beginners),
the :8096, :8080, :3000 port soup gets old
fast — and none of it has HTTPS. A reverse proxy fixes both: one
process listens on ports 80/443, terminates TLS, and forwards
jellyfin.example.com to one container and
photos.example.com to another. Every service gets a clean hostname and
a padlock.
We recommend Caddy for self-hosters. Nginx and Traefik are excellent (more below), but Caddy's defining feature — automatic HTTPS with certificates it obtains and renews itself, zero configuration — removes the single biggest source of pain in this whole area.
Prerequisites
- A domain, with DNS you control (Cloudflare's free tier is fine).
- A server with ports 80 and 443 reachable from the internet — a VPS has this by default; at home you need router port-forwards and a non-CGNAT connection. (If that's not you, Tailscale + Caddy's internal CA works entirely privately — different article.)
- Your services running, ideally in Docker Compose, not publishing ports to the host — the proxy will reach them over the Docker network.
1. DNS
Point each hostname at your server's IP. A wildcard saves future trips:
A *.example.com 203.0.113.10 A example.com 203.0.113.10
Using Cloudflare? Set these to DNS only (grey cloud) for now. Proxied mode works with Caddy but changes how certificates are validated — get the simple version working first.
2. Firewall
sudo ufw allow 80,443/tcp
(Everything else stays closed, per the hardening guide. Your services' ports are never exposed — only Caddy's.)
3. Caddy in Docker Compose
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data # certificates live here — persist it!
- caddy_config:/config
networks:
- proxy
networks:
proxy:
external: true
volumes:
caddy_data:
caddy_config:
Create the shared network once (docker network create proxy) and
attach each service's compose file to it with the same
networks: [proxy] block. Now Caddy can reach containers by their
service name.
4. The Caddyfile
This is the whole configuration for three services with HTTPS:
jellyfin.example.com {
reverse_proxy jellyfin:8096
}
photos.example.com {
reverse_proxy immich-server:2283
}
cloud.example.com {
reverse_proxy nextcloud:80
}
That's not a snippet — that's the file. On docker compose up -d,
Caddy requests a Let's Encrypt certificate for each hostname, serves it, redirects
HTTP → HTTPS, and renews automatically for as long as it runs. Change the file, run
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile,
done.
Useful additions you'll want eventually:
# basic auth in front of something that has none
admin.example.com {
basic_auth {
rob $2a$14$...bcrypt-hash-from-`caddy hash-password`...
}
reverse_proxy some-dashboard:8080
}
# only allow your LAN / VPN to reach a service
internal.example.com {
@denied not remote_ip 192.168.1.0/24 100.64.0.0/10
respond @denied 403
reverse_proxy private-thing:9000
}
# serve a static site straight from a folder
example.com {
root * /srv/www
file_server
}
5. Check it worked
curl -I https://jellyfin.example.com # HTTP/2 200 ... server: Caddy docker compose logs caddy | grep -i certificate
Cert failing? 95% of the time it's one of: DNS not propagated yet, port 80 blocked (Let's Encrypt's HTTP challenge needs it), or Cloudflare proxy turned on. Rate-limit warnings mean you've retried too often — wait an hour.
Caddy vs Nginx vs Traefik
- Caddy — simplest by a distance; automatic HTTPS; config you can read a year later. Our pick unless you have a reason not to.
- Nginx (+ Certbot) — the industry standard, endlessly documented, but certificates are a separate moving part you must set up and keep renewing. Nginx Proxy Manager wraps it in a web UI, which many self-hosters like.
- Traefik — configures itself from Docker labels; wonderful once mastered, steeper to learn, and its config lives scattered across every compose file. Best when services come and go constantly.
Whichever you pick, the architecture is the same — one door, TLS terminated in one place, services on a private network behind it. It's the moment a pile of containers starts feeling like infrastructure.