-
Notifications
You must be signed in to change notification settings - Fork 4
RustDesk Server in Docker
CortenDesk is a console for a RustDesk server you already run. It does not include one. If devices never appear, the cause is usually the server rather than the console — a key that does not match, UDP 21116 closed, or a relay address the clients cannot reach.
This page gets you from nothing to a working hbbs/hbbr pair, then points
CortenDesk at it.
hbbs — the ID/rendezvous server. Devices register their ID and public key with it, heartbeat to it so it knows where they are, and ask it to broker a connection to another ID. It keeps the peer table in a SQLite file next to its key material.
hbbr — the relay. When two peers cannot reach each other directly, both connect to hbbr and it forwards bytes between them. It knows nothing about IDs, users or address books; it pairs two sockets by a UUID and copies traffic.
Most sessions never touch hbbr. The ones behind symmetric NAT or a restrictive firewall always do, which is why a server that "mostly works" is usually one with a broken relay.
Two official images are published, from the same source:
| Image | Contents |
|---|---|
rustdesk/rustdesk-server |
Both binaries, no init system. Run two containers, one per role. |
rustdesk/rustdesk-server-s6 |
Both processes supervised in a single container. |
Both are mirrored to ghcr.io/rustdesk/rustdesk-server and
ghcr.io/rustdesk/rustdesk-server-s6. Each release pushes latest, the exact
version (1.1.16), and the major tag (1).
The rest of this page uses the two-container image. Splitting hbbs and hbbr makes the ports and the logs obvious, and you can move the relay to another host later without changing anything else.
Do not run a second API server. CortenDesk is the API server and console — the thing clients set as their API Server, and the thing that owns users, address books and device records. Some third-party server bundles ship their own API server and web UI in the same container. Running one alongside CortenDesk gives you two systems claiming the same clients, two user databases, and address books that silently diverge. Use the official images above, which are hbbs and hbbr and nothing else.
services:
hbbs:
image: rustdesk/rustdesk-server:latest
command: hbbs -r rustdesk.example.com:21117
ports:
- "21115:21115"
- "21116:21116"
- "21116:21116/udp"
- "21118:21118"
volumes:
- ./data:/root
depends_on:
- hbbr
restart: unless-stopped
hbbr:
image: rustdesk/rustdesk-server:latest
command: hbbr -k _
ports:
- "21117:21117"
- "21119:21119"
volumes:
- ./data:/root
restart: unless-stoppedmkdir -p data
docker compose up -d
docker compose logs -fThree things in there matter.
-r rustdesk.example.com:21117 is the relay address hbbs hands out to
clients. It has to be an address the clients can reach — a public hostname or
IP. hbbr, localhost or a private address works on your bench and fails for
every device outside it. This is the "connects, then sits at connecting"
failure.
Both services mount the same ./data. /root is the working directory of
both binaries. hbbs writes the keypair and the peer database there; hbbr's
-k _ reads the same private key file. If they do not share the directory,
hbbr generates a different keypair and rejects every relay request.
-k _ on hbbr turns on key enforcement for the relay. hbbs enforces a key
by default; hbbr does not unless you ask. Without it, anyone who can reach
21117 can use your relay as an open proxy.
hbbs also writes db_v2.sqlite3 into that directory — the peer table. Back up
./data as a whole.
| Port | Protocol | Process | Carries | Closed means |
|---|---|---|---|---|
| 21115 | TCP | hbbs | NAT-type test, and the "which of these peers are online" query | Clients cannot determine their NAT type; peers in the client's own list all show offline |
| 21116 | UDP | hbbs | ID registration and heartbeat | Devices never come online. See below. |
| 21116 | TCP | hbbs | Connection setup — punch-hole requests, local address exchange, relay negotiation | Sessions never start |
| 21117 | TCP | hbbr | The relay itself | Any session that cannot go peer-to-peer fails |
| 21118 | TCP | hbbs | The rendezvous protocol over WebSocket | The in-browser web client cannot reach the ID server |
| 21119 | TCP | hbbr | The relay over WebSocket | The in-browser web client cannot relay |
21115 and 21116 are derived from the ID port, not configurable independently:
hbbs listens on --port, --port - 1 and --port + 2. hbbr listens on
--port and --port + 2. Change the base port and everything else moves with
it.
Registration and heartbeat are UDP. Everything else on hbbs is TCP, so a
firewall rule set written by pattern — or copied from a TCP-only example — ends
up with 21115:21119/tcp and no UDP at all.
The symptom is specific and misleading: clients start normally, show an ID, report no error, and never come online. Not to each other, and not in CortenDesk. There is nothing in the client log to look at, because from the client's point of view it sent a packet and simply got no reply.
sudo ufw allow 21115:21119/tcp
sudo ufw allow 21116/udpCheck the cloud security group separately — that is a second firewall, and on most providers UDP is not in the default template. Publishing a container port does not open a security group.
Port scanning UDP is unreliable; an open UDP port and a filtered one usually look identical from outside. The definitive test is the server log — see Check it works.
On first start hbbs generates an ed25519 keypair in its working directory and writes two files:
| File | Contents |
|---|---|
id_ed25519 |
The private key, base64. Stays on the server. |
id_ed25519.pub |
The public key, base64, a single line with no trailing newline. |
With the compose above they appear in ./data on the host:
cat data/id_ed25519.pubRead them on the host, not through docker exec. The two-container image is
built FROM scratch — it contains the two binaries and no shell, so there is
nothing to exec into.
The public key is the one you distribute. It goes in three places:
- the Key field of every RustDesk client
-
CORTENDESK_PUBLIC_KEYin the console's environment - the console's Settings screen, which then shows it back to your users as a copy-paste value
The private key never leaves the server.
hbbs enforces by default. --key defaults to -, which means "load or
generate the keypair, and require clients to present the matching public key".
A client with a wrong or empty key gets a licence mismatch on every connection
attempt.
hbbr does not enforce unless started with -k. -k _ means "read the public
key out of id_ed25519 in the working directory and require it". You can also
pass a literal key: -k <base64 public key>.
Registration itself is not key-checked. That is why a key mismatch looks like "the device is online but nothing can connect to it" rather than "the device is missing".
Delete id_ed25519 and hbbs generates a fresh pair on the next start. Every
client still configured with the old key is locked out of connecting — they
keep registering and keep showing online, and every session attempt fails with
a mismatch. There is no gradual migration and no way to accept both. You push
the new key to every device, and you update CORTENDESK_PUBLIC_KEY.
Back up ./data. Restoring or moving a server is a matter of placing
id_ed25519 and id_ed25519.pub into the directory before the first
start; hbbs uses existing files and only generates when both are absent. A
malformed private key is fatal — hbbs exits rather than quietly minting a new
one.
Three values, all from the server you just built:
| Variable | Value |
|---|---|
CORTENDESK_ID_SERVER |
hbbs.example.com:21116 |
CORTENDESK_RELAY_SERVER |
hbbs.example.com:21117 — wherever hbbr is reachable |
CORTENDESK_PUBLIC_KEY |
the contents of id_ed25519.pub, the bare base64 string |
The public key is a single line. Do not add a newline, quotes or whitespace — it is compared byte for byte against what the client sends.
CortenDesk does not manage hbbs and does not read its database. Devices report to CortenDesk over its own HTTP API, which is why each client needs its API Server field pointed at the console as well as the three fields above. The console's Settings screen prints all four as copy-paste values.
Full install instructions: Install with Docker or Install on a VM.
The in-browser client speaks the same protocol over WebSocket, on 21118 (hbbs)
and 21119 (hbbr). The CortenDesk container already bridges /ws/id to 21118 and
/ws/relay to 21119, so the browser only ever talks to the console's hostname.
Those two ports have to be reachable from the CortenDesk container. They do
not have to be reachable from the internet, and preferably are not: the
WebSocket listeners trust X-Real-IP and X-Forwarded-For as sent, so anything
that can reach them directly can claim any source address it likes. Keep them
behind the console's bridge or your own proxy.
If the console is served over HTTPS, browsers refuse a plain ws:// connection
from the page, so CORTENDESK_WS_ID_URL and CORTENDESK_WS_RELAY_URL must be
wss:// URLs on a hostname your proxy terminates. That is the whole of
Reverse proxy and TLS, including why the in-browser
client cannot work over plain HTTP at all.
A healthy hbbs start:
Listening on tcp/udp :21116
Listening on tcp :21115, extra port for NAT test
Listening on websocket :21118
relay-servers=["rustdesk.example.com:21117"]
Key: 3gJK...=
Two lines to actually read:
-
relay-servers=[...]— entries that fail to resolve are dropped silently, so an empty list here means your-rargument is wrong and clients will be told there is no relay. -
Key:— this is the public key, and it must equalcat data/id_ed25519.pubandCORTENDESK_PUBLIC_KEY.
On the very first start you also get Private/public key written to id_ed25519/id_ed25519.pub. On every start after that,
Private key comes from id_ed25519. Seeing the first message twice means the
volume is not persisting and you have just invalidated every client.
hbbr:
Listening on tcp :21117
Listening on websocket :21119
Then register a device. Set ID Server, Relay Server and Key in the client, restart it, and watch:
docker compose logs -f hbbsA device registering logs a line beginning update_pk, with the ID, address,
UUID and public key. If it never appears, UDP 21116 is not getting through —
that log line is written from the UDP handler and from nowhere else.
It is written on the first registration and whenever a device's key, UUID or IP changes, not on every heartbeat. A device that is already registered and has not moved is silent, which is normal.
Devices never come online. UDP 21116. Check the host firewall and the cloud
security group, then confirm with update_pk in the hbbs log.
Devices are online, connections fail immediately. Key mismatch. Compare the
client's Key field, data/id_ed25519.pub, and the Key: line in the hbbs log.
All three are the same string or nothing works.
Connections hang at connecting, then time out. Direct connection failed and
the relay is unreachable. Check relay-servers=[...] in the hbbs log, that
-r names an address the client can resolve, and that 21117 is open.
hbbr rejects everything after you add -k _. hbbr cannot see
id_ed25519 — the volume is not shared with hbbs, or hbbr started first and
gave up waiting. It then generates its own keypair, which matches nothing. Make
sure both services mount the same directory, and delete the stray keypair if
hbbr wrote one into its own.
Everything works except the in-browser client. 21118 and 21119, reachable
from the CortenDesk container, plus wss:// URLs — see
Reverse proxy and TLS.
Clients report an unknown NAT type, or every peer shows offline in the client's own list. TCP 21115.