Docs

Deploy your own server

Kai Core doesn't need to live on a computer at your desk. A small cloud VPS keeps it online 24/7 without your laptop staying awake. This guide is provider-agnostic — it works on DigitalOcean, Hetzner, Linode, AWS Lightsail, or anywhere you can get a fresh Ubuntu box.

For comfortable-with-a-terminal users

When to prefer this

VPS vs. a machine at home

Prefer a VPS when...

  • You don't have a machine that stays powered on 24/7.
  • You want a stable public hostname instead of relying on your home network.
  • You're fine paying a few dollars a month for a small cloud box.

Prefer /get's home setup when...

  • You already have a Mac, Linux box, or Windows machine that's always on.
  • You'd rather keep everything on hardware you physically control.
  • You want the simplest path — see /get.

Setup

Ubuntu server: bootstrap and harden

The kai-core repo ships scripts under deploy/ that were built for Brandon's own multi-tenant pilot host, so treat them as a reference to crib from rather than a single button to press — but the Docker, hardening, and sandboxing pieces apply directly to a single-user deploy too.

1. Get Docker running

deploy/bootstrap-ubuntu.shinstalls Docker Engine from the official repo on a fresh Ubuntu Server box, plus a default firewall posture. It also sets up pieces specific to Brandon's tenant-hosting pilot (a control-plane service, Cloudflare Tunnel ingress) — read it before running the whole thing, and feel free to run just the Docker install section by hand if that's all you need.

run as root, from the kai-core repo
sudo -E ./deploy/bootstrap-ubuntu.sh

2. Harden the host

deploy/harden-host.sh locks down SSH (no password auth, no root login), enforces a default-deny inbound firewall with nftables, and turns on unattended security upgrades. Run it after Tailscale is up so its rules can see the tailnet interface.

sudo tailscale up --ssh --hostname kai-host
sudo ./deploy/harden-host.sh

3. Optional: gVisor sandboxing

deploy/install-gvisor.sh registers gVisor (runsc) as an available Docker runtime without touching your existing daemon config. It doesn't change Docker's default runtime — you opt a container into it explicitly. This is the real isolation boundary worth having, since the agent can run shell commands on the host it's deployed to.

sudo ./deploy/install-gvisor.sh

# then run the gateway container with:
docker run --runtime runsc ... hermes-agent

4. Start the gateway with Docker Compose

The repo's docker-compose.yml is built for exactly this — a single-user deploy. Set your provider key in ~/.hermes/.env, generate a strong API_SERVER_KEY (openssl rand -hex 32 — anything under 16 characters is refused at startup), uncomment API_SERVER_HOST / API_SERVER_KEY in the compose file, and bring it up.

HERMES_UID=$(id -u) HERMES_GID=$(id -g) docker compose up -d

restart: unless-stoppedin the compose file keeps the gateway running across reboots — the repo's deploy/systemd/units are specific to Brandon's multi-tenant pilot, so Compose's own restart policy is the simplest way to keep a single-user gateway alive. If you'd rather run the gateway as a bare process, wrap python -m hermes_cli.main gateway run in your own minimal systemd unit.

Exposure

Tailscale vs. Cloudflare Tunnel

Recommended

Tailscale

Private mesh network — only your own devices can reach the server, over an encrypted tunnel, with no public DNS record at all.

tailscale serve https / http://127.0.0.1:8642
Alternative

Cloudflare Tunnel

Gives you a real public HTTPS hostname without opening any inbound ports — useful if you want to reach the server from a device that isn't on your tailnet. Get a tunnel token from the Cloudflare Zero Trust dashboard, then:

curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo apt-get install -y ./cloudflared.deb
sudo cloudflared service install <your-tunnel-token>

Checklist

Hardening checklist

  • Strong API_SERVER_KEY — generate with openssl rand -hex 32; the server refuses anything under 16 characters.
  • Dashboard stays localhost-only — the dashboard service binds to 127.0.0.1 by default and stores API keys; if you need remote access, tunnel it (ssh -L) rather than exposing it directly.
  • Default-deny inbound firewall— only allow the ports you actually need exposed (or none, if you're routing everything through Tailscale/Cloudflare Tunnel).
  • Keep the host updated — unattended security upgrades, and re-run deploy/harden-host.sh after major changes.

What NOT to do

  • Don't pair or serve over plain http:// on the open internet — the pairing token is a bearer credential.
  • Don't expose the dashboard beyond localhost/a tunnel — it stores API keys.
  • Don't run this as multi-user infrastructure from one instance — it's single-user software today. One server, one person.