Install the Relay
The relay is a stateless QUIC forwarder for NAT traversal between edges. It carries opaque ciphertext only — relay operators can’t see your media. You only need a relay if your edge sites can’t reach each other directly (different ASNs, double-NAT, restrictive firewalls). Two edges on the same LAN, or edges connected over a site-to-site VPN, don’t need it.
What you’ll need
Section titled “What you’ll need”- A Linux host on a public IP, or behind a static port-forward of UDP 4433. Can share a box with the manager — the relay installs alongside under different paths (
/opt/bilbycast-relay/,/etc/bilbycast/) so the two coexist cleanly. - About 5 minutes.
The relay is statically linked against musl, has no runtime dependencies, and runs on x86_64 and aarch64.
Ports & firewall
Section titled “Ports & firewall”The relay listens on two ports:
| Port | Protocol | Source | Purpose |
|---|---|---|---|
| 4433 | UDP (QUIC / TLS 1.3) | Every edge that pairs through this relay | Tunnel data plane. Override with --quic-addr. |
| 4480 | TCP (HTTP) | Manager host, your monitoring host | REST stats + /health. Optional — close it if you don’t query stats remotely. Override with --api-addr. |
The relay itself connects outbound to the manager on TCP 8443 (wss://), so no inbound port is needed for control. If you front the relay with a load balancer (multiple relay instances for HA), the LB needs UDP/QUIC pass-through on 4433 — not TLS termination, since QUIC carries its own TLS 1.3.
Bind address vs advertised address
Section titled “Bind address vs advertised address”QUIC binds on 0.0.0.0:4433 (every interface) by default — that’s what the relay listens on. Remote edges need a different value: the public address they dial to reach you. These two are distinct any time the relay sits behind NAT, a cloud-instance public-IP mapping, or a load balancer:
- Bind address (
quic_addr/quic_addrs) — the listen socket.0.0.0.0:4433is correct for most installs. - Advertised address (
public_quic_addr) — what edges connect to. Set this to a hostname or IP edges can actually reach. The manager reads it from health and pre-populates the tunnel-creation dropdown. Without it, the manager can’t auto-fill a usable relay address for tunnel configs and the relay shows as disabled in the dropdown.
Prefer a DNS name when you have one (relay.example.com:4433). It survives Lightsail static-IP releases, cloud instance migrations, and lets you front a pool of relays behind one record. Edges resolve the name on every connect attempt, so an IP change behind the record is picked up automatically without a tunnel reconfig. Falls back to a raw IP literal cleanly when DNS isn’t an option (54.1.2.3:4433, [2001:db8::1]:4433).
Full network map: Deployment overview.
1. Download
Section titled “1. Download”curl -fsSL -O https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/bilbycast-relay-$(uname -m)-linuxcurl -fsSL -O https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/bilbycast-relay-$(uname -m)-linux.sha256sha256sum -c bilbycast-relay-$(uname -m)-linux.sha256
# Rename to bilbycast-relay so subsequent commands are arch-agnosticmv bilbycast-relay-$(uname -m)-linux bilbycast-relaychmod +x bilbycast-relayYou should see bilbycast-relay-x86_64-linux: OK (or aarch64). The rename keeps the rest of this page concise; the canonical name is what the .sha256 file expects, so we verify under that name before moving.
Verify the Sigstore signature (optional)
Section titled “Verify the Sigstore signature (optional)”Every release ships a Sigstore-signed manifest.json alongside the bare binaries. The sha256sum -c step above catches mid-transfer corruption; verifying the signature additionally proves the manifest was published by the Bilbycast release workflow on a tagged commit.
Install cosign — on Ubuntu / Debian the simplest path is the upstream static binary with SHA-256 verification:
COSIGN_VERSION=v2.4.1case "$(uname -m)" in x86_64) COSIGN_ARCH=amd64 ;; aarch64) COSIGN_ARCH=arm64 ;; *) echo "Unsupported architecture: $(uname -m)"; exit 1 ;;esacCOSIGN_ASSET="cosign-linux-${COSIGN_ARCH}"curl -fsSL -o /tmp/cosign \ "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/${COSIGN_ASSET}"expected="$(curl -fsSL "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign_checksums.txt" | awk -v a="${COSIGN_ASSET}" '$2 == a {print $1}')"got="$(sha256sum /tmp/cosign | awk '{print $1}')"[[ -n "${expected}" && "${got}" == "${expected}" ]] || { echo "cosign checksum mismatch"; exit 1; }sudo install -m 0755 /tmp/cosign /usr/local/bin/cosign && rm /tmp/cosignThen verify the manifest:
curl -fsSL -O https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/manifest.jsoncurl -fsSL -O https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/manifest.sig.bundle
cosign verify-blob \ --bundle manifest.sig.bundle \ --certificate-identity-regexp 'https://github.com/Bilbycast/bilbycast-relay/.github/workflows/nightly-release.yml@refs/tags/v.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ manifest.jsonA successful verify prints Verified OK. The same Sigstore-signed manifest drives the upgrade flow below, so this is the verifier’s main checkpoint.
2. Standalone (zero config)
Section titled “2. Standalone (zero config)”The simplest deployment — useful for testing or when you don’t need the relay reporting back to the manager:
./bilbycast-relayDefaults: QUIC on 0.0.0.0:4433, REST on 0.0.0.0:4480. Override with --quic-addr or --api-addr. Ctrl-C to stop.
3. Attached to the manager (recommended)
Section titled “3. Attached to the manager (recommended)”In the manager UI:
- Go to Admin → Nodes, click + Add Node, pick device type Relay.
- Copy the one-shot registration token.
Next to the relay binary, write relay.json (replace REPLACE_WITH_YOUR_MANAGER_HOSTNAME, REPLACE_WITH_YOUR_RELAY_HOSTNAME, and <token-from-manager> with the real values — don’t paste this verbatim):
{ "quic_addr": "0.0.0.0:4433", "api_addr": "0.0.0.0:4480", "public_quic_addr": "REPLACE_WITH_YOUR_RELAY_HOSTNAME:4433", "require_bind_auth": true, "manager": { "enabled": true, "urls": [ "wss://REPLACE_WITH_YOUR_MANAGER_HOSTNAME:8443/ws/node" ], "registration_token": "<token-from-manager>" }}public_quic_addr is the address edges will dial — see Bind address vs advertised address above. Prefer a DNS name (e.g. relay.example.com:4433) over a raw IP. If the relay shares a host with the manager (typical for small deployments on a single cloud instance), neither the manager nor the relay can discover this from the WS connection alone — you have to set it explicitly. Unspecified values (0.0.0.0:4433, [::]:4433) are rejected at config load.
urls is an array (1-16 entries, each must be wss://). For a single manager that’s one entry; for an HA-paired manager cluster you’d list both hostnames — the relay tries them in order and rotates on WebSocket close with a 5-second backoff.
Launch:
./bilbycast-relay --config relay.jsonFor a self-signed manager cert (only relevant if you skipped ACME / Let’s Encrypt on the manager), add "accept_self_signed_cert": true inside the manager block and export BILBYCAST_ALLOW_INSECURE=1 before launching. The env var is a deliberate safety guard.
On first connect the relay swaps the registration token for a permanent node_id + node_secret, rewrites relay.json in place with those values (removing the now-spent registration token), and reconnects automatically going forward. Don’t be surprised when your relay.json looks different after the first boot — that’s the credential persistence working as intended. The file’s runtime user (bilbycast on the systemd install in step 4) must therefore be able to write it; step 4 sets the ownership accordingly.
4. systemd service
Section titled “4. systemd service”For production, run the relay as a systemd service. Drop into /etc/systemd/system/bilbycast-relay.service:
[Unit]Description=bilbycast-relay QUIC NAT traversalAfter=network-online.targetWants=network-online.target
[Service]Type=simpleUser=bilbycastGroup=bilbycastWorkingDirectory=/opt/bilbycast-relayExecStart=/opt/bilbycast-relay/bilbycast-relay --config /etc/bilbycast/relay.jsonRestart=on-failureRestartSec=2sLimitNOFILE=65536Environment=RUST_LOG=info
[Install]WantedBy=multi-user.targetThen:
# `|| true` — bilbycast user may already exist from a manager install on this boxsudo useradd -r -s /sbin/nologin bilbycast || truesudo mkdir -p /opt/bilbycast-relay /etc/bilbycastsudo install -m 0755 -o bilbycast -g bilbycast bilbycast-relay /opt/bilbycast-relay/# relay.json: bilbycast OWNS it (not root) — the relay rewrites this file on# first connect to swap the registration_token for the permanent node_id +# node_secret. Root-owned 0640 would block that write and you'd be stuck on# next restart with a spent registration token.sudo install -m 0640 -o bilbycast -g bilbycast relay.json /etc/bilbycast/relay.jsonsudo systemctl daemon-reloadsudo systemctl enable --now bilbycast-relaysudo systemctl status bilbycast-relay --no-pagerExpected: active (running). Logs: sudo journalctl -u bilbycast-relay -f.
Co-existing with the manager on the same box
Section titled “Co-existing with the manager on the same box”If this box also runs the manager, the two installs occupy separate trees so they don’t collide:
| Manager | Relay | |
|---|---|---|
| Binary | /opt/bilbycast-manager/bilbycast-manager | /opt/bilbycast-relay/bilbycast-relay |
| Config + secrets | /etc/bilbycast-manager/manager.env | /etc/bilbycast/relay.json |
| Systemd unit | bilbycast-manager.service | bilbycast-relay.service |
| Service user | bilbycast (shared) | bilbycast (shared) |
Same bilbycast user owns both — useradd ... \|\| true above is idempotent.
Upgrading
Section titled “Upgrading”The relay ships an operator-run upgrade script. It downloads the latest signed manifest.json + manifest.sig.bundle, verifies the Sigstore signature against the publishing workflow’s identity (auto-installing cosign with checksum verification if it isn’t already on the host), pulls the matching arch-specific binary (x86_64 / aarch64), verifies SHA-256 against the signed manifest, atomically swaps the binary with a .previous backup, restarts the systemd unit, polls /health, and auto-rolls back to the previous binary on a failed health probe.
The simplest path is curl-pipe-bash from the latest release:
curl -fsSL https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/upgrade-relay.sh \ | sudo bashOperators who’d rather review the script first can grab it once and re-run it as needed:
curl -fsSL -o upgrade-relay.sh \ https://github.com/Bilbycast/bilbycast-relay/releases/latest/download/upgrade-relay.shchmod +x upgrade-relay.shsudo ./upgrade-relay.sh # apply latest stablesudo ./upgrade-relay.sh --dry-run # download + verify only; print plansudo ./upgrade-relay.sh --target-version 0.7.0 # pin to a specific tagThe relay is stateless — a restart drops connected edges, which all reconnect automatically. For zero-disruption upgrades, run multiple relay instances behind a load balancer and roll through them one at a time. Pass --help for every flag, including --service, --binary-path, --health-url, --health-timeout, --no-rollback, and --no-verify-cosign (for air-gapped boxes that can’t install cosign).
The script requires the systemd unit from step 4 — it reads systemctl cat bilbycast-relay to auto-detect the binary path. On a foreground-only install it errors out with systemd unit 'bilbycast-relay' not found. For a foreground install, do the swap by hand:
# 1. Stop the foreground ./bilbycast-relay process (Ctrl-C in its terminal).
# 2. Backup the running binary so you can roll back if needed.rm -f bilbycast-relay.previousmv bilbycast-relay bilbycast-relay.previous
# 3. Re-run step 1's download block to fetch + verify the new binary# into CWD (the final `mv … bilbycast-relay && chmod +x` lands it# next to the .previous backup).
# 4. Restart../bilbycast-relay --config relay.json
# Rollback (if the new version misbehaves):# mv bilbycast-relay.previous bilbycast-relayGoing further
Section titled “Going further”The single-host systemd install above is the right shape for most deployments. For larger or more redundant setups:
- Multiple relays behind a load balancer — the relay is stateless, so an LB doing UDP/QUIC pass-through on 4433 across several relay instances gives you horizontal scale + zero-disruption upgrades. Roll one relay at a time using the upgrade script above; edges reconnect transparently.
- Geographic redundancy — run a relay in each region; edges can be configured with multiple relay candidates and will fail over on tunnel loss.
- Relay security — bind tokens, end-to-end tunnel encryption, why operators can’t see media.
Where to read next
Section titled “Where to read next”- Relay architecture — internal design and stateless forwarding.
- Relay events and alarms — what’s emitted when tunnels go up or down.
- Relay stats reference — Prometheus metrics.