Self-Hosting
The Quickstart covers the install in three lines and gets you to your first sandbox. This page is the longer version — what’s actually happening on your server, how to add teammates, and the operational details I’ve picked up running bhatti on real hardware.
Requirements
Section titled “Requirements”- Linux with KVM (
/dev/kvmmust exist and be readable) - Root access (Firecracker requires it; the daemon runs as root by default and Firecracker drops to an unprivileged UID per VM via the jailer — see Architecture → jailer mode)
- 1 GB+ RAM, NVMe recommended for snapshot performance
I run on:
- Two Raspberry Pi 5s with NVMe HATs (home, integration tests)
- A Hetzner AX102 (Ryzen 9, NVMe — my main box)
Both are linked from the homepage benchmark. Anything in that ballpark or stronger works.
Filesystem (recommended: btrfs)
Section titled “Filesystem (recommended: btrfs)”bhatti’s bhatti create, snapshot create, and snapshot resume paths
all lean on cp --reflink=auto --sparse=always for block-device
copies
(pkg/engine/firecracker/fc.go::copyBlock).
On btrfs or xfs this is a metadata-only CoW clone — instant,
near-zero disk. On ext4 it falls back to a full sparse copy. Same
correctness; very different time and disk cost. The performance
numbers on the homepage are measured on btrfs and don’t apply on
ext4. See Storage for the
cost-by-filesystem breakdown.
The minimum viable setup is a btrfs loopback file. Do this before
running the install script, so /var/lib/bhatti is already btrfs when
the daemon first writes to it:
# Pre-install setup. 500 GiB is sized for tens to low-hundreds of# sandboxes; adjust to your disk. fallocate is instant on btrfs/xfs# hosts (just reserves space, no zero-write).sudo fallocate -l 500G /var/lib/bhatti-btrfs.imgsudo mkfs.btrfs -f /var/lib/bhatti-btrfs.imgsudo mkdir -p /var/lib/bhattisudo mount -o loop,noatime,compress=zstd:1 \ /var/lib/bhatti-btrfs.img /var/lib/bhattiecho '/var/lib/bhatti-btrfs.img /var/lib/bhatti btrfs loop,noatime,compress=zstd:1 0 0' \ | sudo tee -a /etc/fstabLoopback because it works on any host — you don’t need a spare
partition or a repartition. Native btrfs on /var/lib/bhatti’s
underlying device is preferred on multi-disk hosts; the install
works the same way.
If bhatti is already running on ext4 and you want to switch, you’ll need to stop the daemon and rsync the data dir across. The archived migration recipe in the monorepo walks through that path; expect ~10–15 minutes of downtime.
xfs also supports reflink and is a fine alternative if btrfs
stability is a concern. xfs lacks transparent compression — you’ll
get reflink savings but not the additional ~2× from zstd on
mem.snap files.
If you must run on ext4, the system works correctly — only performance and disk usage are worse. See Storage → What changes on ext4 for the concrete cost.
Install
Section titled “Install”curl -fsSL bhatti.sh/install | sudo bashThe script will:
- Detect your architecture (
arm64oramd64) and the OS. - Prompt for a rootfs tier (or pass
--tier <name>to skip the prompt; see below). - Download the components (
bhatti,lohar, Firecracker + jailer, kernel, rootfs). Components that haven’t changed since a previous install are skipped automatically. - Install a systemd unit at
/etc/systemd/system/bhatti.serviceand start the daemon. - Create an
adminuser with high resource caps, save its API key to/root/.bhatti/config.yaml, and also save it to your user’s~/.bhatti/config.yamlif you ran withsudo(so the CLI on the same box works without an explicitbhatti setup). - Print the admin API key once — save it. Anyone with this key can do anything on this server.
Rootfs tiers
Section titled “Rootfs tiers”| Tier | What’s in it | Size |
|---|---|---|
minimal | Bare Ubuntu 24.04 | ~200 MB |
browser | + Chromium, Playwright, Node 22 | ~600 MB |
docker | + Docker Engine | ~550 MB |
computer | + Full desktop: XFCE, KasmVNC, Chromium | ~1.5 GB |
You pick one tier as the default for bhatti create (used when
you don’t pass --image). Other tiers are still installable later
and can be selected per sandbox with --image <tier>.
Non-interactive install
Section titled “Non-interactive install”For Ansible / packer / CI, skip the tier prompt with flags. The
bash -s -- ... syntax passes flags through curl | bash:
# Specific tier as the defaultcurl -fsSL bhatti.sh/install | sudo bash -s -- --tier browser
# Default minimal, plus install every other tiercurl -fsSL bhatti.sh/install | sudo bash -s -- --tiers all
# Default docker, plus also install browsercurl -fsSL bhatti.sh/install | sudo bash -s -- --tier docker --tiers browser--tier (singular) sets the default tier — what bhatti create
uses when no --image is passed. --tiers (plural, comma-separated
list or all) installs additional tiers alongside the default.
Verifying the install
Section titled “Verifying the install”From the same box where you ran the install, no setup needed:
bhatti create --name testbhatti exec test -- uname -abhatti destroy testIf the create succeeds, the daemon is healthy, the engine has KVM access, the agent is reachable, and the CLI’s config was written correctly.
Adding teammates
Section titled “Adding teammates”The remote-CLI flow that used to be Quickstart lives here.
On the server:
sudo bhatti user create --name alice --max-sandboxes 5 --max-cpus 4 --max-memory 4096# → API key: bht_... (shown once, save it)Send Alice the key over a secure channel (1Password, Signal, encrypted email — not Slack, not GitHub Issues). On her machine:
# CLI only, no sudocurl -fsSL bhatti.sh/install | bash
# Wire it up. Either interactive or via flags.bhatti setup --url https://your-server:8080 --token bht_...# or:bhatti setupNow Alice can bhatti create, bhatti exec, etc. — but only against
sandboxes she creates. Each user is isolated at the API layer
(scoped queries) and at L2 (per-user bridge — see
Networking).
For driving bhatti from agents, CI, or provisioning scripts, use the
non-interactive form: bhatti setup --url ... --token .... The auth
test always runs and the command exits non-zero on failure, so your
provisioner picks up bad credentials immediately.
Custom domain (optional but recommended)
Section titled “Custom domain (optional but recommended)”By default bhatti publish generates URLs at
<alias>.<your-server-ip>.nip.io or similar — fine for testing,
ugly for sharing. To get URLs like my-app.yourdomain.com with TLS,
see Custom domain.
Backups
Section titled “Backups”What to back up:
/var/lib/bhatti/state.db— every sandbox/user/secret/template/volume row. The daemon writes WAL, so a hot snapshot of juststate.dbmay be missing recent commits; back upstate.db+state.db-wal+state.db-shmtogether, or runsqlite3 .backupfor a clean copy./var/lib/bhatti/age.key— the encryption key for every secret. Lose it and every encrypted secret on the server is unrecoverable. Treat it the same way you’d treat a TLS private key./var/lib/bhatti/volumes/— standalone volumes (the ones created withbhatti volume create). These are real ext4 images.- Per-sandbox files under
/var/lib/bhatti/sandboxes/are usually not worth backing up — sandboxes are reproducible from images and init scripts. The exception is sandboxes whose state you actually care about; for those, take a named snapshot (bhatti snapshot create <name> --label keep) which writes a self-contained copy under/var/lib/bhatti/snapshots/.
For S3-compatible volume backups, see Volumes → backups.
Updating
Section titled “Updating”sudo bhatti update # bhatti + lohar + kernel + jailersudo bhatti update --tiers all # also pull additional rootfs tiersOr just re-run the install:
curl -fsSL bhatti.sh/install | sudo bashThe install script is idempotent — it skips components that haven’t changed and updates the rest.
Uninstalling
Section titled “Uninstalling”# Remove binaries + service, keep /var/lib/bhatti so you can reinstallcurl -fsSL bhatti.sh/uninstall | sudo bash
# Remove everything, including all sandbox state, volumes, and age.keycurl -fsSL bhatti.sh/uninstall | sudo bash -s -- --purge--purge is destructive and unreversible — it deletes the encryption
key, every secret, every sandbox, every volume.
Where each thing lives
Section titled “Where each thing lives”The full layout is on the Architecture page. Short version:
/var/lib/bhatti/state.db— SQLite, the source of truth/var/lib/bhatti/age.key— secret encryption key (back this up)/var/lib/bhatti/images/— read-only base rootfs templates + kernel/var/lib/bhatti/sandboxes/<id>/— per-sandbox: rootfs, config drive, snapshots/var/lib/bhatti/volumes/— standalone volumes/var/lib/bhatti/snapshots/— named snapshots frombhatti snapshot create/var/lib/bhatti/jails/— jailer chroots (one per running sandbox)/etc/bhatti/config.yaml— daemon config (engine, listen, domain)
Next steps
Section titled “Next steps”- Users & Auth — API key rotation, per-user limits, deleting users
- Custom domain — TLS for
bhatti publish, ACME, wildcard DNS - Concepts — mental model for sandboxes and thermal states
- Architecture — what each process does and how state flows