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