Caddy Reverse Proxy

Installed April 19, 2026 on edge01 VPS (172.93.50.184). Replaces Pangolin/Traefik as the VPS’s reverse proxy and public web server.

Overview

Caddy is the single reverse proxy fronting public-facing services on the VPS. It handles:

  • Automatic HTTPS โ€” cert issuance and renewal via Let’s Encrypt. No certbot, no cron jobs, no manual work forever.
  • Static file serving โ€” hosts the Bee Hub at troglodyteconsulting.com.
  • Reverse proxy โ€” routes subdomains to LAN services via NetBird mesh (once NetBird is set up).
  • HTTP/3 support โ€” out of the box on port 443/udp.
  • Cloudflare DNS-01 challenge โ€” for wildcard certs on *.edmd.me (via the custom build with the Cloudflare DNS module).
Version v2.11.2
Custom build Yes (xcaddy + github.com/caddy-dns/cloudflare)
Binary /usr/bin/caddy
Config /etc/caddy/Caddyfile
Env file /etc/caddy/cloudflare.env (CF_API_TOKEN)
Data dir /var/lib/caddy/
Web root /var/www/bee-hub/
Service systemctl {status,reload,restart} caddy
Logs journalctl -u caddy
How automatic HTTPS works

When you add a site block to the Caddyfile:

n8n.troglodyteconsulting.com {
    reverse_proxy 192.168.8.100:5678
}

On systemctl reload caddy, Caddy:

  1. Notices the domain is public and has no cert yet
  2. Contacts Let’s Encrypt via ACME
  3. Proves ownership โ€” HTTP-01 challenge (default) or DNS-01 (for wildcards)
  4. Receives and installs the cert
  5. Starts serving HTTPS on 443
  6. Redirects HTTP โ†’ HTTPS automatically

All of that in seconds. Renewals (at 30 days remaining) happen silently in the background. You never touch certs again.

What Caddy handles forever:

  • Initial cert request
  • Automatic renewal
  • OCSP stapling
  • Fallback from Let’s Encrypt to ZeroSSL if LE is down
  • Modern TLS (1.3, correct ciphers, HSTS)
  • Certificate hot-reload without dropping connections
Caddyfile structure

Every site block is independent. The starter Caddyfile looks like:

{
    # Global options
    email doctor@edwarddelgrosso.com
}

# Main Bee Hub site โ€” static files
troglodyteconsulting.com, www.troglodyteconsulting.com {
    root * /var/www/bee-hub
    file_server
    encode gzip zstd
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }
}

# Subdomain reverse-proxy (requires NetBird mesh)
# n8n.troglodyteconsulting.com {
#     reverse_proxy 192.168.8.100:5678
# }

# Wildcard via Cloudflare DNS-01 (needs CF_API_TOKEN)
# *.edmd.me {
#     tls {
#         dns cloudflare {env.CF_API_TOKEN}
#     }
#     @lidarr host lidarr.edmd.me
#     handle @lidarr { reverse_proxy 192.168.8.100:8686 }
# }

# Catch-all 404 for unknown Host headers
:80, :443 {
    respond "Not configured" 404
}

Adding a new service is three lines:

newservice.troglodyteconsulting.com {
    reverse_proxy 192.168.8.100:PORT
}

Then: systemctl reload caddy. Cert issued within seconds, service live.

Cloudflare DNS-01 (wildcard certs)

For *.edmd.me wildcard certs, Caddy uses DNS-01 challenge via Cloudflare’s API.

  1. API Token lives in /etc/caddy/cloudflare.env as CF_API_TOKEN=... (mode 600, caddy:caddy ownership)
  2. Scope โ€” Edit zone DNS on both edmd.me and troglodyteconsulting.com zones
  3. Referenced in Caddyfile as {env.CF_API_TOKEN} inside the tls block:
*.edmd.me {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
    }
    # ... site blocks ...
}

Creating a new token โ€” dash.cloudflare.com/profile/api-tokens, pick “Edit zone DNS” template, select the target zones.

Rotation โ€” after replacing the token, systemctl reload caddy picks up the new env file.

Operations

Test config before reload (always):

caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile

Reload (graceful, no dropped connections):

systemctl reload caddy

Watch cert issuance in real time:

journalctl -u caddy -f | grep -iE 'cert|acme|tls'

List loaded modules (including cloudflare):

caddy list-modules | grep -iE 'cloudflare|dns'

Certificates stored in: /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/

Common troubleshooting:

Symptom Likely cause Fix
HTTP 404 on domain but DNS resolves Site block missing from Caddyfile Add block, reload
Cert issuance fails Port 80 blocked OR DNS hasn’t propagated Check UFW, dig the domain
{env.CF_API_TOKEN} is empty cloudflare.env not loaded by systemd Check systemd unit EnvironmentFile= directive
Reload silently fails Syntax error in Caddyfile caddy validate first
Slow first request after reload Cert being issued Check journal; second request is fast
Things to know
  • No certbot, ever. Caddy manages all ACME interactions internally. Any certbot tutorial you find online does not apply.
  • Port 80 must be open for HTTP-01 challenge. UFW allows it on edge01.
  • Cloudflare proxy (orange cloud) is OK โ€” DNS-01 doesn’t require the domain to resolve directly to the VPS. HTTP-01 requires it though, so prefer DNS-01 when Cloudflare proxy is on.
  • Caddyfile syntax is indentation-loose but block braces matter. Always caddy validate before reload.
  • Env vars in Caddyfile use {env.VAR_NAME} syntax โ€” note the dot separator, not underscore.
  • Reverse-proxying to LAN services (192.168.8.x) only works over NetBird. Without the mesh, the VPS can’t reach LAN IPs.
  • HTTP/3 works out of the box once port 443/udp is open in UFW. No extra config.
  • systemctl reload vs restart โ€” always prefer reload. Restart drops in-flight connections; reload does graceful handoff.
  • Deploy from Mac uses /Users/bee/Sync/ED/homelab/bee_hub/deploy-vps.sh โ€” now targeting root@172.93.50.184:/var/www/bee-hub. Cron runs every 30 min.
  • Cloudflare token rotation after any suspected exposure. Template: Edit zone DNS, both zones.
Why Caddy (vs nginx / Traefik)
Caddy nginx + certbot Traefik
Cert mgmt Automatic, zero config Manual (certbot + cron) Automatic
Config lines per site ~3 ~15-20 YAML, more verbose
Systemd unit 1 2 (nginx + certbot.timer) 1
HTTP/3 Out of the box Requires build flags Config flag
Docker-native No (but doesn’t need to be) No Yes, via labels
Best for Self-hosted reverse proxy, mixed static + reverse-proxy Heavy custom rewrite rules, fine-grained caching Docker Compose stacks with many services

Chose Caddy because edge01 is primarily a reverse proxy for a handful of LAN services plus the static Bee Hub site. Caddy’s zero-config HTTPS eliminates the certbot-renewal maintenance burden that plagued the previous Traefik/Pangolin setup.