Caddy Reverse Proxy - Caddyfile Patterns and Automatic HTTPS

Status: Active
Last Updated: 2026-08-26
Category: Networking - Reverse Proxies
Prerequisites: reverse-proxy-basics, cloudflare-dns
Time: 3 hours
Tags: caddy, caddyfile, https, dns-01, wildcard, cloudflare, reverse-proxy

Summary

Practical Caddyfile patterns for the fogserv edge: site blocks per service, automatic Let's Encrypt issuance, and wildcard certificates via the Cloudflare DNS-01 plugin so internal names work without public challenges. Caddy's selling point is that correct TLS is the default, not a configuration project.

🎯 What You'll Learn

By the end of this article, you'll be able to:


Table of Contents

  1. Context / Why This Matters
  2. Caddyfile Fundamentals
  3. Automatic HTTPS
  4. Wildcard via Cloudflare DNS-01
  5. Useful Directives Cheat Sheet
  6. Troubleshooting & Common Pitfalls
  7. Next Steps / Ops Actions
  8. Sources & Related
  9. Change Log

Context / Why This Matters

The fogserv fleet has two kinds of HTTPS names: public ones behind Cloudflare proxying, and LAN-only ones (nas.fogserv.cloud, proxmox.fogserv.cloud) resolved by split-horizon DNS (dhcp-dns-split-horizon). HTTP-01 can't cover the LAN-only names (no public route to :80), so we standardize on DNS-01 with a wildcard, issued and renewed entirely by Caddy.

Caddyfile Fundamentals

/etc/caddy/Caddyfile:

{
    email ops@fogserv.cloud
}

# Public service, backend on same host
grafana.fogserv.cloud {
    reverse_proxy 127.0.0.1:3000
}

# Public service, backend on another LAN host
git.fogserv.cloud {
    reverse_proxy 10.10.0.11:3000
}

# Static files
status.fogserv.cloud {
    root * /var/www/status
    file_server
}

That's a complete working config — site address implies automatic HTTPS. Validate and reload:

caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy     # zero-downtime config swap

Automatic HTTPS

By default Caddy:

  1. Obtains an ACME certificate (HTTP-01 + TLS-ALPN-01) for each site name.
  2. Renews at ~2/3 of lifetime.
  3. Redirects HTTP→HTTPS for you.

This works out of the box for publicly routable names. It fails for wildcard names (* requires DNS-01) and for LAN-only names if your firewall blocks inbound 80/443 to the ACME challenge — hence DNS-01 below.

Wildcard via Cloudflare DNS-01

Stock caddy binaries don't include the Cloudflare DNS provider; build with xcaddy or use the docker image lucaslorentz/caddy-docker-proxy style builds. Simplest reliable path: xcaddy build.

go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy build --with github.com/caddyserver/cloudflaredns
sudo mv caddy /usr/local/bin/caddy && sudo chmod +x /usr/local/bin/caddy

Token: the acme-dns01 scoped token from cloudflare-dns, in /etc/caddy/cf-token (0600, owned by caddy user). Then:

{
    email ops@fogserv.cloud
}

*.fogserv.cloud, fogserv.cloud {
    tls {
        dns cloudflare {env.CF_API_TOKEN}
        resolvers 1.1.1.1
    }

    # Route by subdomain inside the wildcard block:
    @grafana host grafana.fogserv.cloud
    handle @grafana {
        reverse_proxy 127.0.0.1:3000
    }
    @nas host nas.fogserv.cloud
    handle @nas {
        reverse_proxy https://10.10.0.31:5006 {
            transport http {
                tls_insecure_skip_verify   # NAS self-signed upstream
            }
        }
    }
    handle {
        respond "not found" 404
    }
}
# systemd unit must pass the token into env
ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
Environment=CLOUDFLARE_API_TOKEN_FILE=/etc/caddy/cf-token
Environment=CLOUDFLARE_DNS_API_TOKEN_FILE=/etc/caddy/cf-token

One cert now covers every current and future subdomain — adding a service means only editing routing, never cert config.

Useful Directives Cheat Sheet

reverse_proxy 127.0.0.1:3000              # basic proxy
reverse_proxy h2c://10.10.0.5:9000        # proxy to cleartext HTTP/2 backend

basicauth {
    user $2a$14$hashedpasswordhere       # caddy hash-password
}

header {
    Strict-Transport-Security "max-age=31536000"
    -Server                               # hide server header
}

encode gzip zstd                          # compression

handle_path /old/* {                      # strip prefix then proxy
    reverse_proxy 127.0.0.1:8080
}

log {
    output file /var/log/caddy/access.log
}

Practical Examples

Add a service end-to-end (wildcard already in place):

# 1. DNS (public): create grafana CNAME → edge, proxied  (cloudflare API/dashboard)
# 2. Routing: add one handle block above, reload
caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy
# 3. Verify
curl -s https://grafana.fogserv.cloud/api/health | jq .
openssl s_client -connect grafana.fogserv.cloud:443 -servername grafana.fogserv.cloud </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -dates      # shows *.fogserv.cloud cert

LAN-only name: no DNS record publicly needed at all — just split-horizon entry + matching host matcher. Cert still valid because wildcard covers it.

Troubleshooting & Common Pitfalls

Problem Cause Fix
tls: no certificate available for new name Name added but outside wildcard block Put all names under the *.fogserv.cloud block or add explicit site block
DNS-01 fails: unauthorized/rate limited Token lacks DNS Edit, or hit LE duplicate-cert limit Scope check (cloudflare-dns); staging endpoint while testing (acme_ca global option)
Works externally, cert warning on LAN LAN resolver returns different IP but name matches cert — actually fine; warning means old cert cache Trust via wildcard; clear client state
Port 443 bind fails after install Something else (nginx) owns it One edge proxy per host; see nginx-configuration migration note
Backend sees client IP as edge IP Missing trusted-proxies config in app Set app trusted proxy = edge IP (reverse-proxy-basics)
Reload drops connections Using restart instead of reload Use systemctl reload / caddy reload

Next Steps / Ops Actions

Sources & Related

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse