Files
kongkxandClaude Opus 5.5 4abd3ff67f fix(gig-platform): route the object store console to objectstore-console-gate
MinIO was replaced by RustFS; the console label is now objects. and the
public tunnel hostname gig-objectstore.kucial.com.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:09:06 +08:00

6.3 KiB

Caddy Dual TLS + Cloudflare Tunnel Setup

This architecture provides End-to-End Dual TLS Encryption:

  1. Edge TLS (Cloudflare Edge Certificate): Cloudflare terminates HTTPS for public visitors on the internet.
  2. Origin TLS (Let's Encrypt / ACME Certificate): Caddy automatically requests and renews valid SSL/TLS certificates using the Cloudflare DNS-01 ACME challenge plugin.
  3. Tunnel Transport: Cloudflare Tunnel (cloudflared) connects to Caddy over HTTPS://caddy:443 with strict origin TLS verification.

That describes the infra tunnel, for this host's own services. Project preview instances use a second, preview tunnel that bypasses Caddy:

public, infra:    Client → Cloudflare → cloudflared         → Caddy :443 → service
public, preview:  Client → Cloudflare → cloudflared-preview → (edge) instance container
LAN:              Client → split DNS  → Caddy :443 → (edge) instance container

📁 File Structure

  • Dockerfile: Builds Caddy with github.com/caddy-dns/cloudflare using xcaddy.
  • docker-compose.yml: Runs caddy, the infra tunnel cloudflared, and the preview tunnel cloudflared-preview. Caddy and cloudflared-preview join the external edge network shared with project instances; cloudflared-preview joins nothing else.
  • Caddyfile: Global options (ACME email, client-IP trust), the cloudflare_tls snippet, standalone sites, and import sites/*.caddy.
  • sites/: One file per project, mounted read-only at /etc/caddy/sites.
    • gig-platform.caddy: gig-platform instances — main at gig-api.kucial.com (public name) and gig-platform.dev.kucial.com (LAN alias), e2e / pr-<N> by hostname label, and main's permission gates on studio. / objects. / horizon..
    • gig-app.caddy: gig-app web instances — main at gig-web.kucial.com and gig-app.dev.kucial.com, pr-<N> by label.
  • .env: Stores CLOUDFLARE_API_TOKEN, CLOUDFLARE_TUNNEL_TOKEN and CLOUDFLARE_PREVIEW_TUNNEL_TOKEN.
  • data/: Persistent volume storing Caddy's certificates.

sites/gig-platform.caddy and sites/gig-app.caddy are copies of gig-platform's docs/deploy/caddy/; change them there and copy them over, so the two stay identical.


🧩 Project instances (edge network)

Projects deployed as instances (gig-platform, gig-app) publish no host ports. Each instance's entry container joins the external edge network under an instance-qualified alias — gig-platform-<instance>-nginx, gig-app-<instance>-web. Two things reach those aliases: Caddy, for LAN clients, through a site file:

*.gig-platform.dev.kucial.com {
    reverse_proxy gig-platform-{http.request.host.labels.4}-nginx:80
}
  • The apex needs its own site: labels count from the right, so labels.4 is empty there.
  • A hostname whose instance does not exist returns 502.
  • Placeholder upstreams share health state across instances, so do not add health checks to those sites.

…and cloudflared-preview, for public clients, through the preview tunnel's public hostnames (below). It only ever reaches the named hostnames listed there, so an instance is public exactly when one of its hostnames is on that list.

Create the network once, before the first docker compose up:

docker network create edge

Split DNS on the router must resolve both the apex and the wildcard of each project domain to this host, e.g. dnsmasq address=/gig-platform.dev.kucial.com/<mac-mini-ip>.


🚀 Setup & Cloudflare Dashboard Configuration

1. In .env

  • CLOUDFLARE_API_TOKEN: Token with Zone:DNS:Edit permission (allows Caddy to complete the DNS-01 challenge).
  • CLOUDFLARE_TUNNEL_TOKEN: Token of the infra tunnel.
  • CLOUDFLARE_PREVIEW_TUNNEL_TOKEN: Token of the preview tunnel. Create it as a separate tunnel in Zero Trust.

2. In Cloudflare Zero Trust Dashboard (Public Hostnames)

Infra tunnel — the host's own services, through Caddy:

Public Hostname Service Type URL Origin Server Name (under Additional Settings -> TLS)
synotrip.kucial.com HTTPS caddy:443 synotrip.kucial.com
blog.kucial.com HTTPS caddy:443 blog.kucial.com

Note

Setting Origin Server Name tells cloudflared which SNI hostname to send to Caddy during the TLS handshake, allowing Caddy to serve the matching Let's Encrypt certificate.

Preview tunnel — project instances, straight to their edge aliases. Plain HTTP: the hop is inside the Docker network, and Cloudflare terminates TLS.

Public Hostname Service Type URL
gig-api.kucial.com HTTP gig-platform-main-nginx:80
gig-studio.kucial.com HTTP gig-platform-main-db-studio-gate:80
gig-objectstore.kucial.com HTTP gig-platform-main-objectstore-console-gate:80
gig-horizon.kucial.com HTTP gig-platform-main-horizon-gate:80
gig-web.kucial.com HTTP gig-app-main-web:80

One label each, and not by preference: the free plan's Universal SSL covers kucial.com and *.kucial.com only, so a published something.dev.kucial.com fails TLS at the edge however the tunnel is configured — that level needs Advanced Certificate Manager. The dev.kucial.com names stay LAN-only, where Caddy issues its own DNS-01 certificates and depth does not matter.

  • Route the internal surfaces only to their gates, never to db-studio, objectstore:9001 or anything else. The gates enforce the platform permission on every request.
  • Never publish *.gig-platform.dev.kucial.com or *.gig-app.dev.kucial.com: PR and e2e instances are LAN-only, reached through split DNS.
  • Enable WebSockets for the zone; gig-platform serves them at /app/ on the instance hostname.
  • A public hostname must not exist on both tunnels, or traffic goes to whichever connector wins.

3. Start Services

docker compose up -d --build

4. Zero-Downtime Reload

docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

5. Reloading

A new or edited sites/*.caddy file only needs a reload. Changing the sites mount or the networks in docker-compose.yml needs docker compose up -d caddy. The preview tunnel's hostnames change in the dashboard only; cloudflared-preview picks them up without a restart.