What hbbs and hbbr do
hbbs keeps the directory: as long as RustDesk runs on a machine, that machine keeps pinging hbbs so its current address and port are known. When computer A asks for computer B, hbbs tries to connect them directly by hole punching. Only when that fails does the session go through hbbr. The self-host docs say hole punching succeeds in the majority of cases, so the relay often carries little.
The open-source build is those two services: no web console, no user accounts, no server-side address book. RustDesk Server Pro is the separate commercial product that adds a web console, an API, OIDC, LDAP, 2FA, device management, access control and management of several relays. This page covers the open-source server only; the repository README links a guide for moving an OSS install to Pro later.
Ports and what each one carries
The docs list TCP 21114-21119 and UDP 21116, and name 21115-21117 as the minimum for RustDesk to work. The table is the port reference from the repository's docs/environment-variables.md, plus 21114 from the self-host page.
| Port | Protocol | Service | Purpose | Open it? |
|---|---|---|---|---|
| 21114 | TCP | hbbs (Pro) | HTTP API and web console, Pro only | No, not used by OSS |
| 21115 | TCP | hbbs | NAT type test | Yes |
| 21116 | TCP and UDP | hbbs | ID registration and heartbeat (UDP), hole punching and connection (TCP) | Yes, both protocols |
| 21117 | TCP | hbbr | Relay | Yes |
| 21118 | TCP | hbbs | WebSocket, web client | Only behind a proxy |
| 21119 | TCP | hbbr | WebSocket relay, web client | Only behind a proxy |
Keep 21118 and 21119 closed unless you run the web client. The docs warn that on those WebSocket ports hbbs and hbbr trust X-Real-IP and X-Forwarded-For without validating them, so anyone reaching them directly can forge the client address that rate limits, blocks and logs rely on. If you do use the web client, publish them only through a reverse proxy that sets X-Real-IP itself, and firewall them so only that proxy connects.
Sizing the machine
CPU and memory are not the constraint: the installation page calls the requirements very low, with the smallest basic cloud server or a Raspberry Pi enough. Bandwidth is the variable: a relayed session uses between 30 K/s and 3 M/s at 1920x1080 depending on resolution and screen updates, and around 100 K/s for office work, per the same page. Direct sessions never touch the relay.
hbbr caps itself (docs/environment-variables.md): SINGLE_BANDWIDTH, 128 Mb/s per relay connection by default, and TOTAL_BANDWIDTH, 1024 Mb/s across all of them. After 1800 seconds, a connection averaging above about two thirds of SINGLE_BANDWIDTH is held to LIMIT_SPEED, 32 Mb/s. Our rule of thumb: raise TOTAL_BANDWIDTH only when the port is above 1 Gbps.
A Moonshot cartridge fits: that line is meant for one service per machine. Our pick is the Intel Xeon E3-1585L v5 cartridge, the m710x, with a 1 Gbps unmetered port and its own out-of-band console; other cartridges, apart from the m510, have power control only. X-Gene, the ARM cartridge, is out: it boots only Ubuntu 16.04 and 18.04. Racked single-, dual- and quad-socket servers carry a console too (iLO, iDRAC or a Supermicro BMC). Lines, ports and cities are on the dedicated server range; a ready server takes about 30 minutes from approved payment to hand-over, a built-to-order line 4 to 24 hours.
Install with Docker Compose and host networking
The docs recommend Docker, and Compose with network_mode: host as the simplest setup on Linux; host networking lets hbbs and hbbr see the real client address instead of the container's. Install Docker Engine and the Compose plugin first, using Docker's official apt packages. The block below then turns on ufw, opens only the four ports above and writes the compose file. It stops if Docker or ufw is missing (sudo apt install ufw) or a compose file already exists. The OpenSSH profile allows port 22; if sshd listens elsewhere, allow that port before ufw is enabled.
if ! docker compose version >/dev/null 2>&1; then echo "STOP - install Docker Engine and the Compose plugin first"
elif ! sudo sh -c 'command -v ufw' >/dev/null; then echo "STOP - install ufw first (sudo apt install ufw)"
elif sudo test -e /opt/rustdesk/compose.yml; then echo "STOP - /opt/rustdesk/compose.yml exists; this block is for a first install"
elif sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && sudo ufw allow OpenSSH && sudo ufw --force enable \
&& sudo ufw allow 21115:21117/tcp && sudo ufw allow 21116/udp \
&& sudo install -d -m 700 /opt/rustdesk /opt/rustdesk/data \
&& printf '%s\n' 'services:' \
' hbbs:' ' container_name: hbbs' ' image: rustdesk/rustdesk-server:1.1.16' \
' command: hbbs' ' volumes:' ' - ./data:/root' \
' network_mode: "host"' ' restart: unless-stopped' \
' hbbr:' ' container_name: hbbr' ' image: rustdesk/rustdesk-server:1.1.16' \
' command: hbbr -k _' ' volumes:' ' - ./data:/root' \
' network_mode: "host"' ' restart: unless-stopped' \
| sudo tee /opt/rustdesk/compose.yml >/dev/null; then
echo "ok - firewall on, compose file written"
else echo "STOP - firewall or compose step did not complete"; fi
Our changes to the docs' compose file, all of them: the image is pinned to 1.1.16, the newest GitHub release at the time of writing, instead of latest; hbbr runs as hbbr -k _; and the depends_on line is dropped so hbbs can create the key pair before hbbr starts. Our ufw rules also differ from the install page's, which open 21114:21119/tcp: we leave 21114 (Pro) and 21118-21119 (web client) shut. Our note: with host networking Docker publishes no ports, so ufw filters these services like any other host process.
The key pair, the relay check and the first backup
On first start hbbs writes id_ed25519 (private) and id_ed25519.pub (public) into its working directory, which is /root in the image (docker-classic/Dockerfile) and so /opt/rustdesk/data on the host. hbbs loads that key by default and answers a connection request carrying a different key with a licence-mismatch refusal (src/rendezvous_server.rs). hbbr is the catch: its default key is empty, which docs/environment-variables.md says deliberately disables relay key validation, so any client that finds your relay can use it. Giving hbbr -k _ makes it load the same key pair from the shared folder and check it.
C=/opt/rustdesk/compose.yml; K=/opt/rustdesk/data/id_ed25519
sudo docker compose -f $C up -d hbbs \
&& for i in $(seq 30); do sudo test -s $K && sudo test -s $K.pub && break; sleep 1; done \
&& sudo test -s $K && sudo test -s $K.pub && sudo chmod 600 $K \
&& sudo docker compose -f $C up -d hbbr && sleep 3 \
&& P=$(sudo cat $K.pub) && [ -n "$P" ] \
&& sudo docker logs hbbs 2>&1 | grep -F "$P" >/dev/null \
&& sudo docker logs hbbr 2>&1 | grep -F "$P" >/dev/null \
&& echo "ok - hbbs and hbbr both hold the key; clients need $P" \
|| echo "STOP - key check did not pass; read sudo docker logs hbbr"
# backup (sudo); prunes old archives only after a good one
sudo bash -o pipefail -c 'D=/opt/rustdesk; B=/var/backups/rustdesk; o="$B/rustdesk-$(date +%F-%H%M%S).tar.gz"
mkdir -p $B && chmod 700 $B || exit 1
P=$(cat $D/data/id_ed25519.pub 2>/dev/null); [ -n "$P" ] && grep -qF "$P" <<<"$(docker logs hbbr 2>&1)" || { echo "STOP - hbbr does not hold the key; backup not run"; exit 1; }
trap "docker compose -f $D/compose.yml start" EXIT; docker compose -f $D/compose.yml stop || exit 1
if tar -czf "$o" -C /opt rustdesk && L=$(tar -tzf "$o") && grep -qx rustdesk/data/id_ed25519 <<<"$L" \
&& grep -qx rustdesk/data/db_v2.sqlite3 <<<"$L"; then echo "ok - $o"; find $B -name "rustdesk-*.tar.gz" -mtime +30 -delete
else rm -f "$o"; echo "STOP - backup FAILED, nothing pruned"; exit 1; fi'
Both services log the key they loaded (src/rendezvous_server.rs, src/relay_server.rs); the check waits for both key files, then greps both logs for id_ed25519.pub's contents. If it stops, restart the relay as root (docker compose -f /opt/rustdesk/compose.yml restart hbbr) and run the first part again; it is safe to repeat. The chmod is ours: our reading of src/common.rs is that the private key file is created with default permissions. The second part is the backup: run it now and after any change. Pasted with the check, it runs only when the relay holds the key. It stops both containers briefly, dropping relayed sessions, so later runs belong in a quiet hour; copy each archive off the machine yourself.
On each client: menu next to its ID, Network, unlock, then your host name as ID server and the public key as Key. The relay field can stay empty: clients derive it when hbbr runs on the same address on port 21117. This Key is the encryption public key, not a Pro licence key.
Restores, rollback and moving servers
Everything the server holds sits in /opt/rustdesk: the compose file, the key pair, and db_v2.sqlite3, hbbs's SQLite database (DB_URL, default ./db_v2.sqlite3). The key pair is the part you cannot recreate: a new key means re-entering it on every client. The restore below reads the archive once and refuses to touch the live install unless it holds compose.yml, both key files and the database. The rollback returns to the copy the restore set aside; it does nothing until you change GO=no to GO=yes, so pasting the whole block runs only the restore.
# restore (sudo); set A to the archive's real name first
sudo bash -c 'A=/var/backups/rustdesk/rustdesk-DATE-TIME.tar.gz; D=/opt/rustdesk
L=$(tar -tzf "$A") || { echo "STOP - cannot read $A"; exit 1; }
for m in compose.yml data/id_ed25519 data/id_ed25519.pub data/db_v2.sqlite3; do
grep -qx "rustdesk/$m" <<<"$L" || { echo "STOP - archive lacks $m"; exit 1; }; done
[ ! -e $D.previous ] || { echo "STOP - move $D.previous away first"; exit 1; }
{ [ ! -e $D/compose.yml ] || docker compose -f $D/compose.yml down; } && { [ ! -e $D ] || mv $D $D.previous; } \
&& tar -xzf "$A" -C /opt && docker compose -f $D/compose.yml up -d \
&& echo "ok - restored; any old copy is $D.previous" || echo "STOP - restore incomplete; any old copy is $D.previous"'
# rollback (sudo): return to the copy the restore set aside
sudo bash -c 'GO=no # change to GO=yes to roll back
D=/opt/rustdesk; [ "$GO" = yes ] || { echo "rollback not run - set GO=yes"; exit 0; }
[ -e $D.previous/compose.yml ] || { echo "STOP - no $D.previous to return to"; exit 1; }
[ ! -e $D.restored ] || { echo "STOP - move $D.restored away first"; exit 1; }
{ [ ! -e $D/compose.yml ] || docker compose -f $D/compose.yml down; } && { [ ! -e $D ] || mv $D $D.restored; } \
&& mv $D.previous $D && docker compose -f $D/compose.yml up -d \
&& echo "ok - rolled back; restored copy is $D.restored" || echo "STOP - rollback incomplete"'
Delete /opt/rustdesk.previous (and any /opt/rustdesk.restored) once you are happy with the result, or the next restore or rollback refuses to run.
A move to new hardware: run the install block on the target machine, copy the archive across, put it where the restore expects it (sudo install -d -m 700 /var/backups/rustdesk, then sudo mv the file into it), set A to its name and run the restore. Finish with the first part of the key block: the public key it prints must be the one your clients already have. Our advice: give clients a DNS name, not an IP; keep the key pair, repoint the name, and clients should need no change.
Updates, licence and limits
Updates: run the backup, then, with NEW as the release tag from GitHub, sudo sed -i 's/rustdesk-server:1.1.16/rustdesk-server:NEW/' /opt/rustdesk/compose.yml. Check that sudo grep -c 'rustdesk-server:NEW' /opt/rustdesk/compose.yml prints 2 (both services) before pulling; then sudo docker compose --file /opt/rustdesk/compose.yml pull and the same command with up -d. On later updates, put the tag you run now in place of 1.1.16. Re-run the key check.
Licence, read generally by us and not as legal advice: the repository's LICENSE file is the GNU Affero General Public License v3.0 (AGPL-3.0). Running it unmodified on your own machines adds no duty. Distributing copies, modified or not, means passing on the source under AGPL-3.0 as well. Modify it and let users reach your build over a network, and the AGPL requires you to offer them the source of that build. Server Pro is a separate product under its own terms.
The limits are honest ones. The OSS server has no accounts, so anyone who has your host name and public key can open sessions through it; access control is a Pro feature. If you would rather open desktops from a browser with logins in front, Apache Guacamole does that over RDP, SSH and VNC. For a Windows desktop that lives in the datacenter, rather than a route to your own machines, see Windows RDP server.
Questions
Do I still need to start hbbs with -k _?
Not in the current code. docs/environment-variables.md says hbbs defaults to -, which loads or generates the key pair, and that _ behaves the same. The relay is different: hbbr defaults to an empty key and skips validation, so give it -k _ if you want it to check the key.
Where is the public key clients need?
In id_ed25519.pub in the server's working directory; with the compose file on this page that is /opt/rustdesk/data/id_ed25519.pub. Paste its contents into the Key field under Network on each client. Never hand out id_ed25519, the private half.
Is RustDesk Server free, and what does Pro add?
The rustdesk-server repository is AGPL-3.0 and free to run. Server Pro is a separate commercial product adding a web console, API, OIDC, LDAP, 2FA, device management, access control and multi-relay management. Its pricing is on rustdesk.com, not here.