Skip to content

Configuration

bhatti has a single YAML config file. The same file format is used by the daemon and the CLI client; the daemon ignores the client-only fields, the client ignores the daemon-only ones.

The config is loaded from multiple paths and merged. Higher rows override lower rows:

SourceLoaded?Used for
$BHATTI_CONFIG (path)If set, only this is loaded — no merging.Override everything; useful for tests.
/etc/bhatti/config.yamlFirst match wins.Server settings — engine paths, listen address, data dir.
~/.bhatti/config.yamlAlways — fills in only api_url and auth_token if still empty.CLI client credentials.

This means a developer machine that’s also running a server can have the server config in /etc/bhatti/config.yaml (with no client credentials) and the CLI’s API key in ~/.bhatti/config.yaml (without disturbing the server config). Both files are read, neither overwrites the other.

The deprecated location /var/lib/bhatti/config.yaml is still honoured as a fallback; the daemon prints a stderr warning when it loads from there. Migrate to /etc/bhatti/config.yaml.

VariableUsed byDescription
BHATTI_CONFIGbothOverride the config file path. When set, layered loading is bypassed.
BHATTI_LOG_LEVELserverdebug, info (default), warn, error.
BHATTI_URLCLIAPI endpoint. Falls back when --url and api_url: aren’t set.
BHATTI_TOKENCLIAPI key. Same fallback semantics.
BHATTI_FORCE_STREAMCLIForce NDJSON streaming output even when stdout isn’t a TTY.

The CLI’s value precedence is: --flag > config file > env var > built-in default.

A typical server config (the one written by curl -fsSL bhatti.sh/install | sudo bash) looks like:

engine: firecracker
listen: :8080
data_dir: /var/lib/bhatti
firecracker_bin: /usr/local/bin/firecracker
firecracker_jailer: /usr/local/bin/jailer
jail_uid: 10000
jail_gid: 10000
firecracker_kernel: /var/lib/bhatti/images/vmlinux-arm64
firecracker_rootfs: /var/lib/bhatti/images/rootfs-minimal-arm64.ext4
FieldDefaultDescription
enginefirecrackerEngine backend. Only firecracker is implemented.
listen:8080Address for the HTTP API.
data_dir~/.bhatti (CLI) or whatever the install script writes (typically /var/lib/bhatti on a server)Root directory for state — DB, images, sandboxes, snapshots, jails.
firecracker_binAbsolute path to the firecracker binary.
firecracker_kernelPath to the vmlinux kernel image.
firecracker_rootfsPath to the default rootfs image (used when bhatti create is called without --image).
firecracker_jailerPath to the jailer binary. Empty = bare mode (no jailer; less isolation). When set, jail_uid / jail_gid apply.
jail_uid0UID Firecracker runs as inside the jail. Production: a non-root UID like 10000.
jail_gid0GID. Same shape.
public_proxy_listenWhen set (e.g. :8443), the daemon also exposes a path-based public proxy at this address. URLs are http://<host>:8443/<alias>/. Skip this and use domain mode for production.
api_urlCLI-only field. Put it in ~/.bhatti/config.yaml, not /etc/bhatti/config.yaml.
auth_tokenCLI-only. Same.
domainOptional. Enables domain mode — host-based routing + TLS.
backupOptional. Enables volume backups to S3-compatible storage.

For host-based routing with TLS — https://api.<your-domain> for the API, https://<alias>.<your-domain> for published sandboxes.

domain:
api_host: api.bhatti.sh
proxy_zone: bhatti.sh
tls_cert: /etc/bhatti/wildcard.pem
tls_key: /etc/bhatti/wildcard-key.pem
FieldDescription
api_hostHostname for the API. Requests to this host go through normal Bearer auth.
proxy_zoneZone for published sandboxes. <alias>.<proxy_zone> is the URL bhatti publish generates.
tls_cert, tls_keyPaths to a wildcard cert covering *.<proxy_zone> and <api_host>. Recommended.
acme_emailFallback: per-alias Let’s Encrypt certificates. Rate-limited to 50 new aliases per registered domain per week — fine for stable subdomains, fast to hit if you’re stamping out preview environments.

When domain mode is on, the daemon listens on :443 (TLS, both API and proxy by Host header), :80 (ACME challenges + HTTPS redirect), and 127.0.0.1:8080 (internal API for health checks).

You must set either tls_cert+tls_key or acme_email. A wildcard cert is the right answer for any setup that creates more than a handful of aliases per week. See Custom domain for the full setup walk-through.

For volume backups to S3-compatible storage. Enables bhatti volume backup, restore, backup-list, and backup-delete. Without this block, those endpoints return 501.

backup:
s3_endpoint: https://s3.eu-central-003.backblazeb2.com
s3_region: eu-central-003
s3_bucket: bhatti-backups
s3_access_key: ...
s3_secret_key: ...
schedule:
- volume: workspace
cron: "0 3 * * *"
retention: 7
FieldDescription
s3_endpointS3-compatible endpoint URL. Backblaze B2, AWS S3, MinIO, R2, etc.
s3_regionRegion. Required by AWS-compatible APIs even when irrelevant.
s3_bucketBucket name. Must already exist.
s3_access_key, s3_secret_keyCredentials with read/write/delete on the bucket.
scheduleOptional. Array of automatic backup schedules. Each entry has volume, cron (5-field cron expression), and retention (keep last N backups for that volume).

Schedules run inside the daemon — no external cron required. Retention is enforced after each scheduled backup.

Lives at ~/.bhatti/config.yaml. bhatti setup writes it for you.

api_url: https://api.bhatti.sh
auth_token: bht_abc123def456...
FieldDescription
api_urlBhatti API endpoint.
auth_tokenThe user’s API key from bhatti user create.
data_dir/
├── state.db SQLite database (WAL mode)
├── age.key Secret-encryption key (auto-generated on first secret set)
├── id_ed25519, id_ed25519.pub SSH keypair (auto-generated on first start; for guest agent identity)
├── .latest-version Cache for `bhatti version`'s GitHub-release check (~/.bhatti only)
├── images/
│ ├── vmlinux-<arch> Kernel
│ ├── rootfs-minimal-<arch>.ext4
│ ├── rootfs-browser-<arch>.ext4
│ └── ... (other tier images)
├── sandboxes/<id>/
│ ├── rootfs.ext4 CoW copy of the base image
│ ├── config.ext4 Config drive (env, secrets, files; ~1 MB)
│ ├── vol-<name>.ext4 Hard-linked attached volumes (jailer mode only)
│ ├── firecracker.sock FC API socket
│ ├── mem.snap Memory snapshot (when stopped)
│ └── vm.snap VM state snapshot (when stopped)
├── volumes/<user_id>/
│ └── <name>.ext4 Standalone volume images
├── snapshots/<user_id>/
│ └── <name>/ Named-snapshot bundle (rootfs copy + mem.snap + vm.snap)
├── jails/firecracker/<id>/ Jailer chroots, when `firecracker_jailer` is set
└── certs/ ACME certificate cache (when `domain.acme_email` is set, no wildcard cert)

<arch> is arm64 on aarch64 hosts, amd64 on x86_64.

The daemon prints which file it loaded on startup:

config loaded path=/etc/bhatti/config.yaml

If the path is empty, no config file was found and built-in defaults are used. The CLI client doesn’t print this; check with bhatti version (which uses the loaded api_url).