Skip to content

Setting Up a Reverse Proxy with Caddy

A reverse proxy sits in front of your services and routes traffic to the right one based on hostname or path. It also handles SSL certificates so you don't have to manage them per-service. Caddy is the one I reach for first because it gets HTTPS right automatically with zero configuration.

The Short Answer

# Install Caddy (Debian/Ubuntu)
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy

Or via Docker (simpler for a homelab):

services:
  caddy:
    image: caddy:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    restart: unless-stopped

volumes:
  caddy_data:
  caddy_config:

The Caddyfile

Caddy's config format is refreshingly simple compared to nginx. A Caddyfile that proxies two services:

# Caddyfile

nextcloud.yourdomain.com {
    reverse_proxy localhost:8080
}

jellyfin.yourdomain.com {
    reverse_proxy localhost:8096
}

That's it. Caddy automatically gets TLS certificates from Let's Encrypt for both hostnames, handles HTTP-to-HTTPS redirects, and renews certificates before they expire. No certbot, no cron jobs.

For Local/Home Network Use (Without a Public Domain)

If you don't have a public domain or want to use Caddy purely on your LAN:

Option 1: Local hostnames with a self-signed certificate

:443 {
    tls internal
    reverse_proxy localhost:8080
}

Caddy generates a local CA and signs the cert. You'll get browser warnings unless you trust the Caddy root CA on your devices. Run caddy trust to install it locally.

Option 2: Use a real domain with DNS challenge (home IP, no port forwarding)

If you own a domain but don't want to expose your home IP:

nextcloud.yourdomain.com {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    reverse_proxy localhost:8080
}

This uses a DNS challenge instead of HTTP — Caddy creates a TXT record to prove domain ownership, so no ports need to be open. Requires the Caddy DNS plugin for your registrar.

Reloading After Changes

# Validate config before reloading
caddy validate --config /etc/caddy/Caddyfile

# Reload without restart
caddy reload --config /etc/caddy/Caddyfile

# Or with systemd
sudo systemctl reload caddy

Adding Headers and Custom Options

yourdomain.com {
    reverse_proxy localhost:3000

    # Security headers
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
    }

    # Increase timeout for slow upstream
    reverse_proxy localhost:3000 {
        transport http {
            response_header_timeout 120s
        }
    }
}

Gotchas & Notes

  • Ports 80 and 443 must be open. For automatic HTTPS, Caddy needs to reach Let's Encrypt. If you're behind a firewall, open those ports. For local-only setups, use tls internal instead.
  • The caddy_data volume is important. Caddy stores its certificates there. Lose the volume, lose the certs (they'll be re-issued but it causes brief downtime).
  • Caddy hot-reloads config cleanly. Unlike nginx, caddy reload doesn't drop connections. Safe to use on production.
  • Docker networking: When proxying to other Docker containers, use the container name or Docker network IP instead of localhost. If everything is in the same compose stack, use the service name: reverse_proxy app:8080.
  • nginx comparison: nginx is more widely documented and more feature-complete for edge cases, but Caddy's automatic HTTPS and simpler config makes it faster to set up for personal use. Both are good choices.

See Also

  • [[self-hosting-starter-guide]]
  • [[linux-server-hardening-checklist]]
  • [[debugging-broken-docker-containers]]