Adds an OpenSpec change scoping the relocation of BRouter from the co-located flagship (cx23, 4 GB RAM, 40 GB SSD, Europe segments only) to a dedicated Hetzner Robot host in the same datacenter, with private connectivity over Hetzner vSwitch #80672 (VLAN 4000). This first PR only lays the network prerequisite: - Terraform: a Hetzner Cloud Network (10.0.0.0/16) with a cloud subnet (10.0.0.0/24) hosting the flagship at 10.0.0.2, and a vSwitch subnet (10.0.1.0/24) bridged to Robot VLAN 4000. The dedicated host's VLAN sub-interface (10.0.1.10 on enp4s0.4000) is configured out-of-band via netplan and is not Terraform-managed. - lifecycle { ignore_changes = [user_data] } on the flagship server to prevent the Hetzner provider's post-1.45 user_data hash drift from triggering a spurious full-server replacement on unrelated applies. - OpenSpec change with proposal, design, specs (delta for brouter-integration / infrastructure / observability / security-hardening), and tasks; Section 1 (pre-flight) is checked off with operator notes. Verification: ping both directions across the vSwitch is 0% loss, sub-ms latency; dedicated host's VLAN config persists across reboot (verified ~60 s to restore private reachability). Follow-up PRs will land the BRouter host compose project, Planner shared-secret header, CD workflow retarget, observability wiring, and the cutover. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.9 KiB
Why
BRouter currently runs co-located with the Journal, Planner, PostgreSQL, and the observability stack on a single Hetzner Cloud box (cx23: 2 vCPU / 4 GB RAM / 40 GB SSD). The 40 GB disk and 4 GB RAM budget cap us at a Europe-only segment set; global routing needs ~60–80 GB of RD5 files and a JVM heap that doesn't compete with Postgres and Loki for memory. A larger self-hosted server in the same Hetzner datacenter has 3 TB of RAID storage and 32 GB of RAM available with headroom to spare, and co-locating stays on the private Hetzner network for low latency. Moving BRouter there unlocks global routing without contending for resources on the primary host.
What Changes
- Run BRouter on a second Hetzner host in the same datacenter, owned by a non-root
trailsuser that is a member of thedockergroup (no sudo). - Expose BRouter only on a private network interface (Hetzner vSwitch) shared with the primary host; no public ingress. The existing host firewall rejects everything else.
- Front the BRouter container with a Caddy sidecar that enforces a shared-secret header (
X-BRouter-Auth: <token>). The Planner backend adds the header when proxying/api/route. Requests without the header are rejected with 403. - Extend BRouter coverage from the current Europe RD5 set to the full planet (~60–80 GB on disk). The dedicated host's disk accommodates this comfortably.
- Size the BRouter JVM for planet-scale traffic:
-Xmx8gheap on a host with 32 GB total RAM, leaving generous page cache for segment files. - BREAKING for operators:
cd-brouter.ymltargets a new host/SSH identity (trails@<brouter-host>) instead ofroot@<main-host>. The segment-download logic moves with the workflow, and the segment tile list expands to global. - Update the Planner's
BROUTER_URLto the new host's private vSwitch address and addBROUTER_AUTH_TOKENas a new SOPS-managed secret. The Planner sends the token on every request. - Scrape BRouter-specific metrics (cAdvisor filtered to the BRouter container, JVM exporter if available) and tail BRouter container logs from the primary host's Prometheus/Loki over the vSwitch. Do not scrape the shared host's
node_exporter— its other self-hosted workloads are out of scope for trails.cool observability. - Remove the BRouter service from the primary host's
infrastructure/docker-compose.ymlonce cutover is complete; stop shipping BRouter images to the primary viacd-infra.yml. - Document the new host in
CLAUDE.mdanddocs/architecture.mdas a second deployment target.
Capabilities
New Capabilities
None. This change relocates an existing capability rather than introducing a new user-facing one. All new requirements extend existing specs.
Modified Capabilities
brouter-integration: deployment target changes from co-located Docker Compose service to a remote host reached over a private network; adds a shared-secret auth requirement on the proxy hop; extends segment coverage from Europe to global.infrastructure: introduces a second Hetzner host on a vSwitch, a non-roottrailsdeploy user, a global segment management workflow, and split CD targets for app vs. BRouter; updates the "BRouter segment management" and "CD pipeline" requirements accordingly.observability: adds scraping/log shipping for the remote BRouter host over the private network, scoped to BRouter containers only.security-hardening: adds the shared-secret auth requirement for BRouter and the private-network-only exposure rule.
Impact
- Code:
apps/planner/app/lib/brouter.ts(addX-BRouter-Authheader, readBROUTER_AUTH_TOKEN); planner server env wiring. - Infrastructure:
infrastructure/docker-compose.yml(remove brouter service + segments volume); newinfrastructure/brouter-host/directory with the remote compose file, Caddy sidecar config, and segment-download script. - CI/CD:
.github/workflows/cd-brouter.yml(retarget host, new secretsBROUTER_DEPLOY_HOST/BROUTER_DEPLOY_SSH_KEY/BROUTER_AUTH_TOKEN);cd-infra.ymlno longer touches BRouter. - Secrets: new entries in
infrastructure/secrets.infra.env(SOPS) forBROUTER_AUTH_TOKEN; new GitHub Actions secrets for the new host's SSH key and hostname. - Observability: update
infrastructure/prometheus/prometheus.ymlwith a new scrape target over the vSwitch; updateinfrastructure/promtail/to pull logs from the remote host (via Docker socket proxy or a shipped-from agent). - Documentation:
CLAUDE.md(note the second host);docs/architecture.md(topology diagram and failure modes);docs/deployment.mdif present. - Dependencies: no npm/package changes. Requires Hetzner vSwitch configured between the two hosts (operator action).
- Operational risk: BRouter becomes a second SPOF reachable only over vSwitch. Monitored via existing
brouter_request_duration_secondshistogram in the Planner; cutover includes a 48 h rollback window with the old container kept warm on the primary host.