RustDesk Server with Docker: Your Own Self-Hosted Remote Desktop (Linux, Windows, Mac)
Before you start
- 15 min read
Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#
RustDesk server is a free, open-source pair of small programs (hbbs and hbbr) that lets you run remote desktop connections through your own server instead of the public RustDesk one.
Logo: the official RustDesk project (github.com/rustdesk/rustdesk, AGPL-3.0)
You keep control of who can connect and where the connection data goes. It is the "TeamViewer, but mine" setup: the RustDesk apps on your computers stay the same, only the address book behind them is yours.
At a glance#
- What you get: your own ID server (hbbs) and relay server (hbbr), running in two small containers.
- Time needed: about 20 minutes, plus a few minutes per computer you add.
- Difficulty: easy to medium. The only tricky part is opening the right ports.
- You need: Docker with the Compose plugin, an always-on machine (a cheap VPS, a mini PC or a Raspberry Pi is enough), and the RustDesk app on two computers to test.
| What | Value |
|---|---|
| Current server version | 1.1.16, released 20 Jul 2026 (GitHub releases) |
| Pinned image (official, Docker Hub) | rustdesk/rustdesk-server:1.1.16 |
| Ports | 21115, 21116 (TCP and UDP), 21117; optional 21118 and 21119 for the web client |
| Image size | about 5.9 MB compressed (amd64, Docker Hub) |
| Official docs | rustdesk.com/docs, Server OSS |
Why this version? We checked the GitHub issues and forum posts. We found no reports of failed installs or lost data for 1.1.16. Two open issues are worth knowing (see Known bugs): they affect the new 1.5.0 client in special cases. The previous release 1.1.15 had a lag report, so do not roll back to it. If you must roll back, use 1.1.14.
How it fits together#
Both computers register with hbbs, which introduces them. If they cannot connect directly, hbbr passes the screen between them.
Remember three things:
- hbbs is the phone book. It only knows who is online and where.
- hbbr is the relay. It carries the screen only when a direct link fails, so it uses the bandwidth.
- The key (
id_ed25519.pub) is what your clients check. It makes sure they talk to your server, and, once forced, that nobody else uses your relay.
Before you start#
You need Docker on the server machine and a way to open ports to it.
| System | Server host | Notes |
|---|---|---|
| Linux | Docker Engine + Compose plugin | Best choice. network_mode: host shows real client IPs |
| Windows | Docker Desktop (WSL2 backend) | Fine for testing. No host networking, so you map ports |
| Mac | Docker Desktop | Same as Windows |
Check your tools:
docker --version docker compose version
Checklist:
- [ ] Ports 21115, 21116 (TCP and UDP) and 21117 are free on the server
- [ ] You know the server's address (IP or a name) that your clients will use
- [ ] You can open those ports in the firewall, or you will use WireGuard or Tailscale
- [ ] You backed up any config you are about to replace
Hardware is tiny. The official docs say a basic cloud server is enough, even a Raspberry Pi. Relay traffic is what costs bandwidth: roughly 100 KB/s for office work and up to 3 MB/s for a full HD screen with lots of motion.
Tested on 2026-10-26: not run. Commands checked against the official RustDesk docs and the repository files.
Step 1: Start hbbs and hbbr#
You create a folder, save one Compose file and start it. Data, including your key pair, lives in ~/docker_volumes/rustdesk/data.
The -k _ on both services is the important part: it makes both load (or create) your key pair and refuse clients without it. The hbbr default is to accept anybody.
Linux#
mkdir -p ~/docker_volumes/rustdesk/data cd ~/docker_volumes/rustdesk nano docker-compose.yml
services:
hbbs:
container_name: hbbs
image: rustdesk/rustdesk-server:1.1.16
command: hbbs -k _
volumes:
- ./data:/root
network_mode: host
depends_on:
- hbbr
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
docker compose up -d
Open the ports (example for UFW; use your own firewall tool if you have another):
sudo ufw allow 21115:21117/tcp sudo ufw allow 21116/udp
Windows (PowerShell)#
Docker Desktop has no host networking, so this file maps the ports. Replace <your-server-address> with the IP or name your clients will use.
$d = "$HOME\docker_volumes\rustdesk" New-Item -ItemType Directory -Force "$d\data" | Out-Null Set-Location $d notepad docker-compose.yml
services:
hbbs:
container_name: hbbs
image: rustdesk/rustdesk-server:1.1.16
command: hbbs -r <your-server-address>:21117 -k _
ports:
- "21115:21115"
- "21116:21116"
- "21116:21116/udp"
volumes:
- ./data:/root
depends_on:
- hbbr
restart: unless-stopped
hbbr:
container_name: hbbr
image: rustdesk/rustdesk-server:1.1.16
command: hbbr -k _
ports:
- "21117:21117"
volumes:
- ./data:/root
restart: unless-stopped
docker compose up -d
Other computers on your network can only reach the server if Windows Defender Firewall allows it. In an administrator PowerShell:
New-NetFirewallRule -DisplayName "RustDesk server TCP" -Direction Inbound -Protocol TCP -LocalPort 21115-21117 -Action Allow New-NetFirewallRule -DisplayName "RustDesk server UDP" -Direction Inbound -Protocol UDP -LocalPort 21116 -Action Allow
Mac (Terminal)#
Same Compose file as Windows (ports mapped, with -r):
mkdir -p ~/docker_volumes/rustdesk/data && cd ~/docker_volumes/rustdesk nano docker-compose.yml # paste the file from the Windows tab, save docker compose up -d
macOS may ask whether Docker may accept incoming connections. Say yes.
Check it worked#
docker compose ps docker compose logs --tail 20 cat data/id_ed25519.pub
Expected: both containers are running, and id_ed25519.pub holds one line of letters and numbers. That line is your public key. On Windows use Get-Content data\id_ed25519.pub.
Potential issue: if hbbs and hbbr start in the very same second on a fresh folder, they could each write a key. If clients later say "invalid key", run docker compose down, delete the data folder, start hbbs alone first (docker compose up -d hbbs), then docker compose up -d.
Step 2: Set up the clients#
Each RustDesk app needs three things: the ID server, the key, and (optional) the relay. Install the app first: rustdesk.com or your package tool.
Linux#
flatpak install flathub com.rustdesk.RustDesk
Or download the .deb or .AppImage from the client releases.
Windows (PowerShell)#
winget install RustDesk.RustDesk
Mac (Terminal)#
brew install --cask rustdesk
Then, on every computer (these steps are the same everywhere, from the official client docs):
- Open RustDesk. Click the menu button (three dots) next to your ID, then Network.
- Click Unlock network settings (it needs administrator rights).
- ID Server: your server address, for example
<your-server-address>. A port is optional (:21116). - Relay Server: leave empty. RustDesk works it out from the ID server.
- API Server: leave empty. It is for Server Pro only.
- Key: paste the single line from
id_ed25519.pub. - Click Apply.
Do it once, then use Export Server Config on that computer and Import Server Config on the others to save typing.
Check it worked#
At the bottom of the RustDesk window the status says Ready (not "Connecting to the RustDesk network..."). Enter the ID of the other computer, connect, and accept the connection there.
Reaching it from outside#
Pick the smallest door that works. Do not open more than you need.
| Option | What is open to the internet | Good for |
|---|---|---|
| WireGuard or Tailscale | Nothing (the VPN port only) | Best. Use the server's VPN address as the ID server |
| Forward only the needed ports | 21115, 21116 TCP+UDP, 21117 | A public server, with the key forced |
| Everything open (21114 to 21119) | Too much | Never. 21114 is Pro only, 21118 and 21119 are for the web client |
With a VPN, every client must be on the VPN too. That is the cost, and the safety.
If you forward ports, forward exactly: TCP 21115, 21116 and 21117, plus UDP 21116. Keep 21118 and 21119 closed unless you read the web client note below.
Put it behind HTTPS (with or without Traefik)#
Short and honest: the RustDesk ID and relay traffic is not HTTP, so Traefik and Caddy cannot sit in front of it. This section only applies to the optional web client.
hbbs and hbbr speak RustDesk's own protocol on ports 21115 to 21117 (TCP and UDP). It is already encrypted with your server key, and a web reverse proxy cannot carry it. So for the normal setup, keep it safe like this:
- Force the key (
-k _, done in Step 1), so only clients with your key can use the server. - Open only the needed ports, or use WireGuard or Tailscale.
- Do not publish 21114, 21118 or 21119 unless you use the web client.
The only part that is web traffic is the WebSocket pair for the web client: hbbs on 21118 and hbbr on 21119. Official docs warning: hbbs and hbbr trust the X-Real-IP and X-Forwarded-For headers on those ports without checking them. Anyone who reaches 21118 or 21119 directly can fake their IP address. So expose them only through a reverse proxy that sets X-Real-IP itself, and block 21118 and 21119 in the firewall for everyone else.
The official Server Pro FAQ proxies /ws/id to port 21118 and /ws/relay to port 21119 on HTTPS. We use the same paths. We did not test the web client ourselves (VERIFY with your client).
With Traefik#
Use this only if you want the web client and already run Traefik for a domain. Traefik runs in its own container, so it cannot use the host-network file. Use the mapped-ports file from the Windows tab, add the proxy network and these labels:
docker network create proxy
services:
hbbs:
container_name: hbbs
image: rustdesk/rustdesk-server:1.1.16
command: hbbs -r <your-domain>:21117 -k _
ports:
- "21115:21115"
- "21116:21116"
- "21116:21116/udp"
volumes:
- ./data:/root
networks:
- proxy
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.http.routers.rd-id.rule=Host(`<your-domain>`) && PathPrefix(`/ws/id`)
- traefik.http.routers.rd-id.entrypoints=https
- traefik.http.routers.rd-id.tls=true
- traefik.http.services.rd-id.loadbalancer.server.port=21118
restart: unless-stopped
hbbr:
container_name: hbbr
image: rustdesk/rustdesk-server:1.1.16
command: hbbr -k _
ports:
- "21117:21117"
volumes:
- ./data:/root
networks:
- proxy
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.http.routers.rd-relay.rule=Host(`<your-domain>`) && PathPrefix(`/ws/relay`)
- traefik.http.routers.rd-relay.entrypoints=https
- traefik.http.routers.rd-relay.tls=true
- traefik.http.services.rd-relay.loadbalancer.server.port=21119
restart: unless-stopped
networks:
proxy:
external: true
The labels are our example, not from the official docs (VERIFY against your Traefik setup). Ports 21118 and 21119 are deliberately not published; Traefik reaches them over the proxy network.
Without Traefik: Caddy#
Choose Caddy if you have no Traefik and want the web client over HTTPS on one small server.
| Case | Result | You do |
|---|---|---|
| Public domain | Automatic Let's Encrypt | DNS A record to the server, ports 80 and 443 open |
| Home only | tls internal (local certificate) | Trust Caddy's root certificate on each device |
Caddyfile for a public domain (Caddy runs on the same machine as the Linux host-network file, so it can use localhost):
rustdesk.example.com {
@id path /ws/id
@relay path /ws/relay
reverse_proxy @id localhost:21118 {
header_up X-Real-IP {remote_host}
}
reverse_proxy @relay localhost:21119 {
header_up X-Real-IP {remote_host}
}
}
Caddyfile for home only:
rustdesk.home.arpa {
tls internal
@id path /ws/id
@relay path /ws/relay
reverse_proxy @id localhost:21118 {
header_up X-Real-IP {remote_host}
}
reverse_proxy @relay localhost:21119 {
header_up X-Real-IP {remote_host}
}
}
Trust the root certificate (pki/authorities/local/root.crt in Caddy's data directory):
# Linux: on the Caddy machine sudo caddy trust # other Linux devices, with root.crt copied over sudo cp root.crt /usr/local/share/ca-certificates/caddy-root.crt && sudo update-ca-certificates # Windows (PowerShell as administrator) certutil -addstore -f ROOT root.crt # Mac sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain root.crt
Run Caddy (Linux: install from the official apt repository on caddyserver.com/docs/install first):
# Linux sudo systemctl reload caddy # Windows (PowerShell) winget install CaddyServer.Caddy caddy run # Mac brew install caddy && caddy run
Or use the Docker image. On a host-network Linux setup the container must share the host network too, so localhost means the host. Save this in ~/docker_volumes/caddy next to the Caddyfile above:
services:
caddy:
image: caddy:2.11.7
container_name: caddy
restart: unless-stopped
network_mode: host
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy_data:/data
- ./caddy_config:/config
Caddy passes WebSockets by default. Block 21118 and 21119 for outside visitors (for example sudo ufw deny 21118/tcp and sudo ufw deny 21119/tcp): Caddy reaches them over the local machine.
Check it worked: curl -I https://rustdesk.example.com/ws/id answers with an HTTP status from hbbs (a plain request is not a WebSocket, so an error code such as 400 still proves Caddy reached it). The real test is the web client. The desktop apps never use this address.
Security and testing#
Keep the key forced, keep the ports few, keep the private key private.
- Never share
id_ed25519(the private key). Onlyid_ed25519.pubgoes to clients. Lock the folder:
chmod 700 ~/docker_volumes/rustdesk/data chmod 600 ~/docker_volumes/rustdesk/data/id_ed25519
- Test that the key is forced. On a computer with an empty Key field and your ID server set, the status should not become Ready (an invalid-key error or "Connecting..." forever). With the right key it becomes Ready.
- Set a permanent password on each remote computer in the RustDesk app and use "Unattended access" only where you need it. The server does not replace access control on the client.
- Pin the image tag. The official docs use
latest; we pin so updates are your decision. - Fail2ban or CrowdSec: release 1.1.15 added log lines for failed authentication that these tools can read. See
docs/environment-variables.mdin the repository for the options. - Server Pro or open source? The open-source server has no accounts, web console, audit log or custom clients. If you want those, look at Server Pro. This guide covers only the free server.
Tests:
docker compose ps docker logs hbbs --tail 50 docker logs hbbr --tail 50
Expected: both running, no repeating errors.
Troubleshooting#
| Problem | Solution |
|---|---|
| Client shows "Connecting to the RustDesk network..." forever | Check the ID server address, the key, and that TCP 21115/21116 and UDP 21116 are open |
| Key error ("invalid key" style message) | The key in the app must equal id_ed25519.pub. Re-copy it. Do not delete the key files |
| Connects even with no key | -k _ is missing on hbbr (or hbbs). Add it and run docker compose up -d |
| New key after every restart | The ./data:/root volume is not mounted. Check docker compose config |
| "Failed to connect to relay server" | TCP 21117 is blocked, or the relay address is wrong. Open 21117 or set the Relay Server field |
| Everything goes through the relay, nothing direct | UDP 21116 is closed. Open it |
Client IP in logs shows 172.17.0.1 | Bridge networking hides real IPs. Use host networking on Linux |
| Windows or Mac: peers cannot reach the server | Set -r <your-server-address>:21117 and allow the ports in the local firewall |
| Two computers in the same home cannot connect through the public address | Router "NAT loopback" problem. Use the server's local address on the local computers |
Known bugs#
Open on GitHub at the time of writing (read 2026-10-26):
- #733: one report of hbbs 1.1.16 not answering a RustDesk 1.5.0 client behind carrier-grade NAT, so that client never appears online. Other clients are fine. If you hit it, try the previous client version.
- #734: a proposed change that adds the 1.5.0 client's new key exchange, WebRTC and UDP hole punching to hbbs. Until it ships, 1.5.0 clients fall back to the older paths.
Diagnostics:
docker logs hbbs --tail 100 docker logs hbbr --tail 100 docker compose ps
Updates and backups#
Update by changing the tag, back up by copying the data folder. The data folder holds your key pair and the SQLite database (db_v2.sqlite3). Lose the private key and every client needs a new key.
Update (Linux/Mac; on Windows use the same docker compose lines in PowerShell):
cd ~/docker_volumes/rustdesk # read the release notes, edit the image tags in both services, then: docker compose pull && docker compose up -d
Backup script (Linux/Mac). It stops the stack briefly so the database is consistent:
mkdir -p ~/docker_volumes/rustdesk/backups cd ~/docker_volumes/rustdesk docker compose down tar czf backups/rustdesk-$(date +%F).tgz data docker compose up -d
Windows (PowerShell):
Set-Location "$HOME\docker_volumes\rustdesk" docker compose down New-Item -ItemType Directory -Force backups | Out-Null Compress-Archive -Path data -DestinationPath "backups\rustdesk-$(Get-Date -Format yyyy-MM-dd).zip" docker compose up -d
Schedule: Linux/Mac cron at 03:00 (0 3 * * * <path-to-script>). Windows: Task Scheduler running the PowerShell lines.
Restore test#
Do this once, before you need it. It starts a copy of the server on other ports and checks the key is the same:
mkdir -p /tmp/rd-restore && tar xzf ~/docker_volumes/rustdesk/backups/rustdesk-*.tgz -C /tmp/rd-restore diff /tmp/rd-restore/data/id_ed25519.pub ~/docker_volumes/rustdesk/data/id_ed25519.pub && echo "same key" docker run --rm -d --name rd-restore -v /tmp/rd-restore/data:/root -p 22115:21115 -p 22116:21116 -p 22116:21116/udp rustdesk/rustdesk-server:1.1.16 hbbs -k _ docker logs rd-restore --tail 20
Expected: same key and no errors in the log. Then clean up:
docker stop rd-restore && rm -rf /tmp/rd-restore
Performance and resources#
The server is very light. The image is about 5.9 MB compressed. We did not measure RAM or CPU ourselves and the docs give no official numbers, so measure on your machine:
docker stats --no-stream hbbs hbbr
- The real cost is bandwidth through hbbr: roughly 100 KB/s for office work, up to 3 MB/s for HD video-like screens (official docs).
- Direct connections (when UDP 21116 is open and the NAT allows it) cost the server almost nothing.
- Setting
ALWAYS_USE_RELAY=Yon hbbs forces every session through the relay. Useful for testing, expensive in bandwidth. - One user reported high CPU on a Docker server that also had the RustDesk app installed; removing the app from the server fixed it.
FAQ#
Is it really free? Yes. The open-source server is free (AGPL-3.0). Server Pro is a separate, paid product with extra features.
Do I need to set the relay server on the clients? Usually not. RustDesk works it out from the ID server. Set it only if the relay is on another address or port.
How do I stop strangers using my relay? Start both services with -k _, as in this guide, and test with a client that has no key.
Do I need host networking? On Linux it is recommended because logs show real client IPs. On Windows and Mac it does not exist, so map the ports.
Can I use the web client? Yes, but only through a proxy and with ports 21118 and 21119 blocked for everyone else. See the HTTPS section.
Can I use the latest tag? It currently equals 1.1.16. We pin the exact version so a later release cannot surprise you.
Sources#
- Caddy docs: automatic HTTPS, tls internal, reverse_proxy, install (Caddy 2.11.7, read 2026-10-26)
- RustDesk docs: Server OSS installation, Docker, Client configuration, Server Pro FAQ (WebSocket paths) (read 2026-10-26)
- rustdesk-server README and environment variables, releases (1.1.16, 20 Jul 2026) and issues
- Docker Hub: rustdesk/rustdesk-server
Checked against the official docs on 2026-10-26. Not run by DIY Lab Hub. Items marked VERIFY are not confirmed in the official docs.