I wanted a way to reach my home machine from my work PC, which locks down what I can install and what I can reach on the network. The only realistic entry point was a browser-based web client, which meant I needed something I could self-host for free. RustDesk’s web client moved behind a paid plan a while back. The domestic alternatives were worse. RayLink and ToDesk both want a subscription for the web client, and the pricing is the kind of pricing where you stop reading after the second zero. The enterprise plans have everything, but you cannot buy a single seat.
So I went looking for something self-hostable that also has a real web client. HopToDesk fits. It is a RustDesk fork with a simpler server (HopSignal), a web client you can host yourself, and a normal desktop client you point at your own rendezvous. No keys, no account, no telemetry to a third party.
This post is what I ended up with after a weekend of fiddling.
What HopToDesk actually is
HopToDesk forked from RustDesk a few releases back. The desktop client still looks like a RustDesk build, the protocol is the same shape, and the wire format did not need much translation. The big difference for self-hosters is the server side. Instead of the hbbs/hbbr pair RustDesk ships, HopToDesk splits the server into two pieces: a HopSignal binary for the rendezvous (where the two peers find each other), and a stock coturn TURN server for the relay (where the actual screen-and-keyboard bytes flow when peer-to-peer fails). coturn is a separate third-party project, not part of HopToDesk; it is just the de-facto TURN server everyone uses, and HopToDesk’s api.json is shaped to talk to it directly. HopSignal itself is stateless. There is no id_ed25519.pub to copy around and no key = "..." line in the client config. The installer (hsinstall.sh) does not generate a keypair because there is nothing to pin.
That took some unlearning for me. Every older runbook I had saved assumed the RustDesk model, and I spent an embarrassing amount of time hunting for a key file that does not exist.
Installing HopSignal
The server is a single binary. On Linux:
curl -L -o /tmp/hopsignal https://github.com/hoptodesk/HopSignal/releases/latest/download/hopsignal-linux-x86_64
chmod +x /tmp/hopsignal
sudo mv /tmp/hopsignal /usr/local/bin/hopsignal
Verified flags as of 1.0.0:
--ADDR bind address (default :21116)
--LOGFILE write logs to this file
--FEDLIST federation peer list
--FEDADDR federation peer address
--FEDPWD federation peer shared secret
For a single-machine setup you only need --ADDR. I run it under a systemd unit. The signal WebSocket endpoint listens on port 9000 internally. The relay piece is a separate part of the stack: a coturn container speaking the TURN protocol, not anything inside HopSignal. HopSignal itself stays stateless: no database, no shared volume, nothing else.
Adding coturn
The TURN relay is coturn, a third-party open-source TURN server maintained independently of HopToDesk. It is in the official repositories of every major Linux distribution, so apt install coturn (or the distro equivalent) is enough. No container, no build step. Container images also exist if you want one.
Standard ports: 3478 for plain TURN over UDP and TCP, 5349 for TURN over TLS. Configure a realm, a long shared secret, and credentials. HopToDesk’s only requirement is that the realm, host, and credentials match the entries you put in api.json (next section). On my own host I apt-installed coturn, exposed 3478 and 5349 directly for the desktop client, and pointed coturn’s WebSocket TURN endpoint at 8443 locally so the web client (which only speaks HTTPS in the browser) can reach TURN through Caddy.
The web client
HopToDesk ships the web client in the same repo. Build it once and drop the bundle at /var/www/web. Caddy owns ports 80 and 443 and does all the routing, with certbot-managed Let’s Encrypt certs on 443.
Caddy, roughly:
hop.{$DOMAIN} {
root * /var/www/web
file_server
@relay path /relay*
reverse_proxy @relay localhost:8443
}
web.{$DOMAIN} {
root * /var/www/web
file_server
@relay path /relay*
reverse_proxy @relay localhost:8443
}
hop. and web. are two hostnames pointing at the same Caddy. Both serve the static web bundle; both reverse-proxy /relay* to coturn’s WebSocket TURN endpoint listening on 127.0.0.1:8443. Pick whichever hostname you want to advertise to your browser.
Cloudflare sits in front in DNS-only mode (orange cloud off) and does no TLS termination, so there is no Cloudflare intermediate in the chain. The certbot-issued Let’s Encrypt certificate is what every TLS client sees. The browser-based web client connects over 443 without trouble, because every modern browser handles the current LE chain fine.
For issuing and renewing the cert I went with whatever challenge fit my host. DNS-01 is the easy answer if Cloudflare holds your DNS and you are willing to give certbot an API token. HTTP-01 works too if Caddy listens on 80 long enough to answer the challenge when it fires.
Pointing the Mac client at it
The Mac client config lives at ~/Library/Preferences/com.hoptodesk.HopToDesk/config.toml. The TOML spec is older and most modern docs steer you toward api.json instead. A working api.json for this setup looks like this:
{
"turnservers": [
{
"protocol": "turn",
"host": "turn.your-domain.com",
"port": "3478",
"username": "you",
"password": "long-shared-secret"
},
{
"protocol": "turn-tls",
"host": "turn.your-domain.com",
"port": "5349",
"username": "you",
"password": "long-shared-secret"
}
],
"rendezvous": { "host": "hop.your-domain.com", "port": "80" },
"rendezvousssl": { "host": "hop.your-domain.com", "port": "443" },
"winversion": "1.0.0",
"macversion": "1.0.0",
"linuxversion":"1.0.0"
}
Five pieces: the TURN fallback servers (plain and TLS, both useful since strict firewalls often let only one through), the plain rendezvous entry on port 80, the TLS rendezvous entry on port 443, and per-platform client version pins.
Both rendezvous and rendezvousssl point at the same hostname. The difference is the scheme: rendezvous is plain ws:// on port 80, rendezvousssl is wss:// on port 443. The desktop client concatenates the two into one host list and dials them in order, wss first and ws second, and uses whichever connects first. On Mac the wss handshake fails (next section), so the plain-ws entry at port 80 picks up the slack and the user never notices. I keep both in the file because the wss entry is the working path on every other platform; leaving it costs nothing on Mac.
If you only filled in rendezvousssl, the Mac client would sit at a handshake that never completes. The HopToDesk Mac client is built on native-tls 0.2.11, which on macOS routes through Apple Secure Transport. The current Let’s Encrypt chain that certbot issues by default trips the handshake on a few intermediates that Apple’s stack does not yet trust the way NSS or BoringSSL does. This is independent of Cloudflare: even with certbot directly on Caddy and DNS-only Cloudflare, the Mac client still fails on wss://.
The working path is to keep the Mac client on plain HTTP and let Caddy forward:
Mac client → hop.your-domain.com:80 → Caddy:80 → HopSignal:9000 (signal WebSocket)
The browser web client keeps using 443 because the browser handles TLS fine. Two paths, one Caddy, both work.
What still annoys me
- The Mac client settings panel is fragmented. The gear icon on Home only shows connectivity toggles (Choose Network, Proxy Settings, Direct IP access, Firewall Mode, Send Server Name, LAN Discovery). To set a permanent password you go elsewhere in the menu and you only see that section once a password has been set. Took me a few minutes to find it the first time.
- There is no central web UI for managing devices. Each device shows up with its own auto-generated ID and that is it. Adding device names or grouping is on you.
- The web client does not do audio or clipboard out of the box. You get a screen, a keyboard, and a mouse. For most of what I do that is fine, but it is worth knowing before you depend on it.
Why bother
For me: free, no per-seat billing, no “upgrade to business” popups. The web client is what made this viable in the first place, since the work PC that drove the search is exactly the kind of locked-down machine that benefits most from a browser-based entry point. Self-hosting means I do not depend on HopToDesk’s hosted relay at all, which matters because the hosted web client hardcodes a relay hostname my work network blocks.
The RustDesk ecosystem is more polished and has more eyes on it, and if you only need Windows-to-Windows with no web client you should probably stay on hosted RustDesk or its self-hosted variant. HopToDesk is the right answer if the web client is the thing you actually need and you would rather run your own relay than pay for one.
Source for the installer and the server binary: github.com/hoptodesk/HopSignal. I will update this post if hsinstall.sh changes, if a later HopSignal release ships with a different cert-loading story, or if the Mac client’s TLS situation gets fixed upstream.