Everything not listed in this document should behave the same as upstream RustDesk Server. If a feature, setting, or behavior is not mentioned here, the upstream documentation is accurate and fully applicable — see the Documentation section of
instructions.mdfor links.
RustDesk Server is the self-hosted half of the RustDesk remote desktop system: hbbs, the ID/rendezvous server that devices register with and find each other through, and hbbr, the relay that carries a session when NAT traversal fails. This package runs upstream's two binaries unmodified as two daemons on one shared data directory, binds the three ports the RustDesk apps use as a single port range so TCP and UDP travel together, and surfaces the server's public key, which every client must present, through an action. There is no web UI, no account, and no password anywhere in the system.
- Image and Container Runtime
- Volume and Data Layout
- File Models
- Dependencies
- Network Access and Interfaces
- Installation and First-Run Flow
- Actions
- Tasks
- Health Checks
- Backups and Restore
- Limitations and Differences
- Quick Reference for AI Consumers
One image, built by the package FROM upstream's classic rustdesk/rustdesk-server image, runs both daemons. The upstream image is FROM scratch: the two binaries and nothing else, no shell, no init system, no CMD.
| Layer | Source | Runs |
|---|---|---|
rustdesk-server |
Dockerfile in this repo, FROM upstream |
hbbs in subcontainer hbbs, hbbr in subcontainer hbbr |
The Dockerfile adds only what StartOS needs and upstream omits: a CMD (the pack step exports images through docker create, which refuses an image without one) and an /etc/passwd and /etc/group naming root (StartOS resolves a daemon's user from the container's passwd file). Both files are checked in under docker/etc/. The package publishes x86_64 and aarch64. It has been run and tested on x86_64 only: the aarch64 build packs, but has never run on ARM hardware (see Limitations).
Each daemon runs the binary directly, not an entrypoint, with the data volume as its working directory, because both binaries read and write their working directory rather than a configured path. hbbr waits for hbbs to report healthy before starting, so the key pair hbbs writes on first start exists before hbbr loads it. There is no shell in either subcontainer, so start-cli package attach cannot run commands inside them; the package's own log carries both daemons' output.
Two volumes. Everything upstream writes lands on main; the package's own settings live on startos, which no container mounts.
| Volume | Mounted at | Into | Holds |
|---|---|---|---|
main |
/root |
hbbs, hbbr |
id_ed25519 and id_ed25519.pub (the server key pair), db_v2.sqlite3 (the peer database), blacklist.txt and blocklist.txt if the operator creates them |
startos |
not mounted | — | store.json |
/root is upstream's WORKDIR and HOME. The key pair is generated by hbbs on its first start and never rotated by the package: it is the credential every client carries, and a new pair invalidates every client. The peer database is SQLite, owned and migrated by hbbs, and holds device IDs with their public keys; it grows by one small row per registered device.
One model, and it is package state: upstream reads flags and environment variables and keeps no config file of its own.
| File | Format | Seeded | Rewritten by | Hand edit survives |
|---|---|---|---|---|
startos/store.json |
JSON | at install and every init, with schema defaults | configure |
yes, but the file is not on a mounted volume |
Fields: alwaysUseRelay (boolean, default off) and relayServers (string, default empty). Both reach hbbs on every start, alwaysUseRelay as the ALWAYS_USE_RELAY environment variable and relayServers as the -r flag when non-empty, so neither can drift. main.ts reads each field separately, so a change to one restarts the daemons exactly once.
The package fixes, and does not expose, RUST_LOG=info on both daemons and -k - on hbbr. That flag makes the relay require the same key as the ID server, which upstream leaves off by default; with a shared working directory it costs nothing, since every client already carries the key for the ID server. The upstream .env mechanism (an INI file in the working directory) is not used and nothing creates one, but a file placed there by hand would be read by both binaries.
None.
One port-range interface covers every port the clients use. A range binds TCP and UDP together, which the ID server's port requires, and forwards as one kernel NAT rule, so a client's real source address reaches hbbs and hole punching works through StartOS.
| Interface | Type | Internal ports | Daemon | Protocol | Purpose |
|---|---|---|---|---|---|
rustdesk |
api |
21115 | hbbs |
TCP | NAT type test, and the loopback runtime console |
| 21116 | hbbs |
TCP + UDP | device registration, rendezvous, hole punching | ||
| 21117 | hbbr |
TCP | relayed sessions |
Upstream's two WebSocket listeners, 21118 on hbbs and 21119 on hbbr, run inside the container but are deliberately outside the range; see Limitations.
The range asks for the same external ports; if StartOS assigns a different block, the whole range shifts by one offset and connection-details reports the shifted ports. Range interfaces are IPv4-only and carry no TLS, which matches upstream: the RustDesk protocol encrypts itself end to end with the server key and each device's own key pair. Public addresses are disabled on the range until the user enables one, as with every range interface. Enabling a StartTunnel gateway's public IP creates the tunnel's port forward for the whole block; a home router's public IP needs the router to forward 21115–21117, TCP and UDP.
A DNS name for the server needs nothing from StartOS. Nothing in the protocol carries a host name (no TLS, no SNI, no HTTP Host), so a client that resolves the name to the enabled public IP reaches the range by port alone. Verified through StartTunnel with a name whose A record points at the tunnel's public IP and that was never attached to the interface: a phone on mobile data registered and held sessions through it. Attaching the name as a public domain is not possible today (see Limitations); the only cost is that connection-details lists the IP, not the name.
Outbound, hbbs sends one UDP datagram to its own listener at start-up as a self-test and exits if that fails; nothing reaches the internet.
Nothing to set up. Install seeds store.json with its defaults; the first start generates the key pair and creates the peer database. There is no wizard, no admin account, and no credential to hand out beyond the key, which connection-details reads from the volume. Every client is configured by hand from that action's output, and upstream's client has no way to be enrolled from the server side.
Two actions, both user-facing.
Run it when setting up a client, or to recover the key. It reads id_ed25519.pub from the data volume and the range binding's address list, and returns the ID server addresses, the relay addresses, and the key. Read-only and instant; safe to repeat. It fails, with a message saying so, if the service has never started (no key yet) or if no address is enabled on the interface.
The address list applies the same rules the Interfaces page applies: loopback and the container bridge are omitted, a public IP is listed only once the user has enabled it on the interface, and a private address or domain is listed unless the user disabled it. The action reproduces those rules itself because a range binding, unlike a single-port binding, carries no pre-filtered address set.
Run it to force every session through the relay, or to point clients at a relay that lives somewhere other than the ID server's address on the standard relay port. It writes store.json; main.ts picks the change up and restarts both daemons, which drops any session in progress for a few seconds. Safe to repeat.
Forcing the relay changes how sessions travel, not what the devices learn: hbbs still sends the requesting device's address (socket_addr in the punch-hole message) to the device it wants to reach, and only marks the NAT type symmetric so both sides go to the relay. Verified end to end: with it on, a session between a phone on mobile data and a desktop behind home NAT produced the relay's New relay request and got paired lines.
None. The service is never held on a prompt and its ordinary controls are always available.
Two, one per daemon, each a port-listening check with no network I/O and the SDK's default 10-second grace period, during which a failure shows as starting. Both ports bind within a second of launch, so a check still failing after the grace period is a daemon that exited.
| Check | Probes | Failure means |
|---|---|---|
ID Server (hbbs) |
TCP 21116 bound | hbbs exited. Its log shows why; the two known causes are a failed UDP self-test (the process exits with Failed to run hbbs test) and a corrupt db_v2.sqlite3. |
Relay Server (hbbr) |
TCP 21117 bound | hbbr exited, or hbbs never became healthy so hbbr was never started (its status reads "waiting on hbbs"). |
A port that is bound is a daemon that is up; neither check proves a client can reach the server, which depends on the address and forwarding the user chose. A client that shows anything other than "Ready" while both checks are green has a network path problem, not a server problem.
Both daemons log at info into the package's log (start-cli package logs rustdesk-server), and it is the only view into the server there is. What each line means:
| Line | From | Means |
|---|---|---|
Key: … |
both | The public key, at every start. It must match the key in every client. |
ALWAYS_USE_RELAY=Y / =N, relay-servers=[…] |
hbbs |
The configure settings actually in force, at every start. |
New relay request <uuid> from <ip>, then Relayrequest <uuid> from <ip> got paired |
hbbr |
A relayed session: the controlled device and then the controller joined the relay. Relay of <ip> closed marks the session's end. |
Authentication failed from <ip> for peer <id> - invalid key |
hbbs |
A client with a wrong or missing key tried to reach device <id>. Usually a mistyped key. |
Relay authentication failed from <ip> - invalid key |
hbbr |
A connection to the relay without the key. From an address nobody recognises, this is an internet scanner being refused. |
A direct session leaves no line at all: the ID server logs neither registrations nor the hole-punch that introduces two devices. So a session that worked with no New relay request line was direct, and one with that line was relayed.
Both volumes are copied wholesale (ofVolumes). StartOS stops the service first, so the SQLite database is quiescent and a file copy is consistent. Nothing is excluded.
A restore brings back the key pair, so every client keeps working without change; that is the point of backing this service up at all, since the peer database rebuilds itself as devices reconnect. Restoring onto a different server keeps the key but not the address, so clients pointed at the old address must be updated.
The address choices on the rustdesk interface are not in the backup: which addresses are enabled is StartOS network state, not package data, and uninstalling discards it. A restore after an uninstall therefore comes back reachable on the LAN only, with the range on but no public address enabled and no tunnel or router forward, and every remote client fails to connect until the public address is enabled again. Nothing on the clients needs changing once it is. Verified by an uninstall and restore: the key came back (Private key comes from id_ed25519 at the first start, same Key: line), and the one difference in the host record was the enabled public address and its port forward.
Where this package departs from, or cannot offer, what upstream RustDesk Server does.
- Upstream's
rustdesk-server-s6image and itsRELAY,ENCRYPTED_ONLY,KEY_PUBandKEY_PRIVvariables are not used. The classic image runs instead, and the equivalents are fixed or exposed as described under File Models. - The relay validates the key (
hbbr -k -), which upstream's default does not. A client that reaches the relay without the key is refused. - Bandwidth tuning (
SINGLE_BANDWIDTH,TOTAL_BANDWIDTH,LIMIT_SPEEDand the downgrade thresholds) and the relay'sblacklist.txt/blocklist.txtare not exposed. Upstream's defaults apply; the files can be created on the volume by hand and are read at the relay's next start. - The loopback runtime consoles on 21115 and 21117 are reachable only from inside the container, which has no shell, so they are effectively unavailable.
- There is no web console, API port (21114), or user management: those belong to RustDesk Server Pro, which is a separate, licensed product and not what this package ships.
- The key pair cannot be rotated or imported through the package. A new key means uninstalling, reinstalling, and reconfiguring every client.
- The WebSocket listeners (21118 on
hbbs, 21119 onhbbr) are not exported. Upstream takes a WebSocket client's address from itsX-Real-IPorX-Forwarded-Forheader without validating it, so exposing those ports directly would let anyone evadehbbs's per-IP limits and the relay's blocklist and write false addresses into the log; upstream requires a reverse proxy that sets the header in front of them. The key check does not depend on the address and is unaffected either way. As a result, RustDesk's web client and a desktop client with itsallow-websocketoption on cannot use this server. The web client would also needwss://with a certificate the browser trusts, which the package does not provide. - Range interfaces are IPv4-only.
- A relay address set through
configureis resolved byhbbsonce, at start, and silently dropped if resolution fails;.localnames never resolve inside the container. The log linerelay-servers=[...]at each start shows what survived. An IP or a public DNS name is safe. - A public domain cannot be attached to the
rustdeskinterface. The web UI andstart-cli ... domain public addboth fail withbinding not found for internal port 21115and roll back, because the OS looks the port up among single-port bindings only, and this host has only a range. A plain DNS record works instead (see Network Access and Interfaces). - aarch64 is untested. Everything this document says was verified on x86_64; the aarch64 package is built from upstream's arm64 image with the same code and has not been run on ARM hardware. A failure specific to ARM is a gap in testing, not a documented behavior.
package_id: rustdesk-server
image: rustdesk/rustdesk-server # built FROM it by ./Dockerfile
architectures: [x86_64, aarch64] # aarch64 built, never run on hardware
subcontainers: [hbbs, hbbr]
volumes:
main: /root
startos: unmounted
file_models:
- startos/store.json
startos_managed_env_vars:
- ALWAYS_USE_RELAY
- RUST_LOG
dependencies: none
interfaces:
rustdesk: { type: api, port: 21115, port_range: 3 }
actions:
- connection-details
- configure
tasks: none
health_checks:
- hbbs
- hbbr