Deploy to a VPS
A Team-Hub is just askTheodor running headless on a server you control. You start the same app
with the --runner flag, pair it from your Command Center, and it becomes a tireless 24/7 fleet member —
no screen, no mouse, no operator required. This page is the practical recipe: get it running, connect it,
keep it running, and license it.
1. Get the app onto the server
The runner is the same binary as the desktop app — there’s no separate download; you just launch it
with --runner. Build it on a Linux box matching your server’s architecture (or in CI) and copy it over:
# copy a release build to the VPSscp ./asktheodor user@vps:/usr/local/bin/asktheodor2. First run + choosing where data lives
SSH in and start it once by hand to create the workspace:
ssh user@vpsasktheodor --runner --data-dir /srv/asktheodor--data-dir <path>puts the database and workspace files wherever you choose — a mounted volume, a backed-up path, etc. Recommended on a server, so the location is explicit.- Omit it and the runner uses the same per-OS app-data folder the desktop uses — handy only if you’d ever run the GUI on the same box and want them to share one workspace.
The first run creates the database, applies all migrations, and downloads the built-in embedder model. Later runs start instantly. Press Ctrl-C to stop this manual run before installing the service below.
3. Pair it into your Fleet
A runner is just another peer. From your Command Center:
- Open Fleet (or Settings → Peers).
- Add the VPS by its host/tunnel URL and complete the pairing handshake (same as pairing two desktops).
- Give it a scope: sync (mirror only), delegate (also run work you hand it), or full.
Once paired, the hub appears live in your Fleet screen, its workspace mirrors via sync, and it accepts delegated work and key pushes.
The mechanics of what sync and delegation actually do are in Fleet & sync.
4. Keep it running (systemd)
So the hub survives reboots and restarts cleanly, install it as a service. Save as
/etc/systemd/system/asktheodor-runner.service (adjust the user and paths):
[Unit]Description=askTheodor headless runnerAfter=network-online.targetWants=network-online.target
[Service]Type=simpleUser=asktheodorEnvironment=RUST_LOG=info# To move the Fleet port, add e.g.:# Environment=ASKTHEODOR_FLEET_ADDR=127.0.0.1:7118ExecStart=/usr/local/bin/asktheodor --runner --data-dir /srv/asktheodorKillSignal=SIGTERMTimeoutStopSec=30Restart=on-failureRestartSec=5# Hardening (optional but recommended):NoNewPrivileges=trueProtectSystem=strictReadWritePaths=/srv/asktheodor
[Install]WantedBy=multi-user.targetThen enable and watch it:
sudo systemctl daemon-reloadsudo systemctl enable --now asktheodor-runnersudo journalctl -u asktheodor-runner -f # follow the logssudo systemctl stop asktheodor-runner # clean shutdown (SIGTERM)It now auto-starts on boot, restarts on failure, and logs to the journal.
5. Updating the hub
The in-app auto-updater targets the desktop GUI. On a server you update by swapping the binary and restarting the service:
sudo systemctl stop asktheodor-runnersudo cp ./asktheodor /usr/local/bin/asktheodorsudo systemctl start asktheodor-runnerUpdates are never gated behind a license — you can always update.
6. Licensing the hub (the “remote” entitlement)
Running askTheodor headless on a server is the paid “remote” instance type. The entitlement rides your
account’s license token (a remote flag), much like the kiosk add-on. On start, the runner reports its
status in the log — licensed for remote, running under trial with N days left, or running anyway
pre-production.
There are two ways to get the license onto a hub, and both are shipped:
-
Push it from a Command Center. The Fleet/Peers panel activates your key against the hub’s device id and delivers the minted token over the encrypted key-push channel. The hub verifies it offline and flips from trial to licensed — you never SSH in.
-
Over SSH. Start the runner once with the token as a flag; it’s stored in the workspace and survives restarts, so you only do this once:
Terminal window asktheodor --runner --data-dir /srv/asktheodor --set-license <token>
In practice
Sizing: the runner is the desktop binary, so budget for a desktop-class process rather than a small web service. The first run downloads the embedder model and applies migrations — allow disk for the workspace plus the model, and expect that run to take noticeably longer than later ones. If your workers use cloud providers, the heavy thinking happens at the provider and the VPS mostly coordinates; if you plan to run local models on the hub as well, size for the model, not for askTheodor.
Two failure modes that account for most bad first deploys:
- Missing graphics libraries. The binary links against the desktop’s GTK/WebKit libraries even though it never opens a window. Without them it won’t start at all, and the loader error looks unrelated to askTheodor. Install them before the first run.
- No inbound path. Outbound sync and reporting work from any normal connection, so a hub can look perfectly healthy in your Fleet screen while delegation and key pushes silently fail. Those need the reverse proxy or tunnel. If “Mirror now” works but “Delegate” doesn’t, this is why.
Always pass --data-dir on a server. It makes the workspace location explicit, which matters when
you come to back it up, move it to a bigger volume, or work out what to include in the server’s snapshot
schedule. The per-OS default is fine on a desktop and a nuisance on a VPS.
Watch journalctl -u asktheodor-runner -f on the first real run, not just the manual one. The runner
logs the Fleet address it bound to, its licensing state, and a loud error if the port was already taken —
which is the single fastest way to diagnose delegation that never arrives.
Updating is a binary swap, deliberately. The in-app auto-updater targets the GUI; on a server you stop the service, copy the new binary in, and start it. Worth doing at a quiet moment — a restart interrupts Plans in flight, and the hub is usually the machine with work in flight.
Take the hardening lines seriously. NoNewPrivileges, ProtectSystem=strict and a narrow
ReadWritePaths cost nothing and matter here: this is a machine on the public internet running agent
work unattended. Combined with the no-send guarantee, that’s what keeps a
24/7 hub a reasonable thing to own.
🎓 Learn it hands-on: Fleet: a Team-Hub on a VPS
Terms in this page
- Team-Hub — askTheodor running headless on a server as a 24/7 fleet member; the paid “remote” add-on.
- Runner /
--runner— headless mode: the app with no window, requiring no display server. - Headless — running without a graphical interface, suitable for a server reached over SSH.
- VPS — a Virtual Private Server: a rented Linux machine in the cloud, a common home for a hub.
--data-dir— the flag that sets where the hub keeps its database and workspace files.- Embedder model — the small local model the hub downloads on first run to power semantic search and RAG.
- Pairing / scope — linking the hub to your Command Center and granting it a capability level: sync, delegate, or full.
- Fleet server — the runner’s inbound endpoint (default
127.0.0.1:7117) that receives delegated work and key pushes. ASKTHEODOR_FLEET_ADDR— the environment variable that changes the address/port the Fleet server binds to.- Reverse proxy / tunnel — a front-end (e.g.
cloudflared,ngrok, or your own proxy) that gives the server a reachable inbound URL. - systemd unit — the Linux service definition that keeps the runner alive across reboots and restarts.
- journald /
journalctl— the Linux logging system where the service’s logs land. - “Remote” entitlement — the licensing flag that authorizes running a hub; reported by the runner on start.
- License push — sending the license token from a Command Center to a hub over the encrypted key-push channel.