Checkpoint: Cloudflare tunnel + public toggle, and static-runner frontends

Two features built this session, committed together as a checkpoint before the
deployment-model refactor (they share files, so kept as one rollback point):

- Public exposure via Cloudflare tunnel: a `public` toggle on services (requires
  proxy), gateway.public_domain/tunnel_id config, a cloudflared ingress generator
  (generators/tunnel.py) that maps <name>.<public_domain> to the gateway's
  internal host, deploy wiring, and docs/tunnel-setup.md.
- Frontends as `static`-runner services: a RunStatic runner served by Caddy
  (file_server), no systemd unit (like `remote`); the Caddyfile generator now
  derives static routes from services instead of a behavior==frontend branch.
This commit is contained in:
2026-07-01 06:44:23 -07:00
parent 2cafcbb372
commit 5f6afbf007
10 changed files with 465 additions and 40 deletions

View File

@@ -118,6 +118,12 @@ class GatewayConfig:
domain: str | None = None
acme_email: str | None = None
acme_dns_provider: str = "cloudflare"
# Public exposure via the Cloudflare tunnel (optional). `public_domain` is the
# separate zone public services are published at (e.g. "pub.payne.io" →
# <service>.pub.payne.io), kept distinct from `domain` so internal subdomain
# names never appear in public DNS. `tunnel_id` is the cloudflared tunnel UUID.
public_domain: str | None = None
tunnel_id: str | None = None
@dataclass
@@ -265,6 +271,8 @@ def load_config(root: Path | None = None) -> CastleConfig:
domain=gateway_data.get("domain"),
acme_email=gateway_data.get("acme_email"),
acme_dns_provider=gateway_data.get("acme_dns_provider", "cloudflare"),
public_domain=gateway_data.get("public_domain"),
tunnel_id=gateway_data.get("tunnel_id"),
)
# repo: field points to the git repo for repo-relative sources