ADR 0001 — Caddy over Traefik for CX23 TLS
Context
Section titled “Context”The CX23 server is a dedicated Hetzner VPS running the self-hosted Supabase stack (Auth, Postgres, Realtime, Storage, Studio). It hosts a single logical service — Supabase — plus the Tailscale daemon and basic hardening tooling.
When the server was first provisioned, cx23-setup-plan.md specified Traefik as the TLS
terminator, reasoning that Traefik was already the standard reverse proxy on CX43 (which runs
Dokploy, Authelia, and every other hosted service). Using the same tool across both hosts seemed
consistent.
However, by the time the authoritative server guide (docs/server/supabase/cx23-supabase-server.md)
and the production deployment were written, Caddy had replaced Traefik on CX23. A cross-document
review surfaced the discrepancy: cx23-setup-plan.md still described Traefik; the running system
used Caddy. platform-roadmap.md Section 1.3 also contained the stale line “TLS via Traefik”.
The mismatch needed a formal decision to avoid future confusion and prevent an accidental “fix” that re-introduces Traefik onto CX23.
Why Traefik is the right choice for CX43 and wrong for CX23
Section titled “Why Traefik is the right choice for CX43 and wrong for CX23”Traefik excels at dynamic multi-service routing: it discovers Docker containers via labels and reconfigures itself without restarts. CX43 hosts many independent services (Dokploy, Authelia, the knowledge base, future apps), so Traefik’s label-driven config is genuinely useful there.
CX23 hosts one service. Traefik’s dynamic config model adds no value for a single-service host and introduces unnecessary moving parts: label annotations on every Supabase container, a separate Traefik container, a network overlay for routing, and a dashboard that expands the attack surface.
Caddy’s Caddyfile for a single upstream is six lines. Auto-HTTPS via Let’s Encrypt (HTTP-01
challenge, port 80) works without any plugin or side-car. The simplicity is the right tradeoff
for a server whose only job is running Supabase.
Decision
Section titled “Decision”Caddy is the TLS terminator on CX23. Traefik is confined to CX43.
- CX23: Caddy handles all inbound HTTPS, terminates TLS, and reverse-proxies to the Supabase Kong gateway container on the internal Docker network.
- CX43: Traefik remains the single entry point for all hosted services via label-based routing.
- The two hosts are independent; no Traefik config change on CX43 affects CX23, and vice versa.
Consequences
Section titled “Consequences”Operational
- The Caddy config lives in
docker-compose.caddy.ymlalongside the Supabase compose stack on CX23. Modifying TLS config means editing oneCaddyfilevolume-mounted into the Caddy container. - Let’s Encrypt certificates are obtained and renewed automatically by Caddy via HTTP-01
challenge. Port 80 must remain open on CX23’s firewall. Certificates are stored in a named
Docker volume (
caddy_data) and survive container restarts. - Caddy logs are JSON-formatted and collected by the existing log pipeline.
Supabase Studio access
Studio (port 3000 in the Supabase stack) is not exposed through Caddy and is therefore
not reachable on the public internet. Studio is served exclusively via tailscale serve on the
Tailscale interface — HTTPS on the tailnet only. This is intentional: Studio has broad admin
access to Postgres and should never be public.
Documentation
cx23-setup-plan.mdis superseded by this decision and the running deployment. It should be treated as historical planning artefact, not as authoritative guidance.platform-roadmap.mdSection 1.3 must read “TLS via Caddy on CX23, Traefik on CX43” — the old “TLS via Traefik” line was stale.- Any future runbook referring to CX23 TLS must reference Caddy, not Traefik.
Maintenance
- Caddy upgrades: update the image tag in
docker-compose.caddy.yml, pull, and restart. No config migration required between minor versions. - If CX23 ever hosts a second service, revisit whether Caddy’s multi-site support is sufficient or whether Traefik should be introduced. At that point, create a superseding ADR.
Alternatives considered
Section titled “Alternatives considered”Traefik on CX23
Keep parity with CX43 by running Traefik on both hosts. Rejected because Traefik’s value proposition — dynamic multi-container routing via labels — is irrelevant for a single-service host. The added complexity (extra container, label annotations across the Supabase stack, dashboard exposure) costs more than the consistency benefit.
nginx
Mature and widely documented. Rejected primarily because it has no built-in ACME/Let’s Encrypt support. Certificate issuance and renewal require certbot or a similar side-car, which adds operational overhead (cron jobs, renewal hooks, permission management) that Caddy eliminates entirely.
Cloudflare proxy (orange-cloud)
Would provide DDoS protection and CDN caching at the edge. Rejected because it routes all inbound traffic — including Supabase Auth tokens, health data payloads, and Realtime WebSocket frames — through Cloudflare’s infrastructure. EU data residency and control over health data are hard requirements; proxying through a third party violates both.