From 1e3626b2094ed5c4fea928b76632c556a443ad45 Mon Sep 17 00:00:00 2001 From: Sladey Date: Sat, 1 Aug 2026 10:34:00 +0100 Subject: [PATCH] fix(deploy): wire mobile pairing relay into docker-compose self-host bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-hosted relays with BUZZ_REQUIRE_RELAY_MEMBERSHIP=true (the production default in deploy/compose/.env.example) advertise NIP-43 in their NIP-11 document regardless of whether a pairing relay is actually deployed (crates/buzz-relay/src/nip11.rs advertise_nip43). The desktop/mobile pairing client (desktop/src-tauri/src/commands/pairing.rs probe_pairing_relay / pairing_relay_from_nip11) then falls back to the legacy "/pair" convention, but the compose bundle never ran buzz-pair-relay or routed /pair anywhere, so pairing failed with "WebSocket connection failed: HTTP error: 404 Not Found". The published image already bundles the buzz-pair-relay binary alongside buzz-relay/buzz-admin (Dockerfile), and the relay already reads BUZZ_PAIRING_RELAY_URL to advertise pairing_relay_url directly in NIP-11 (crates/buzz-relay/src/config.rs, nip11.rs) — so this is a deploy-config-only fix, no Rust changes needed. Mirrors the existing Helm chart's pattern (deploy/charts/buzz/templates/pairing-relay.yaml). - deploy/compose/compose.yml: add a pairing-relay service (same image, entrypoint override to buzz-pair-relay) and pass through BUZZ_PAIRING_RELAY_URL on the relay service. - deploy/compose/compose.caddy.yml: caddy now also depends on pairing-relay being healthy. - deploy/compose/Caddyfile: route /pair (and /pair/*) to pairing-relay:5000 ahead of the catch-all reverse_proxy to relay:3000. - deploy/compose/.env.example: document and default BUZZ_PAIRING_RELAY_URL=wss://buzz.example.com/pair. - deploy/compose/README.md: new "Mobile pairing" section explaining the 404, what's wired automatically, and the options for custom reverse proxies or open relays that don't want pairing. Co-authored-by: Sladey Signed-off-by: Sladey --- deploy/compose/.env.example | 8 ++++++++ deploy/compose/Caddyfile | 10 +++++++++- deploy/compose/README.md | 29 +++++++++++++++++++++++++++ deploy/compose/compose.caddy.yml | 2 ++ deploy/compose/compose.yml | 34 ++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/deploy/compose/.env.example b/deploy/compose/.env.example index cebe879da0..b2d6c85dbf 100644 --- a/deploy/compose/.env.example +++ b/deploy/compose/.env.example @@ -16,6 +16,14 @@ BUZZ_CORS_ORIGINS=https://buzz.example.com BUZZ_REQUIRE_AUTH_TOKEN=true BUZZ_REQUIRE_RELAY_MEMBERSHIP=true BUZZ_ALLOW_NIP_OA_AUTH=true +# BUZZ_REQUIRE_RELAY_MEMBERSHIP=true makes the relay advertise NIP-43 (device +# pairing) in its NIP-11 doc, which the mobile/desktop app requires for +# pairing. The compose bundle runs a pairing-relay sidecar for this — see +# compose.yml and (with BUZZ_COMPOSE_TLS=true) the Caddyfile's /pair route. +# This URL is what gets advertised; leave it pointed at your domain's /pair +# path so pairing "just works". Set to empty only if you provide your own +# /pair routing elsewhere, or if you set BUZZ_REQUIRE_RELAY_MEMBERSHIP=false. +BUZZ_PAIRING_RELAY_URL=wss://buzz.example.com/pair BUZZ_AUTO_MIGRATE=true BUZZ_GIT_CONFORMANCE_PROBE=true RUST_LOG=buzz_relay=info,buzz_db=info,buzz_auth=info,buzz_pubsub=info,tower_http=info diff --git a/deploy/compose/Caddyfile b/deploy/compose/Caddyfile index 205cf4c5bc..ccfd8652d7 100644 --- a/deploy/compose/Caddyfile +++ b/deploy/compose/Caddyfile @@ -1,5 +1,13 @@ {$BUZZ_DOMAIN} { encode zstd gzip - reverse_proxy relay:3000 + # route {} disables Caddy's automatic directive sorting for this block, so + # the /pair match below is always tried before the catch-all reverse_proxy. + # Caddy proxies WebSocket upgrades transparently — no extra directive needed. + route { + @pairing path /pair /pair/* + reverse_proxy @pairing pairing-relay:5000 + + reverse_proxy relay:3000 + } } diff --git a/deploy/compose/README.md b/deploy/compose/README.md index 0de524fb5b..cc2c69ca6c 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -41,6 +41,35 @@ keypair. Run `./run.sh backup-hint` for the backup checklist. +## Mobile pairing + +`BUZZ_REQUIRE_RELAY_MEMBERSHIP=true` (the production default in `.env.example`) +makes the relay advertise NIP-43 device pairing in its NIP-11 document, +regardless of whether a pairing relay is actually reachable. Without the +wiring below, the desktop/mobile app's pairing probe finds NIP-43 advertised, +falls back to the legacy `/pair` convention, and gets +`HTTP error: 404 Not Found` — there is nothing listening on `/pair`. + +To make mobile pairing work out of the box, this bundle includes: + +- A `pairing-relay` service in `compose.yml` — the same image as `relay`, + running the bundled `buzz-pair-relay` binary instead. It's stateless and + needs no database/Redis/secrets. +- A `BUZZ_PAIRING_RELAY_URL` env var (see `.env.example`) that the main relay + advertises directly in NIP-11, so the client connects to the pairing relay + without needing the legacy fallback at all. +- With `BUZZ_COMPOSE_TLS=true` (`compose.caddy.yml`), a `/pair` route in the + `Caddyfile` that proxies to `pairing-relay:5000`, including the WebSocket + upgrade. + +If you're **not** using `compose.caddy.yml` (bringing your own reverse +proxy), you must add an equivalent `/pair` → `pairing-relay:5000` route +yourself — see `crates/buzz-pair-relay`'s module docs for the requirements +(route only `/pair`, terminate TLS, enforce read timeouts). Without that +route, either wire it up or set `BUZZ_REQUIRE_RELAY_MEMBERSHIP=false` and +clear `BUZZ_PAIRING_RELAY_URL` to disable device pairing entirely on an open +relay. + ## Validation Before sharing an install link publicly, verify a fresh install with: diff --git a/deploy/compose/compose.caddy.yml b/deploy/compose/compose.caddy.yml index c7dcbf106c..cf7c4f372a 100644 --- a/deploy/compose/compose.caddy.yml +++ b/deploy/compose/compose.caddy.yml @@ -7,6 +7,8 @@ services: depends_on: relay: condition: service_healthy + pairing-relay: + condition: service_healthy environment: BUZZ_DOMAIN: ${BUZZ_DOMAIN:?set BUZZ_DOMAIN} ports: diff --git a/deploy/compose/compose.yml b/deploy/compose/compose.yml index bc3c27501e..73a97d1dfe 100644 --- a/deploy/compose/compose.yml +++ b/deploy/compose/compose.yml @@ -18,6 +18,14 @@ services: BUZZ_GIT_REPO_PATH: /data/git BUZZ_AUTO_MIGRATE: ${BUZZ_AUTO_MIGRATE:-false} BUZZ_GIT_CONFORMANCE_PROBE: ${BUZZ_GIT_CONFORMANCE_PROBE:-true} + # Advertised in NIP-11 so the desktop/mobile pairing client connects + # directly instead of falling back to the legacy "/pair" + # convention. Required for mobile pairing whenever + # BUZZ_REQUIRE_RELAY_MEMBERSHIP=true, since that flag makes the relay + # advertise NIP-43 (device pairing) whether or not a pairing relay is + # actually wired up. Points at the bundled `pairing-relay` service + # below, routed through /pair — see compose.caddy.yml + Caddyfile. + BUZZ_PAIRING_RELAY_URL: ${BUZZ_PAIRING_RELAY_URL:-} ports: - "${BUZZ_HTTP_PORT:-3000}:3000" volumes: @@ -46,6 +54,32 @@ services: networks: - buzz-net + # Sidecar relay for NIP-AB mobile device pairing (kind:24134 handshake). + # Same image as `relay` (the runtime bundles buzz-relay, buzz-admin, and + # buzz-pair-relay), just a different entrypoint. Stateless/ephemeral — no + # DB, Redis, or secrets required. Not published to the host: it must sit + # behind a reverse proxy that routes only /pair to it (see + # crates/buzz-pair-relay's module docs for why). compose.caddy.yml wires + # that route automatically; a custom reverse proxy needs the equivalent + # rule pointed at pairing-relay:5000 over the buzz-net network. + pairing-relay: + image: ${BUZZ_IMAGE:-ghcr.io/block/buzz:main} + entrypoint: ["/usr/local/bin/buzz-pair-relay"] + environment: + BUZZ_PAIR_RELAY_BIND_ADDR: 0.0.0.0:5000 + # Plain TCP connect check (buzz-pair-relay has no HTTP health endpoint — + # matches the tcpSocket probe used for this same binary in + # deploy/charts/buzz/templates/pairing-relay.yaml). + healthcheck: + test: ["CMD-SHELL", "bash -ec 'exec 3<>/dev/tcp/127.0.0.1/5000'"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 10s + restart: unless-stopped + networks: + - buzz-net + postgres: image: postgres:17-alpine environment: