Skip to content

ADR 0001 — Caddy over Traefik for CX23 TLS

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.

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.

Operational

  • The Caddy config lives in docker-compose.caddy.yml alongside the Supabase compose stack on CX23. Modifying TLS config means editing one Caddyfile volume-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.md is superseded by this decision and the running deployment. It should be treated as historical planning artefact, not as authoritative guidance.
  • platform-roadmap.md Section 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.

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.