Skip to content

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:

Terminal window
# copy a release build to the VPS
scp ./asktheodor user@vps:/usr/local/bin/asktheodor

2. First run + choosing where data lives

SSH in and start it once by hand to create the workspace:

Terminal window
ssh user@vps
asktheodor --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:

  1. Open Fleet (or Settings → Peers).
  2. Add the VPS by its host/tunnel URL and complete the pairing handshake (same as pairing two desktops).
  3. 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 runner
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=asktheodor
Environment=RUST_LOG=info
# To move the Fleet port, add e.g.:
# Environment=ASKTHEODOR_FLEET_ADDR=127.0.0.1:7118
ExecStart=/usr/local/bin/asktheodor --runner --data-dir /srv/asktheodor
KillSignal=SIGTERM
TimeoutStopSec=30
Restart=on-failure
RestartSec=5
# Hardening (optional but recommended):
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/srv/asktheodor
[Install]
WantedBy=multi-user.target

Then enable and watch it:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now asktheodor-runner
sudo journalctl -u asktheodor-runner -f # follow the logs
sudo 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:

Terminal window
sudo systemctl stop asktheodor-runner
sudo cp ./asktheodor /usr/local/bin/asktheodor
sudo systemctl start asktheodor-runner

Updates 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:

  1. 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.

  2. 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.