Self-hosting a relay
The relay is one small service. It pairs a host and a viewer by ID and forwards their encrypted traffic. It never has the keys, so it can’t read sessions, but it does see connection metadata (see the security model). Running your own means that metadata stays with you, and you aren’t sharing the test relay’s limits with everyone else.
What you need
Section titled “What you need”- A server the computers can reach: any small VPS, amd64 or arm64.
- A DNS name pointing at it, such as
relay.example.com. - Ports 80 and 443 open. Port 80 only answers the certificate challenge.
The computers using OpenDesk need no open ports. They connect out to the relay on 443.
Docker Compose (recommended)
Section titled “Docker Compose (recommended)”The repository’s compose.yaml runs the relay behind
Caddy, which gets and renews a Let’s Encrypt
certificate by itself.
git clone https://github.com/treeot/opendesk.gitcd opendeskDOMAIN=relay.example.com docker compose up -dInstead of the variable you can copy .env.example to .env and set
DOMAIN there. Check that it’s up:
curl https://relay.example.com/health# {"status":"ok"}- Update:
git pull && docker compose up -d --build - Logs:
docker compose logs -f relay(setRUST_LOG=debugin.envfor more)
The Caddy config is two lines: it serves $DOMAIN and passes everything,
WebSocket upgrades included, to the relay on port 8080.
Prebuilt image
Section titled “Prebuilt image”CI publishes ghcr.io/treeot/opendesk-relay: latest and version tags from
releases, edge from main. Run it behind your own TLS proxy:
docker run -d --restart unless-stopped -p 8080:8080 \ -v relay_data:/data -e RELAY_STATE_FILE=/data/state.json \ ghcr.io/treeot/opendesk-relay:latestForward wss:// on port 443 to the container’s port 8080. The relay doesn’t
do TLS itself.
docs/DEPLOYMENT.md
also covers deploying on Coolify.
Point the apps at it
Section titled “Point the apps at it”On the home screen of each app, under RELAY REGIONS, enter one WebSocket
URL per line, ending in /ws, and click Save relay list:
wss://relay.example.com/wsOpenDesk tries them in order and uses the first one it can reach. Give the host and the viewer the same list. In the browser viewer, set the same URL under Relay.
Use wss:// for any relay you reach over a network. Plain ws:// still
keeps the session encrypted, but anyone on the path can see which IDs connect
and when. Keep ws:// for testing on 127.0.0.1 or a LAN.
Settings
Section titled “Settings”All optional, as environment variables on the relay.
| Variable | Default | Meaning |
|---|---|---|
RELAY_STATE_FILE |
unset | JSON file that keeps ID ownership across restarts. The Compose setup sets it. |
RELAY_ID_LEASE_SECS |
2592000 (30 days) |
How long an offline host keeps its ID. |
RELAY_TRUSTED_PROXIES |
loopback and private ranges | Comma-separated CIDRs of your reverse proxies, or none. |
RELAY_MAX_ID_BINDINGS |
100000 |
Most IDs tracked at once. New IDs are refused when full. |
RELAY_NEW_ID_BURST / RELAY_NEW_ID_REFILL_SECS |
20 / 3600 |
How many new IDs one IP (or IPv6 /64) may claim per window. |
RELAY_IDLE_TIMEOUT_SECS |
60 |
Closes a paired connection after this long without traffic. |
RELAY_SESSION_CONTROL_BURST / RELAY_SESSION_CONTROL_PER_SEC |
64 / 16 |
Control messages allowed per paired connection. |
RELAY_MAX_RATE_ENTRIES |
100000 |
Most IPs tracked by each rate limiter. |
RELAY_SWEEP_SECS |
60 |
How often expired IDs are cleared and the state file saved. |
IDs. The first host to register an ID owns it until it has been offline for the lease period. The state file stores a SHA-256 hash of each host’s token, not the token. Without a state file, ownership resets when the relay restarts.
Client addresses. Rate limits and logs use the client’s address. Behind a
trusted proxy, the relay reads it from X-Forwarded-For or X-Real-IP. The
defaults suit a relay on a private Docker network behind Caddy or Traefik. If
the relay’s port is reachable from a network you don’t control, narrow
RELAY_TRUSTED_PROXIES to your proxy’s address.
To find a direct path, the apps ask Google’s public STUN server
(stun.l.google.com:19302) for their public address. STUN only helps
discover addresses; it never carries session data. Changing it is a client
setting in the source (AgentConfig.urls in host-core), not a relay option.