# RustDesk Server on Docker, behind Caddy Deploys the self-hosted [RustDesk](https://rustdesk.com) server (OSS) on `monoco`: the ID/rendezvous server `hbbs` and the relay server `hbbr`, both supervised by s6 inside the official `rustdesk/rustdesk-server-s6` container, reachable only through Caddy on `rust.tanshu.com:443`. No RustDesk port is published on the host. ## How the traffic is carried RustDesk's native transports on 21115/21116/21117 are a custom TCP/UDP protocol, not HTTP, so an HTTP reverse proxy cannot carry them. What *is* HTTP is the WebSocket transport that `hbbs` and `hbbr` also listen on (21118 and 21119), and clients that enable WebSocket use it for both rendezvous and relay. Caddy terminates TLS on 443 and forwards two paths into the container over the Docker network: | Path | Upstream | Purpose | | --- | --- | --- | | `/ws/id` | `rustdesk:21118` (hbbs) | ID registration and signalling | | `/ws/relay` | `rustdesk:21119` (hbbr) | Relayed sessions | The code that decides this is in the client: `hbb_common::websocket::check_ws` maps a domain `host:21116` to `wss://host/ws/id` and `host:21117` to `wss://host/ws/relay`, and the path names are fixed there. Caddy sets `X-Real-IP`, which `hbbs`/`hbbr` trust to keep the real client address for rate limiting and logs (upstream warns that these ports must therefore never be exposed directly). ## Settings - `host`: the public name, `rust.tanshu.com`. Both the Caddy site and the address `hbbs` advertises come from it. - `docker_container`, `image_name`, `image_version`: container name and the `rustdesk/rustdesk-server-s6` tag to run. - `data_location`: host directory bind-mounted to `/data` (`/var/lib/rustdesk/data`), holding the SQLite database and the server key pair. - `rustdesk_relay_server`: handed to clients by `hbbs` as the relay address. It keeps the public name, so clients rewrite it to `wss://rust.tanshu.com/ws/relay`. - `rustdesk_publish_raw_ports` / `rustdesk_raw_ports`: publish 21115/21116/21117 as well. Off by default, see "Clients that cannot use WebSocket" below. - `rustdesk_encrypted_only`: `ENCRYPTED_ONLY=1`, so `hbbr` validates the server key too instead of accepting unkeyed clients. - `rustdesk_always_use_relay`: `ALWAYS_USE_RELAY=Y`, forcing relayed sessions. WebSocket clients are relay-only anyway, so this only affects clients on the raw ports. ## Prerequisites - A DNS `A` record for `rust.tanshu.com` pointing at `monoco`; Caddy issues the certificate over ports 80/443. - The Caddy container already running on `monoco` (`caddy/playbook.yml`), with its Caddyfile at `/var/lib/caddy/conf/Caddyfile`. ## Running this Playbook The inventory in `/etc/ansible/hosts` already lists `monoco`, which is what `hosts:` targets: ```command ansible-playbook playbook.yml ``` The playbook creates the `rustdesk_net` network, attaches Caddy to it, starts the container, appends the site block to the Caddyfile, reloads Caddy, and prints the public key. ## Configuring the clients On each client, open **Settings → Network → ID/Relay Server** and set: | Field | Value | | --- | --- | | ID Server | `rust.tanshu.com` | | API Server | `https://rust.tanshu.com` | | Key | the printed value, i.e. `/var/lib/rustdesk/data/id_ed25519.pub` on the host | then enable **Settings → Network → Use WebSocket** (`allow-websocket=Y`). The **API Server** field looks odd for an OSS deployment, which has no API, but it is what selects the TLS scheme: the client uses `wss://` only when the API server starts with `https`, and otherwise silently falls back to plaintext `ws://rust.tanshu.com/...` on port 80. Because the client also skips its own client↔server key exchange whenever WebSocket is enabled — on the assumption that the transport is already encrypted — that fallback would leave the connection unprotected rather than merely failing. ## Trade-offs - **Relayed sessions only.** With WebSocket enabled the client never hole-punches (`force_relay` is set, and `hbbs` skips the same-intranet path for WebSocket peers), so every session flows through `monoco`. Remote control works, but throughput is bounded by the server, and latency is higher than a direct connection. - **Upstream labels this a Pro feature.** The client's own tooltip says "RustDesk server OSS doesn't include this feature", even though the OSS binaries do listen on 21118/21119 and the OSS docs describe proxying them. Treat the WSS path as needing a real test: if clients fail to register, the fallback is the raw ports below. - **The address has to be the domain.** `check_ws` only builds a TLS URL and the `/ws` paths for hostnames; an IP would become `ws://ip:21118` and bypass Caddy entirely. ### Clients that cannot use WebSocket Clients without the WebSocket option (older builds, third-party clients, or anything that must connect directly) need the native ports. Set `rustdesk_publish_raw_ports: true` and re-run the playbook, then open 21115-21116 (TCP) plus 21116/udp and 21117 (TCP) on the host and in any cloud firewall: ```command ufw allow 21115:21117/tcp ufw allow 21116/udp ``` WebSocket clients keep working at the same time; they ignore the raw ports. ## Notes - **Logs.** `docker logs rustdesk` covers both services. - **Server key.** If `/var/lib/rustdesk/data/id_ed25519` is lost, every client has to be reconfigured with the new public key. Back it up. - **Upgrading.** Re-running the playbook pulls the tag in `image_version` and recreates the container; the key pair and database live in `data_location` and survive.