Caddy

Fast, multi-platform web server with automatic HTTPS

Reverse Proxy & Gateways ★ 76k stars Easy setup Apache-2.0

Caddy is a powerful, extensible web server and reverse proxy written in Go that provides automatic HTTPS out of the box. Its simple configuration makes it popular for self-hosting.

Caddy setup guides & articles

Hands-on coverage of Caddy from the blog.

Key features

  • Automatic HTTPS by default
  • Simple Caddyfile config
  • Single binary
  • Plugin ecosystem

Quick deploy

A starting point for self-hosting Caddy - check the official docs for the full set of options.

  • Persist /etc/caddy/Caddyfile /data
Docker Compose
services:
  caddy:
    image: caddy:latest
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./data:/data
      - ./config:/config
    restart: unless-stopped

Watch out for

  • Persist /data or every container recreate re-issues certificates and burns rate limits
  • The 443/udp mapping is what enables HTTP/3 - keep it

Pros & cons

Strengths

  • Zero-config TLS
  • Easy to learn

Trade-offs

  • Fewer tuning knobs than Nginx

Caddy replaces

Last reviewed Aug 23, 2026 · 641 words

Caddy's pitch is that a reverse proxy with automatic HTTPS should be a five-line text file, and it delivers: point DNS at your box, write a Caddyfile block, and certificates appear, renew, and redirect HTTP to HTTPS without a single line of TLS configuration. This guide is the cookbook version — the handful of patterns that cover a homelab's entire proxy needs, and the two operational rules that keep it boring.

The whole job, in one file

jellyfin.example.com {
    reverse_proxy jellyfin:8096
}

vault.example.com {
    reverse_proxy vaultwarden:80
}

paperless.example.com {
    reverse_proxy paperless:8000
}

That is a complete, production-grade config for three services: TLS issued and renewed per domain, HTTP→HTTPS redirects, HTTP/2 and websockets proxied automatically (no special-casing for Uptime Kuma-style socket-heavy apps). Run it from the official container with ports 80, 443, and 443/udp published — the UDP mapping is HTTP/3, keep it — and your services on the same Docker network so the container names resolve.

Rule one: persist /data. Rule two: see rule one

Caddy stores its certificates and ACME account in /data. Mount it (./data:/data) or every container recreation throws the certificates away and re-issues them — which works right up until Let's Encrypt's rate limit (5 duplicate certs per week) locks you out of your own domains for days. This is the single most common way people hurt themselves with Caddy, it happens during exactly the compose-file fiddling phase when you recreate containers constantly, and the fix is one volume line. Reloads, by contrast, are free: docker exec caddy caddy reload --config /etc/caddy/Caddyfile applies changes with zero downtime, and caddy validate checks the file before you bet on it.

Wildcard certs and internal-only services

Publishing *.home.example.com services that should never face the internet? Two useful patterns. First, a wildcard certificate via the DNS-01 challenge means one cert covers every subdomain and nothing appears in certificate-transparency logs announcing your service names — the trade-off is that DNS-01 needs a plugin for your DNS provider, which means a small custom Docker build (the one place Caddy costs more friction than Traefik; the TLS guide walks through it). Second, guard internal services by source address:

photos.home.example.com {
    @internal remote_ip 192.168.0.0/16 100.64.0.0/10
    handle @internal {
        reverse_proxy immich:2283
    }
    respond 403
}

The second range is Tailscale's address space, so the rule reads "LAN and VPN only." For upload-heavy apps like Immich, add request_body { max_size 5GB } — Caddy's default is generous but not unlimited, and phone videos will find the ceiling.

Snippets stop the copy-paste

Once five services share the same headers or auth, define the block once and import it:

(secure) {
    header {
        Strict-Transport-Security "max-age=31536000"
        X-Content-Type-Options "nosniff"
    }
}

notes.example.com {
    import secure
    reverse_proxy trilium:8080
}

Between reverse_proxy, handle/matchers, import, and respond, you have read 95% of the Caddyfile vocabulary a homelab ever uses. The remaining 5% — file_server for static sites, basic_auth for quick gates, php_fastcgi — turns Caddy into the web server it also is, which is why a blog plus a dozen proxied apps can be one 40 MB process idling at 30 MB of RAM.

What I'd do

One Caddy container at the network edge, the Caddyfile in git, /data persisted, a snippet for shared headers, wildcard-plus-remote_ip for everything internal, and reload-not-restart as the habit. Check the config against caddy validate before every reload and Caddy becomes the component you forget exists — the correct emotional state for the thing holding all your TLS. How it stacks against the alternatives is in the reverse proxy showdown.

Compare Caddy

11 head-to-head comparisons.

Similar reverse proxy & gateways apps