Linux & Hosting

Reverse proxy with automatic HTTPS: Caddy for self-hosters

Updated August 2026

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.