Running Caddy Server: The Zero-Config HTTPS Reverse Proxy

You’re running a dozen Docker containers. Each one listens on a different port. You’re tired of remembering 192.168.1.50:8096 for Jellyfin and :3000 for Gitea. You want jellyfin.yourdomain.com with HTTPS — and you don’t want to wrestle with Nginx config files or manually renew Let’s Encrypt certificates.

Caddy does all of this automatically. Point a domain at it, tell it where your service lives, and it handles HTTPS certificates, renewal, and reverse proxying with almost zero configuration.

Why Caddy Over Nginx or Traefik

Every reverse proxy gets the job done. Here’s why Caddy stands out for self-hosters:

  • Automatic HTTPS — Caddy obtains and renews Let’s Encrypt (or ZeroSSL) certificates with zero config. No certbot, no cron jobs, no renewal scripts.
  • Dead-simple config — A reverse proxy rule is literally two lines in a Caddyfile.
  • HTTP/3 support — Enabled by default. Your services get QUIC support for free.
  • No external dependencies — Single binary. No Lua modules, no separate certificate managers.

The trade-off: fewer community plugins than Nginx, and less flexible routing than Traefik for complex microservice setups. For most self-hosters running 5–30 services behind a single domain, Caddy is the sweet spot.

Prerequisites

  • A Linux server (Ubuntu/Debian recommended) with Docker and Docker Compose installed
  • A domain name with DNS pointed to your server’s public IP
  • Ports 80 and 443 open on your firewall/router
  • Basic familiarity with Docker Compose files

Setting Up Caddy with Docker Compose

Create a directory for your Caddy setup:

mkdir -p ~/caddy && cd ~/caddy

Create a docker-compose.yml:

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"  # HTTP/3
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - caddy

networks:
  caddy:
    name: caddy
    driver: bridge

volumes:
  caddy_data:
  caddy_config:

Key details:

  • Port 443/udp enables HTTP/3 (QUIC). Optional but free performance.
  • caddy_data stores your TLS certificates. Back this volume up.
  • caddy_config stores Caddy’s runtime configuration.
  • The caddy network lets other containers connect to Caddy without exposing their ports to the host.

Writing Your Caddyfile

Create a Caddyfile in the same directory:

