diff --git a/deploy/compose/.env.example b/deploy/compose/.env.example index cebe879da0..9c44a88b16 100644 --- a/deploy/compose/.env.example +++ b/deploy/compose/.env.example @@ -35,13 +35,28 @@ BUZZ_S3_ACCESS_KEY=CHANGE_ME_RANDOM_ACCESS_KEY BUZZ_S3_SECRET_KEY=CHANGE_ME_RANDOM_SECRET_KEY BUZZ_S3_BUCKET=buzz-media -# Optional host ports. Base compose publishes the relay directly on BUZZ_HTTP_PORT. +# Optional host ports. Base compose publishes the relay directly on +# BUZZ_HTTP_PORT and the pairing sidecar on BUZZ_PAIR_RELAY_PORT; +# compose.caddy.yml unpublishes both and fronts them with Caddy. BUZZ_HTTP_PORT=3000 +BUZZ_PAIR_RELAY_PORT=5000 # Caddy host ports. Only used with compose.caddy.yml. CADDY_HTTP_PORT=80 CADDY_HTTPS_PORT=443 +# Device pairing (mobile QR). The stack always runs the buzz-pair-relay sidecar. +# - TLS mode (compose.caddy.yml): Caddy routes /pair on your main domain to the +# sidecar, so the desktop's NIP-43 legacy fallback works with no config here. +# - Split-domain or your own proxy: advertise a dedicated URL so the desktop +# uses it directly instead of the /pair fallback. Must be ws:// or wss://. +# BUZZ_PAIRING_RELAY_URL=wss://pair.buzz.example.com +# - Non-TLS / direct: the pair service is already published on +# BUZZ_PAIR_RELAY_PORT, so just point clients at it, e.g. +# BUZZ_PAIRING_RELAY_URL=ws://buzz.example.com:5000 — otherwise use +# TLS mode. See README.md § Device pairing. +# BUZZ_PAIRING_RELAY_URL= + # Dev override ports. Only used with compose.dev.yml. POSTGRES_PORT=5432 REDIS_PORT=6379 diff --git a/deploy/compose/Caddyfile b/deploy/compose/Caddyfile index 205cf4c5bc..3eef421fc3 100644 --- a/deploy/compose/Caddyfile +++ b/deploy/compose/Caddyfile @@ -1,5 +1,15 @@ {$BUZZ_DOMAIN} { encode zstd gzip - reverse_proxy relay:3000 + # Device-pairing sidecar. The desktop's NIP-43 legacy fallback resolves to + # wss://{$BUZZ_DOMAIN}/pair, so routing that path to buzz-pair-relay makes + # mobile pairing work with no extra DNS. The sidecar ignores the request path. + @pair path /pair /pair/* + handle @pair { + reverse_proxy pair:5000 + } + + handle { + reverse_proxy relay:3000 + } } diff --git a/deploy/compose/README.md b/deploy/compose/README.md index 0de524fb5b..b0f653f247 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -41,6 +41,37 @@ keypair. Run `./run.sh backup-hint` for the backup checklist. +## Device pairing + +Mobile pairing needs a dedicated pairing relay (`buzz-pair-relay`). The desktop +generates the QR, then resolves where the phone should connect: it first reads +the main relay's NIP-11 `pairing_relay_url`, and if that is unset it falls back +to the legacy `/pair` path on the main relay. The base relay does **not** serve +`/pair`, so the stack runs a `pair` sidecar (bundled in the same relay image) +and the deployment just has to route traffic to it. Three cases: + +- **TLS (`compose.caddy.yml`) — works out of the box.** Caddy routes + `/pair` on your main domain to the sidecar, so the desktop's legacy fallback + (`wss:///pair`) Just Works with no extra DNS and nothing to set. The + overlay unpublishes the sidecar's host port; Caddy is the only way in. +- **Split domain or your own reverse proxy.** Expose the sidecar at its own + host name and advertise it so the desktop uses it directly: + set `BUZZ_PAIRING_RELAY_URL=wss://pair.` in `.env` and point that + name at the sidecar in your proxy — the base stack publishes it on + `BUZZ_PAIR_RELAY_PORT` (default 5000). If your proxy runs *outside* this + compose project and you would rather not publish the port, attach the proxy + to the project's `buzz-net` network and target `pair:5000` directly. The + relay then advertises the URL in NIP-11 and the desktop skips the `/pair` + fallback. +- **Non-TLS / direct (no Caddy).** The base stack publishes the sidecar on + `BUZZ_PAIR_RELAY_PORT` (default 5000); set + `BUZZ_PAIRING_RELAY_URL=ws://:5000` so the QR points at it. Without + that, the desktop QR points at a `/pair` endpoint the relay 404s and pairing + fails. + +`BUZZ_PAIRING_RELAY_URL` must be a `ws://` or `wss://` URL; the relay rejects +anything else at startup. + ## 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..3ae0709ac0 100644 --- a/deploy/compose/compose.caddy.yml +++ b/deploy/compose/compose.caddy.yml @@ -2,11 +2,17 @@ services: relay: ports: !reset [] + # Caddy routes /pair to the sidecar, so its direct host port goes away too. + pair: + ports: !reset [] + caddy: image: caddy:2-alpine depends_on: relay: condition: service_healthy + pair: + 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..6692309d22 100644 --- a/deploy/compose/compose.yml +++ b/deploy/compose/compose.yml @@ -46,6 +46,31 @@ services: networks: - buzz-net + # Ephemeral NIP-AB device-pairing sidecar. The relay image already ships the + # binary; without this service the desktop QR resolves to a /pair endpoint the + # relay itself does not serve and mobile pairing fails. Published directly on + # BUZZ_PAIR_RELAY_PORT, mirroring the relay's BUZZ_HTTP_PORT; compose.caddy.yml + # unpublishes it and routes /pair through Caddy instead (see README.md + # § Device pairing). + pair: + 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 + ports: + - "${BUZZ_PAIR_RELAY_PORT:-5000}:5000" + # The sidecar speaks only WebSocket upgrades and answers a plain GET with 400, + # so probe the raw TCP port over /dev/tcp rather than expecting an HTTP 200. + healthcheck: + test: ["CMD-SHELL", "bash -ec 'exec 3<>/dev/tcp/127.0.0.1/5000'"] + interval: 10s + timeout: 3s + retries: 6 + start_period: 5s + restart: unless-stopped + networks: + - buzz-net + postgres: image: postgres:17-alpine environment: