How the two halves work together
Think of hbbs as a switchboard: every computer running RustDesk keeps checking in, so it always knows where each one is. When you ask to reach another machine, hbbs first tries to connect the two of you straight to each other (a trick called hole punching). RustDesk's docs say this works in most cases, and then hbbr is never involved. hbbr, the relay, only steps in when a direct link is impossible, for example behind strict office routers.
The free server is exactly those two services: no user logins, no web dashboard, no shared address book. Those come with RustDesk Server Pro, a separate paid product with an API, a web console, OIDC or LDAP sign-in, two-factor login, device management, access control and handling of several relays. The README links a guide for converting a free install to Pro.
Will a small server keep up?
Yes. RustDesk's installation page calls its hardware needs very low: the most basic cloud server, or even a Raspberry Pi, will do. The network is what to think about. A session that goes through the relay uses anywhere from 30 K/s up to 3 M/s on a 1920x1080 screen, depending on resolution and how much changes on it; plain office work sits around 100 K/s, according to that page. Sessions that connect directly never pass through the relay, so they use next to none of the server's bandwidth.
The relay also limits itself. Per docs/environment-variables.md in the repository, it allows 128 Mb/s for each relayed session and 1024 Mb/s for all of them together (SINGLE_BANDWIDTH and TOTAL_BANDWIDTH). In our view you only need to touch them on a port faster than that.
So the right machine is a small one with a good port. Our pick is the m710x cartridge with its Intel Xeon E3-1585L v5 processor: its port runs at 1 Gbps, unmetered, and you can open its out-of-band console yourself, which matters if a firewall change ever locks you out of SSH. That console comes with rack servers and blades too, and among cartridges with the m510 and m710x; any other cartridge offers power control alone. As for ARM, the X-Gene line only runs old Ubuntu releases, so it is not an option. Compare every option on our dedicated servers page; ready servers are handed over within 30 minutes of approval.
Which ports to open, and which to leave shut
- TCP 21115 for hbbs's NAT type test.
- TCP and UDP 21116 for hbbs: UDP handles ID registration and the heartbeat, TCP handles hole punching and connections. Both are needed.
- TCP 21117 for hbbr, the relay.
- TCP 21118 and 21119 only if you use RustDesk's web client. Leave them closed otherwise.
- TCP 21114 belongs to Server Pro's web console and API. The free server does not need it.
RustDesk's docs name 21115 to 21117 as the minimum for it to work. They also warn that, on the web-client ports 21118 and 21119, the server believes whatever client address the X-Real-IP or X-Forwarded-For headers claim, so someone reaching those ports directly can fake their address and dodge rate limits and blocks. If you need the web client, put those two ports behind a reverse proxy which writes that header itself, and let only the proxy reach them.
Up and running in two blocks
RustDesk recommends Docker, and on Linux it suggests Docker Compose with host networking so the server sees your computers' real addresses. Install Docker Engine and its Compose plugin from Docker's official apt repository, then paste the first block. It switches on the firewall with SSH allowed, opens the four ports and writes the compose file. If Docker isn't there, or ufw isn't (it installs with sudo apt install ufw), or you already have a compose file, it stops and tells you why. The OpenSSH rule it adds lets port 22 in; if your SSH runs on another port, allow that one before the firewall goes on.
D=/opt/rustdesk-server
if ! docker compose version >/dev/null 2>&1; then echo "STOP - Docker Engine with the Compose plugin is not installed"
elif ! sudo sh -c 'command -v ufw' >/dev/null; then echo "STOP - ufw is missing; run sudo apt install ufw"
elif sudo test -e $D/compose.yml; then echo "STOP - $D/compose.yml is already there; first installs only"
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 $D $D/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 $D/compose.yml >/dev/null; then
echo "ok - firewall is on and the compose file is written"
else echo "STOP - something in the firewall or compose step did not finish"; fi
What we changed from RustDesk's sample compose file, in full: we pin the image to release 1.1.16, the newest on GitHub as we write, rather than latest; the relay starts as hbbr -k _; and we removed depends_on so hbbs gets to create the key before the relay starts. The install page's firewall example opens 21114 to 21119; we open only what the free server needs. One more note of ours: with host networking, Docker adds no port forwarding of its own, so ufw guards these ports as it would any program on the server.
Lock the relay to your key, then save it
The first time hbbs starts, it creates a key pair in its data folder: id_ed25519, which is private, and id_ed25519.pub, the public key your apps need. In this setup that folder is /opt/rustdesk-server/data, because the image works from /root (see docker-classic/Dockerfile). hbbs uses the key straight away and turns down connection requests carrying the wrong one. The relay does not, unless asked: per docs/environment-variables.md, hbbr's key is empty by default, which switches its check off on purpose. Hence -k _ in the compose file, so it reads the same key pair and checks it.
C=/opt/rustdesk-server/compose.yml; K=/opt/rustdesk-server/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-server; B=/var/backups/rustdesk-server; o="$B/rustdesk-server-$(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-server && L=$(tar -tzf "$o") && grep -qx rustdesk-server/data/id_ed25519 <<<"$L" \
&& grep -qx rustdesk-server/data/db_v2.sqlite3 <<<"$L"; then echo "ok - $o"; find $B -name "rustdesk-server-*.tar.gz" -mtime +30 -delete
else rm -f "$o"; echo "STOP - backup FAILED, nothing pruned"; exit 1; fi'
Each service logs the key it uses at start (src/relay_server.rs and src/rendezvous_server.rs). The first part waits until both key files exist, and says ok only when your public key shows up in both logs. On STOP, restart the relay (sudo docker restart hbbr) and paste that part again; repeating it is harmless. Tightening the private key to mode 600 is our addition: we read src/common.rs as writing that file with ordinary permissions. The second part makes a backup, and only when the relay really holds your key, so pasting both parts together is safe. Run it as soon as you see ok, since the key pair cannot be remade, and after any change. It pauses both services briefly, dropping relayed sessions, so pick a quiet time; copy each archive off this server.
Pointing your computers at it
- In the RustDesk app, open the menu beside your ID and choose Network.
- Unlock the settings (it asks for administrator rights).
- Type your server's name, such as rustdesk.example.com, into ID server.
- Paste the public key from the block above into Key.
- Leave the relay field empty: the app works it out when the relay shares the server's address and uses port 21117.
After the first app, Export Server Config copies these settings for the next computer to import. The Key is your server's encryption key, not the licence key Pro customers get.
Getting it back: restore, undo and a move
Everything worth saving lives in /opt/rustdesk-server: compose.yml, both key files, and db_v2.sqlite3, the small SQLite database hbbs keeps (its default DB_URL). Lose the keys and every computer needs a new one typed in. The restore reads the archive's file list once and leaves your running server alone unless all four of those files are inside. The second command undoes a restore you regret. It stays inert while its first line reads GO=no; edit that to GO=yes when you really want to undo, and a full paste of the block will only restore.
# restore (sudo); set A to the archive's real name first
sudo bash -c 'A=/var/backups/rustdesk-server/rustdesk-server-DATE-TIME.tar.gz; D=/opt/rustdesk-server
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-server/$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-server; [ "$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"'
When you are happy, delete the .previous folder (and any .restored one), otherwise a later restore or undo stops at its first check.
Moving house: paste the first install block on the new server, copy your archive over, and put it in the folder the restore looks in: sudo mkdir -p /var/backups/rustdesk-server, sudo chmod 700 that folder, then sudo mv the file there. Set A to its name, run the restore, and end with the opening part of the block in "Lock the relay to your key, then save it". The key it prints must match the one already in your apps. A tip from us: give your apps a host name rather than an IP address, so moving only means pointing the name at the new server.
Updates, the licence and when to look elsewhere
To update, run a backup first. Swap the version on both image lines, with NEW standing for the newer GitHub release: sudo sed -i 's/:1.1.16$/:NEW/' /opt/rustdesk-server/compose.yml. Make sure sudo grep -c ':NEW$' /opt/rustdesk-server/compose.yml answers 2 before going on. Then fetch and restart, giving the full path each time: sudo docker compose -f /opt/rustdesk-server/compose.yml pull, then the same command ending in up -d. Next time, use the version you are on in place of 1.1.16. Paste the key check again afterwards.
About the licence, as our general reading rather than legal advice: the repository's licence (its LICENSE file) is version 3 of the GNU AGPL, AGPL-3.0. Using it as it is for your own computers asks nothing more of you. Pass copies on to anyone, changed or unchanged, and you must hand over the source code on those same terms. Change it and let people reach your changed version over a network: the AGPL then says you must offer them that version's source too. Pro comes under its own separate terms.
Be clear about one limit: the free server has no user accounts. Anyone who knows your server name and public key can connect through it. Treat the key as something you share only with your own people. If you want logins and a browser instead of an app, our guide to Apache Guacamole covers that. If you are after a Windows machine you log in to inside the data center, read Windows RDP server. You manage the server; we keep the hardware and the network running.
Questions people ask
- Is the self-hosted RustDesk server really free?
- The open-source server in the rustdesk-server repository is free to run under the AGPL-3.0 licence. It gives you the two core services, hbbs and hbbr. Features such as a web console, accounts, single sign-on and access control are in RustDesk Server Pro, a paid product; RustDesk's own site has its pricing.
- Why give the relay -k _ but not hbbs?
- Because their defaults differ. According to docs/environment-variables.md, hbbs already loads or creates the key pair without being told, and -k _ adds nothing. hbbr starts with an empty key and checks nothing, so -k _ is what makes it insist on your key.
- Can I use the web client with this setup?
- Not as written. The web client needs ports 21118 and 21119, which this guide keeps closed because RustDesk warns they trust client-supplied address headers. To use it, put a proxy in front that fills in X-Real-IP itself, and allow only that proxy through.