j}g}n}eieltxletyrarcrfe.eleivyvovneoeue.rurdrysrs.soedeyeu_o_o_rpmpupdrarrrooiodomxnxoxay.ymyicanjogin.emineclt.xol{ectmyaocf:ml{i3on0{u:0d80:08906

That’s it. Each block is a domain name, and inside it you tell Caddy where to forward traffic. No ssl_certificate directives, no proxy_pass with upstream blocks, no location / nesting.

When Caddy starts, it will:

  1. Check that each domain resolves to your server
  2. Request a TLS certificate from Let’s Encrypt (or ZeroSSL as fallback)
  3. Complete the ACME challenge automatically
  4. Start serving HTTPS with automatic HTTP→HTTPS redirects
  5. Renew certificates before they expire (no cron needed)

Connecting Docker Services

For Caddy to reach your other containers by name (like jellyfin or gitea), they need to be on the same Docker network. Add the caddy network to each service’s compose file:

# In your Jellyfin docker-compose.yml
services:
  jellyfin:
    image: jellyfin/jellyfin
    container_name: jellyfin
    # No need to expose ports to the host!
    networks:
      - caddy
    volumes:
      - ./config:/config
      - ./media:/media

networks:
  caddy:
    external: true

The critical change: remove the ports mapping. Your services no longer need to expose ports to the host. Caddy reaches them internally through the Docker network. This is more secure — the only ports open to the internet are 80 and 443 on Caddy.

Start Everything

# Start Caddy
cd ~/caddy && docker compose up -d

# Check logs to verify certificate issuance
docker logs caddy

You should see lines like:

{{""lleevveell""::""iinnffoo"",,""mmssgg""::""cceerrttiiffiiccaatteeoobbttaaiinneeddssuucccceessssffuullllyy"",,""nnaammeess""::[[""jgeiltleyaf.iyno.uyroduormdaoimna.icno.mc"o]m}"]}

Visit https://jellyfin.yourdomain.com — you’ll get a valid HTTPS connection with no browser warnings.

Common Patterns

Wildcard Certificates with DNS Challenge

If you have many subdomains or use internal-only services, a wildcard certificate avoids per-domain ACME challenges:

}.yout}@h}@h}h}rljagaadseninnoldtddm{dllrelrlranyeeaeeeeisfvvsni@eh@e{p.cnjrogroclessisnoohlettedmuol_e_dsypgap"{ftfrirNliot{ooajnxextreyayel{.fljygo{yeoiueflutnnilredvnyda"..fo:Cyim34Fona00_u:i04Ar8n0Pd0.Io9c_m6oTamOiKnE.Nc}om

For the DNS challenge, you need the Caddy image with a DNS provider plugin. Use the official builder:

services:
  caddy:
    image: caddy:2-alpine
    # Or build with DNS plugin:
    # build:
    #   context: .
    #   dockerfile: Dockerfile.caddy
    environment:
      - CF_API_TOKEN=your_cloudflare_api_token

Create Dockerfile.caddy:

FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare

FROM caddy:2-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Basic Authentication

Protect a service with a username and password:

a}dminb}r.aeysvoieucarradsdumeoti_mhnpari{$on2x.ayc$o1pm4o$r{YtOaUiRn_eBrC:R9Y0P0T0_HASH_HERE

Generate a bcrypt hash:

docker run --rm caddy:2-alpine caddy hash-password --plaintext 'your-secure-password'

Headers and Security

Add security headers to all sites:

(}j}seelcluh}yirrefmeiaipvtdnoeyeXXRS.rr_r--etytshCFfroee{oreius_anarcrepdtmrtdcreeee-ouorn-rTmrxstO-raiy)-pPaitTtonnyj{yils._epoipchlencooel-syrmayOtdfpSs-{eitAtSrniMres:oEic8nOcu0sRtr9I-i6nGotoIrysNing"iimfnaf-xw-haegne-=c3r1o5s3s6-0o0r0i;giinncludeSubDomains"

WebSocket Support

Caddy handles WebSockets automatically — no special config needed. Services like Nextcloud, Gitea, and code-server work out of the box.

Redirect www to Bare Domain

w}ww.yroeudridromhatitnp.sc:o/m/y{ourdomain.com{uri}permanent

Local Network with Self-Signed Certs

Running Caddy on a local network without a public domain? Use Caddy’s built-in CA:

h}ttpstr:le/sv/ejirensltele_yrpfnriaonlx.ylo1c9a2l.1{68.1.50:8096

Caddy generates a self-signed certificate from its internal CA. You can install Caddy’s root CA on your devices to trust these certificates:

# Export Caddy's root certificate
docker exec caddy caddy trust
# Or copy it manually
docker cp caddy:/data/caddy/pki/authorities/local/root.crt ./caddy-root.crt

Troubleshooting

Certificates not issuing:

  • Verify your domain’s DNS A record points to your server’s public IP: dig +short yourdomain.com
  • Ensure ports 80 and 443 are forwarded from your router to your server
  • Check Caddy logs: docker logs caddy --tail 50
  • ACME challenges require port 80 to be reachable from the internet

502 Bad Gateway:

  • The upstream container isn’t running or isn’t on the caddy network
  • Check: docker network inspect caddy — your service container should appear in the list
  • Verify the container name matches what’s in your Caddyfile

Permission denied on volumes:

  • The caddy_data volume stores certificates. Don’t delete it during upgrades.
  • If you moved hosts, restore the caddy_data volume or Caddy will re-request certificates (rate limits apply)

Let’s Encrypt rate limits:

  • 50 certificates per registered domain per week
  • Use the staging endpoint for testing: add acme_ca https://acme-staging-v02.api.letsencrypt.org/directory to your global options

Caddy not reloading config:

# Reload without downtime
docker exec caddy caddy reload --config /etc/caddy/Caddyfile

Caddy vs Nginx Proxy Manager vs Traefik

FeatureCaddyNginx Proxy ManagerTraefik v3
HTTPSAutomaticAutomatic (via UI)Automatic
Config styleCaddyfile (text)Web UIYAML + Docker labels
Learning curveLowLowestMedium-High
HTTP/3Yes (default)NoYes
Docker integrationNetwork-basedNetwork-basedLabel-based
Best forSimple to medium setupsBeginners who want a GUIComplex microservices

Conclusion

Caddy removes the worst parts of running a reverse proxy: certificate management, config syntax, and renewal automation. For most self-hosters, it’s the fastest path from “I have services on random ports” to “everything has HTTPS on clean subdomains.”

Start with the basic Caddyfile, add services as you deploy them, and enjoy never thinking about SSL certificates again.

Next steps: