I Connected Hermes Desktop to a Remote Server. I Opened Zero Ports.
How I gave my headless Hermes agent a native desktop UI — running on a Hetzner VPS, accessed from my Fedora laptop, through an encrypted mesh tunnel with no firewall rules.
I’ve been running Hermes Agent on a Hetzner VPS for months. It’s fast, it’s always on, and it sits in a data center with good connectivity. But there’s one thing I kept wishing for: a GUI.
The CLI is excellent. But sometimes you want a chat interface you can glance at, drag files into, keep in a window while you’re doing other things. The Hermes Desktop app looked perfect. The problem: my agent was on a remote server, and the desktop app runs locally.
What followed was a three-hour rabbit hole that taught me more about networking than I expected. It also introduced me to Tailscale, which I’m now installing on everything.
The setup I wanted
One Hermes agent, two ways to talk to it:
- CLI on the server, for quick commands, git work, when I’m SSH’d in
- Desktop app on my Fedora laptop, for longer sessions, file drag-and-drop, that satisfying feeling of a native UI
Same config, same API keys, same session history. Not two separate agents — one agent, two surfaces.
The official docs mentioned a “Remote Gateway” feature in Desktop settings. Looked straightforward: start the dashboard on the server, point the desktop app at it. Done.
It was not done.
The auth gate nobody warned me about
First attempt: start hermes dashboard --host 0.0.0.0 --port 9119, connect Desktop, done. Simple.
hermes dashboard --host 0.0.0.0 --port 9119 --insecure --no-open --skip-build
Refusing to bind dashboard to 0.0.0.0 — the auth gate engages
on non-loopback binds, but no auth providers are registered.
Hermes 0.16.0 added an auth requirement. If you bind to anything other than localhost, you need credentials — basic auth or OAuth. The message even tells you the intended workaround:
“To keep it local, bind 127.0.0.1 and tunnel in (SSH / Tailscale).”
Fine. I set up basic auth with a username and a hashed password. Dashboard starts. Desktop connects. REST handshake works. But then it just… hangs. “Connecting…” forever.
The WebSocket catch
Here’s something the error messages don’t tell you: the Desktop app talks to the dashboard over two channels. REST for the handshake, WebSocket for the live chat stream. The REST call succeeds, which is why “Test connection” looks fine. But without --tui, the WebSocket endpoints (/api/ws, /api/events) don’t exist. The app loops on “reconnecting” indefinitely because the socket upgrade returns a silent 403.
Adding --tui to the dashboard command fixes it. This flag isn’t just for the terminal UI — it enables the embedded chat channels the Desktop app needs. Not obvious. Not documented prominently. Cost me an hour.
The part that actually blew my mind
At this point I had a dashboard on port 9119, bound to 0.0.0.0 with basic auth. It worked. But the idea of having an HTTP endpoint exposed to the internet — even with auth — felt wrong.
The docs kept saying “bind 127.0.0.1 and tunnel in.” I’d heard of Tailscale but never used it. Figured this was the moment to learn.
Tailscale is a mesh VPN built on WireGuard. That description undersells it. Here’s what it actually does:
Every device on your “tailnet” gets a fixed private IP in the 100.x.y.z range. Your laptop, your VPS, your phone — they can all talk to each other as if they’re on the same LAN. No open ports. No firewall rules. No public exposure.
The clever bit is NAT traversal. Tailscale uses STUN, UPnP, and when those fail, encrypted relay servers (called DERP) to punch a direct WireGuard tunnel between your devices. The coordination server just introduces them. Actual traffic never touches Tailscale’s infrastructure unless it absolutely has to.
Authentication is through your existing identity — Google, GitHub, Microsoft, whatever. No passwords to manage. No pre-shared keys to rotate.
I installed it on the Hetzner box (ARM64, userspace mode since I didn’t want to mess with kernel modules), installed it on my laptop, and within three minutes I could ping 100.127.208.120 from my living room.
The dashboard now binds to 0.0.0.0 safely because the only thing that can reach 100.127.208.120:9119 is a device on my tailnet. The port isn’t open to the internet. It’s not in any firewall rule. It’s just… reachable. Through an encrypted tunnel. From my laptop.
The full setup, step by step
On the Hetzner server
1. Install Tailscale
# If you can use the install script (needs sudo):
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# Or static binary (no sudo, userspace mode):
curl -fsSL "https://pkgs.tailscale.com/stable/tailscale_1.98.4_arm64.tgz" -o /tmp/ts.tgz
tar xzf /tmp/ts.tgz -C /tmp
cp /tmp/tailscale_*/tailscale /tmp/tailscale_*/tailscaled ~/.local/bin/
2. Set up basic auth for the dashboard
# Generate a password hash
python -c "
from plugins.dashboard_auth.basic import hash_password
print(hash_password('your-password-here'))
"
# Store in config
hermes config set dashboard.basic_auth.username hermes
hermes config set dashboard.basic_auth.password_hash 'scrypt$...'
3. Pin a session token so it survives restarts
echo "HERMES_DASHBOARD_SESSION_TOKEN=*** rand -base64 32)" >> ~/.hermes/.env
4. Create systemd services
Two services so everything comes back after a reboot. Tailscale first:
# ~/.config/systemd/user/tailscaled.service
[Unit]
Description=Tailscale (userspace networking)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=%h/.local/bin/tailscaled \
--statedir=%h/.local/var/tailscale \
--tun=userspace-networking \
--socket=%h/.local/var/tailscale/tailscaled.sock \
--port=0
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
Then the dashboard, which depends on Tailscale:
# ~/.config/systemd/user/hermes-dashboard.service
[Unit]
Description=Hermes Agent Dashboard
After=tailscaled.service
Requires=tailscaled.service
[Service]
Type=simple
ExecStart=%h/.hermes/hermes-agent/venv/bin/hermes dashboard \
--host 0.0.0.0 \
--port 9119 \
--insecure \
--no-open \
--tui \
--skip-build
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
Enable and start:
systemctl --user daemon-reload
systemctl --user enable tailscaled hermes-dashboard
systemctl --user start tailscaled hermes-dashboard
# Optional: start services at boot without login
sudo loginctl enable-linger $USER
On your local Linux PC
1. Install Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
Verify you can reach the server:
ping 100.127.208.120
2. Install Hermes Desktop
Download from https://hermes-agent.nousresearch.com/
3. Connect
Settings → Gateway → Remote Gateway:
- Remote URL:
http://100.127.208.120:9119 - Session Token: the value from
HERMES_DASHBOARD_SESSION_TOKEN - Username / Password: your basic auth credentials
What I’d do differently
The userspace Tailscale mode works but has limits. The throughput warnings about UDP buffer sizes don’t matter for dashboard traffic, but if you’re pushing large files through the tunnel, just install the kernel module with sudo and be done with it.
Also: the session token in .env is fine on a single-user VPS. If I ever put this on a box other people touch, I’d use something proper instead of a flat file with API keys in it.
If you work from the same local network as your server most of the time, SSH tunneling is simpler and you don’t need Tailscale:
ssh -L 9119:127.0.0.1:9119 user@your-server
Then point Desktop at http://127.0.0.1:9119. No extra software, no auth required (bind to 127.0.0.1). But the moment you leave your home network, it breaks. Tailscale works from anywhere.
The thing I actually learned
Not the commands. Not the config flags. The thing that stuck with me is how Tailscale flips the mental model.
For years, exposing a service meant: open a port, set up a reverse proxy, configure SSL, harden auth, monitor for attacks. Even for a personal dashboard nobody else would use.
Tailscale says: don’t expose it at all. Put your devices on the same virtual network and just… talk to each other. Like they’re in the same room.
That’s not a networking trick. That’s a different way of thinking about who can reach what. And it changes how you build.
This is part of my AI Agents for Daily Life series. Read the series opener and follow me on X or LinkedIn.