trails/infrastructure/brouter-host/README.md
Ullrich Schäfer c49047fd33
BRouter host compose + Planner auth + cd-brouter rewrite
Lands sections 3-5 of the relocate-brouter-to-dedicated-host change:
everything needed to run BRouter on the dedicated Hetzner Robot host
and have the Planner talk to it with the shared-secret header. Does
NOT flip the cutover — the flagship BRouter stays warm during soak.

## BRouter host compose (section 3)

New `infrastructure/brouter-host/` — a standalone compose project that
runs as the `trails` user on `ullrich.is`:

- `docker-compose.yml` — brouter + caddy sidecar. BRouter has no host
  port; caddy binds only to `10.0.1.10:17777` (vSwitch IP). Every
  service explicitly overrides the host's default Loki logging driver
  to `json-file` so logs don't leak to the operator's personal Loki.
- `Caddyfile` — single-purpose reverse proxy that requires
  `X-BRouter-Auth: ${BROUTER_AUTH_TOKEN}` on every request. `auto_https
  off` (vSwitch-only); default access log format omits request
  headers, so the token is never written to disk.
- `download-segments.sh` — crawls brouter.de, pulls planet-wide RD5
  tiles via `wget -N` (incremental). Idempotent, safe to cron.
- `README.md` — one-shot provisioning + token rotation + rollback
  notes.

`docker/brouter/Dockerfile` is patched to honor `JAVA_OPTS` (was
hardcoded `-Xmx1024M` in CMD). Default keeps the flagship's current
heap; compose on the dedicated host overrides to `-Xmx8g` for planet
scale on a 32 GB box.

## Planner shared-secret header (section 4)

`apps/planner/app/lib/brouter.ts`:

- Module-level guard: throws at startup in production if
  `BROUTER_AUTH_TOKEN` is unset.
- `authHeaders()` helper (reads env at call time, so tests can
  `vi.stubEnv` without module reset).
- Header attached on both `computeRoute` (per-segment) and
  `computeSegmentGpx`.

3 new unit tests cover header attachment + the no-token path.

`infrastructure/docker-compose.yml` passes `BROUTER_AUTH_TOKEN` to
the Planner service, and makes `BROUTER_URL` overridable via SOPS so
the cutover is a one-variable flip.

## cd-brouter workflow (section 5)

Rewritten to deploy to the dedicated host:

- SSH as `trails@${BROUTER_DEPLOY_HOST}` on port
  `${BROUTER_DEPLOY_SSH_PORT}` (2232) using
  `${BROUTER_DEPLOY_SSH_KEY}`.
- Decrypts SOPS, extracts ONLY `BROUTER_AUTH_TOKEN` into a `.env`
  file, scp'd alongside the compose project.
- `paths:` trigger now includes `infrastructure/brouter-host/**`.
- Segment download is NOT run here — first-time seed is a manual
  operator step (multi-hour). Routine re-runs are cron-able on the
  dedicated host.
- Grafana annotation step preserved (reaches flagship Grafana as
  before).

## What's NOT here

- `brouter:` service on the flagship is intentionally left in place
  (removed in section 7.5 after the 48 h soak window post-cutover).
- Observability (section 6) — Prometheus scrape + Loki shipping from
  the dedicated host — comes in a follow-up PR.
- Cutover itself (section 7) — flip `BROUTER_URL`, verify, remove the
  flagship brouter — is an operator action gated on first-time
  provisioning + smoke testing.

## Verification

`pnpm typecheck && pnpm lint && pnpm test` all clean; planner build
passes (the CI regression from #286 was fixed in #290).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:52:48 +02:00

3.1 KiB
Raw Blame History

BRouter host compose project

Runs on a dedicated Hetzner Robot server (currently ullrich.is, private IP 10.0.1.10 over vSwitch #80672), owned by the non-root trails user. Services:

  • brouter — the BRouter Java server, planet-scale segments, 8 GB JVM heap, no public port.
  • caddy — thin sidecar enforcing the X-BRouter-Auth shared-secret header. Bound to 10.0.1.10:17777 (vSwitch IP only).

Public ingress is blocked at the host's UFW (port 17777 is only allowed on the VLAN interface from 10.0.0.2, the flagship's vSwitch IP).

One-time provisioning

Runs as the trails user on the dedicated host.

# 1. Land the compose project
cd ~
git clone https://github.com/trails-cool/trails.git repo
mkdir -p brouter
cp -r repo/infrastructure/brouter-host/* brouter/
cd brouter

# 2. Provide the shared secret (matches BROUTER_AUTH_TOKEN in SOPS)
#    The CD workflow normally writes this file; for manual bring-up,
#    do it yourself.
cat > .env <<'EOF'
BROUTER_AUTH_TOKEN=<paste value from sops -d infrastructure/secrets.app.env | grep BROUTER_AUTH_TOKEN>
EOF
chmod 0600 .env

# 3. Seed segments (multi-hour, ~6080 GB)
./download-segments.sh

# 4. Start services
docker compose pull
docker compose up -d

# 5. Smoke test from the flagship (over vSwitch)
#    Should return 200 with the token, 403 without.
# ssh root@trails.cool 'curl -sSf -H "X-BRouter-Auth: <TOKEN>" http://10.0.1.10:17777/brouter?lonlats=... '

Subsequent deploys

The cd-brouter GitHub Actions workflow handles routine updates: it pulls the latest image, rewrites the compose file + Caddyfile from the repo, and restarts.

Segment updates

Segments are refreshed by brouter.de weekly. To pull updates:

./download-segments.sh
docker compose restart brouter

Schedule via cron if you want automatic updates (not wired in this repo yet).

Token rotation

  1. Regenerate: openssl rand -base64 32.
  2. Update SOPS: sops infrastructure/secrets.app.env (writer uses the sops -d | append | sops -e pattern via the CD workflow; editing directly works too).
  3. Merge the SOPS change to main.
  4. cd-apps redeploys the Planner (sends the new token outbound).
  5. cd-brouter redeploys Caddy (matches on the new token).
  6. Brief overlap window where Planner sends new token but Caddy still accepts old: both deploys should fire within a minute of each other, so a few 403s are the worst case.

Rollback

If BRouter is misbehaving and the flagship BRouter is still warm (during the 48 h soak window post-cutover), flip BROUTER_URL in infrastructure/secrets.app.env back to http://brouter:17777 and redeploy the Planner. After the soak window, see the change's design.md for the longer rollback path.

Logging

The dedicated host's Docker daemon default logging driver is loki (the operator's personal Loki). Our compose file explicitly overrides each service to json-file so logs stay local; a promtail sidecar (section 6.3 of the relocate change) tails them and ships to trails.cool's Loki over the vSwitch. If you disable that sidecar, the BRouter logs will NOT flow to trails.cool's Grafana — they'll just accumulate locally and eventually rotate.