Self-Hosting
The open-source backend is the remote control plane. The CLI always talks to a backend; it does not provision cloud machines locally. Run the complete backend with your own provider credentials and no built-in limits. No BoxHaven subscription or company account is required; your cloud provider charges for the machines you create.
The browser app is built with TanStack Router and TanStack Query. It is the console/auth surface: login, signup, CLI device approval, invitations, and authenticated box/team/image views. The API serves the built console from dist-app, and teams can host the static docs/ build alongside it.
Use your own domains for the console, API, documentation, and box previews. The API can serve the console on the same origin for a simple installation.
The open-source console shows a compact update banner when the backend reports a newer stable core BoxHaven GitHub release. Desktop and skill releases are excluded. The backend caches successful checks for 24 hours, retries failed checks after an hour, and returns no update when offline, so release discovery never prevents the console from loading. Banner links open the exact GitHub release in a new tab.
Run Locally
cd backend
npm ci
BETTER_AUTH_SECRET="$(openssl rand -hex 32)" \
DIGITALOCEAN_ACCESS_TOKEN=dop_v1_example \
RESEND_API_KEY=re_replace_with_a_key \
BOXHAVEN_EMAIL_FROM="BoxHaven <noreply@example.com>" \
npm run devRun the web app during development from a second shell:
npm run dev:appBy default the server listens on 127.0.0.1:8787. Better Auth data, machines, images, and the lifecycle outbox share one SQLite database at ~/.local/state/boxhaven/boxhaven.sqlite.
Then point the CLI at it:
bh login --backend-url http://127.0.0.1:8787
bh create workRun With Docker Compose
From the repository root:
export BETTER_AUTH_SECRET="$(openssl rand -hex 32)"
export DIGITALOCEAN_ACCESS_TOKEN=dop_v1_example
export RESEND_API_KEY=re_replace_with_a_key
export BOXHAVEN_EMAIL_FROM="BoxHaven <noreply@example.com>"
export BOXHAVEN_VERSION="$(git describe --tags --match 'v[0-9]*' --always)"
docker compose -f docker-compose.backend.yml up --buildThe Compose service publishes 127.0.0.1:8787 by default and persists backend state in the boxhaven-backend-data Docker volume. Override the host bind with BOXHAVEN_BACKEND_PORT, for example BOXHAVEN_BACKEND_PORT=127.0.0.1:8877. When the public URL changes, also set BETTER_AUTH_URL, BOXHAVEN_APP_URL, and BOXHAVEN_API_URL so browser login links point at the reachable host. Set BOXHAVEN_DOCS_URL before docker compose ... up --build if the console footer should link to an internal docs site instead of https://docs.boxhaven.dev.
A dummy DIGITALOCEAN_ACCESS_TOKEN is enough for build, startup, health, and read-only API checks. Creating boxes from this stack still requires a real DigitalOcean token and a CLI login token for the local backend.
Build A Distribution
The backend is also a local npm package named @boxhaven/backend. A custom distribution can depend on it and call startBackendFromEnv({ modules, appDir }) from its own entrypoint. Compile-time modules can add sequential migrations, module tables, authenticated Fastify routes, and one in-process commercial policy. Their migrations share the boxhaven_migrations ledger and SQLite database with core, while a separately built appDir replaces the open-source browser UI for that distribution.
The standard entrypoint loads no modules and has no locked features. Modules can reuse the core API, auth, provider, SSH, and CLI implementations.
Environment Variables
BETTER_AUTH_SECRET: required signing secret for Better Auth sessions.BETTER_AUTH_URL: public auth base URL, defaulthttp://<listen>/v1/auth.BETTER_AUTH_TRUSTED_ORIGINS: comma-separated trusted browser origins.RESEND_API_KEY: required Resend API key. Password signups must follow an emailed verification link before signing in. The link verifies the email and signs in to that account automatically, replacing any other account open in the browser.GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRET: optional GitHub OAuth credentials. The console shows GitHub sign-in only when both are configured; email sign-in remains available.BOXHAVEN_EMAIL_FROM: transactional From address, required verified sender address from your own domain (for example,BoxHaven <noreply@example.com>).BOXHAVEN_EMAIL_VERIFICATION_EXPIRES_SECONDS: positive verification-link lifetime, default3600.BOXHAVEN_APP_URL: public console/auth app URL, default derived fromBETTER_AUTH_URLin direct runs andhttp://127.0.0.1:8787in Compose.BOXHAVEN_API_URL: public API URL, default derived fromBETTER_AUTH_URLin direct runs andhttp://127.0.0.1:8787in Compose.BOXHAVEN_VERSION: current backend version used by the public/v1/versionrelease-status endpoint. Set it from CLI release tags usinggit describe --tags --match 'v[0-9]*' --always; this excludes separately versioned skills. The production deploy script derives it automatically and verifies the running backend received it.BOXHAVEN_DOCS_URL: public documentation URL used by console footer links in Docker builds. Set this when self-hosting internal docs; otherwise the app links tohttps://docs.boxhaven.dev.BOXHAVEN_BACKEND_CORS_ORIGINS: comma-separated browser origins allowed to call the API.BOXHAVEN_PREVIEW_BASE_DOMAIN: optional base domain for generated machine preview hosts, such asat.example.com.BOXHAVEN_PREVIEW_TARGET_PORT: machine port that preview hosts proxy to, default80.BOXHAVEN_DATABASE_PATH: shared SQLite database path, default~/.local/state/boxhaven/boxhaven.sqlite.BOXHAVEN_BACKEND_LISTEN: listen address, default127.0.0.1:8787.BOXHAVEN_SSH_CA_KEY: backend SSH user CA private key path, default besideBOXHAVEN_DATABASE_PATH.BOXHAVEN_ADMIN_EMAILS: comma-separated emails granted admin access to the image-management endpoints.BOXHAVEN_MAX_TEAMS_PER_USER: optional positive cap on teams a user owns. Concurrent creates reserve capacity and invitations to someone else's team do not consume an ownership slot.BOXHAVEN_MAX_MACHINES_PER_USER: optional positive per-user cap on existing and provisioning boxes across all of their teams. When the cap is reached,POST /v1/machinesreturns403with{ "id": "limit_reached" }. Capacity is released after a definitive provider failure or successful destroy. Leave it unset for no built-in limit.BOXHAVEN_COMMERCIAL_POLICY_RETRY_MS: failed policy delivery or policy-requested machine cleanup retry delay, default30000.BOXHAVEN_COMMERCIAL_POLICY_RECONCILE_INTERVAL_MS: complete active-machine reconciliation and lifecycle-policy evaluation interval, default300000.BOXHAVEN_BACKEND_PROVIDER: default provider for creates that do not request one explicitly. When unset, the first configured provider is the default (DigitalOcean when both are configured).
Provider credentials and image variables (DIGITALOCEAN_*, HCLOUD_TOKEN, HETZNER_*, EXE_DEV_SIGNING_KEY, BOXHAVEN_REMOTE_IMAGE*) are documented on the Cloud Providers page. The backend modules page documents the build-time extension boundary for distributions that add custom models, routes, or policy.
Production DigitalOcean Deployment
The repository includes a production bundle in deploy/digitalocean/ for a self-hosted installation:
app.example.comfor the browser console/auth appapi.example.comfor API and Better Auth routesdocs.example.comfor the static documentation site*.at.example.comfor generated machine preview URLs- Caddy-managed TLS in front of the backend container
- a Caddy file-server mount for the built
docs/.vitepress/distartifact - host-mounted backend and Caddy data under
/opt/boxhaven/data - a systemd timer that writes daily archives to
/opt/boxhaven/backups
Provision
Create an Ubuntu 24.04 Droplet with deploy/digitalocean/cloud-init.yml. Enable DigitalOcean Droplet backups for machine-level restore coverage.
Required DNS records:
app.example.com. A <droplet-ip>
api.example.com. A <droplet-ip>
docs.example.com. CNAME app.example.com.
*.at.example.com. A <droplet-ip>Configure
Copy env.production.example to .env.production on the server and fill in the secret values:
cp deploy/digitalocean/env.production.example deploy/digitalocean/.env.productionBETTER_AUTH_SECRET must be a long random value and RESEND_API_KEY is required for password-account verification in every self-hosted deployment. Configure a Resend-verified BOXHAVEN_EMAIL_FROM before startup. The backend also needs DIGITALOCEAN_ACCESS_TOKEN so it can create remote VMs for users. The backend SSH user CA is stored at /opt/boxhaven/data/backend/ssh_ca_ed25519 and is included in the backend data backups. Startup creates this keypair before serving requests, so backups work before the first box is created. Set BOXHAVEN_DOCS_HOST to the documentation hostname, BOXHAVEN_DOCS_URL to the public URL the console should link to, and BOXHAVEN_PREVIEW_BASE_DOMAIN to the wildcard domain above.
Deploy
Deploy the public self-hosted stack from the repository root:
npm run deploy:app -- --target root@app.example.comThe SSH target is required: use -- --target user@host or set BOXHAVEN_DEPLOY_TARGET. The command fast-forwards /opt/boxhaven/app on master, builds the docs site, and runs the Compose deploy on that machine. Health checks use BOXHAVEN_API_URL, BOXHAVEN_APP_URL, and BOXHAVEN_DOCS_URL from its Compose environment. The remote VM snapshot is rebuilt separately. On the Droplet itself, use npm run deploy:production:local.
Distributions can add a build-time module or replace deployment wiring with BOXHAVEN_PRODUCTION_COMPOSE_OVERLAY_FILE and an optional BOXHAVEN_PRODUCTION_COMPOSE_OVERLAY_ENV_FILE. The corresponding flags are --compose-overlay and --compose-overlay-env-file. When an existing backend was deployed with an overlay, deployment and verification require an overlay again. Use the distribution's deployment command to supply its full configuration.
Health Checks And Backups
npm run deploy:production:verify -- --target root@app.example.com
sudo systemctl status boxhaven-backend-backup.timer --no-pagerBackups are installed through deploy/digitalocean/install-backups.sh and write archives under /opt/boxhaven/backups. The backend data backup uses SQLite's online backup command and requires boxhaven.sqlite and the SSH CA keypair. It validates the copied database before atomically publishing the archive and includes Caddy data when present. The golden remote image includes the sqlite3 CLI so the checked-in backup integrity test can run from a box.
Golden Image Rotation
Remote runtime dependencies belong in the golden VM image. After changing cmd/bh/assets/remote-vm-install.sh, build and activate a new snapshot from a clean committed checkout or pushed ref:
deploy/digitalocean/build-remote-image.sh \
--env-file deploy/digitalocean/.env.production \
--set-activeThe installer pins Codex CLI 0.153.3 for gpt-6-astra support. After rotating the snapshot, create new boxes to use the updated CLI; existing boxes keep their installed version.
Or use the checked-in npm entrypoint:
npm run deploy:runtime -- --target root@app.example.comEach build creates a fresh snapshot and retains existing snapshots. Names are also chosen by teams, so a boxhaven-remote-* prefix does not identify an image as safe to delete. After a successful production smoke, review old snapshots and delete only specific IDs you have confirmed are no longer needed, retaining a known-good golden image for rollback. Retained snapshots incur storage charges.
The runtime deploy creates and snapshots a temporary DigitalOcean builder Droplet, updates BOXHAVEN_REMOTE_IMAGE, then restarts and verifies the backend so new boxes use the image. When an active BOXHAVEN_REMOTE_IMAGE exists, the builder starts from that snapshot by default instead of reinstalling the full OS/toolchain from Ubuntu. Use npm run deploy:runtime -- --target root@app.example.com --full-base-image only for base OS or runtime dependency rebuilds.
npm run deploy:runtime writes BOXHAVEN_REMOTE_IMAGE to the env file as the env-configured default. Team images do not override that default globally; they are selected per box with bh create --image <name-or-id> or the console create form.
Keep the previous snapshot id until the remote lifecycle smoke passes.
Remote Lifecycle Smoke
Run the reusable remote lifecycle smoke against the backend after remote VM, SSH, sync, snapshot, preview, or agent changes:
BOXHAVEN_SMOKE_BACKEND_URL=https://api.example.com \
BOXHAVEN_TOKEN=... \
GH_TOKEN=... \
BOXHAVEN_SMOKE_GIT_REMOTE=https://github.com/<org>/<smoke-repo>.git \
make smoke-remoteThe default smoke is intentionally fast: it creates one box from the active snapshot, syncs a temporary Git project, verifies runtime tools, fetches the preview URL, optionally pushes and deletes a temporary GitHub smoke branch, and destroys the box unless BOXHAVEN_SMOKE_KEEP=1 is set.
Useful options:
BOXHAVEN_SMOKE_BACKEND_URL=https://api.example.com
BOXHAVEN_SMOKE_TIER=small
BOXHAVEN_SMOKE_PREFIX=my-smoke
BOXHAVEN_SMOKE_KEEP=1
BOXHAVEN_SMOKE_REQUIRE_PREVIEW=0Use make smoke-remote-full with BOXHAVEN_SMOKE_RESTART_BACKEND_CMD when the agent reconnect path needs coverage. Use make smoke-remote-two-box only for concurrency, provider import, or multiple-machine behavior.
Web Preview
Each box receives a public preview URL when the backend is configured with a preview base domain. The backend warms the preview URL during machine create so Caddy has already completed on-demand certificate issuance before the URL is shown. Public HTTPS and WebSocket traffic terminate at the BoxHaven control plane (Caddy terminates HTTPS on the control-plane Droplet), then the backend proxies plain HTTP/WebSocket traffic to the machine's BOXHAVEN_PREVIEW_TARGET_PORT, default 80. Remote apps do not need to manage public TLS for the preview URL.
Inside the box, commands receive:
BOXHAVEN_PREVIEW_URL: the browser URL to share.BOXHAVEN_PREVIEW_HOSTNAME: the public hostname.BOXHAVEN_PREVIEW_TARGET_PORT/BOXHAVEN_WEB_PORT: the machine port to serve, normally80.BOXHAVEN_WEB_BIND: the bind address to use, normally0.0.0.0./run/boxhaven/context.json: structured runtime context with the same preview details under.preview.
Apps should bind HTTP to 0.0.0.0:$BOXHAVEN_WEB_PORT or run a reverse proxy on that port to the app's internal dev-server port. Framework dev-server WebSockets, including Vite HMR, use the same preview URL. The default boxhaven user has sudo access if binding to port 80 is required.
Version compatibility
Product versions can advance without changing the wire protocol. Clients send X-BoxHaven-Protocol: 1; the backend advertises the supported protocol at /v1/compatibility and rejects a different protocol with HTTP 426 and upgrade instructions. Clients and images predating negotiation speak the original protocol 1. Deploy the backend before clients when making an incompatible change.
VM agents report their protocol and an SHA-256 fingerprint of their agent code. bh status NAME and machine JSON expose runtime_protocol and runtime_version; older agents show an unreported fingerprint. Incompatible agent RPC is blocked without stopping the VM or its running processes. A golden-image rotation affects new boxes only. Preserve existing work, create a replacement from the new image, and verify it before destroying the old box. BoxHaven does not rewrite running VMs during a backend or desktop update.
Upgrading a core installation
Release images are published as ghcr.io/finbarr/boxhaven-backend:vX.Y.Z for AMD64 and ARM64. The release's release-manifest.json records the immutable image digest and compatible desktop, CLI, API, and runtime versions.
For an initial installation, configure the variables documented above, set BOXHAVEN_BACKEND_IMAGE to the release image, and run:
docker compose -p boxhaven --env-file .env -f docker-compose.release.yml up -dUpgrade explicitly; existing boxes keep running and reconnect when the backend returns. From the repository checkout:
npm run upgrade:backend -- --version v0.4.0 --env-file .envThe command pulls the image, stops the backend, checks SQLite integrity, and backs up /data (database and SSH CA) plus resolved Compose configuration before migrations run. It pins the selected image locally by ID, starts it, and checks health, version, and protocol. Backups are private under .boxhaven-upgrades/; they contain secrets. Retain the previous Docker image until the rollback window closes. Use your normal off-host backup as well.
After an upgrade, use the saved resolved configuration for subsequent starts:
docker compose -p boxhaven -f .boxhaven-upgrades/active.json up -d --no-build --pull neverA failed health check stops the new backend and prints a recovery directory. After inspecting logs and preserving newer work, restore the earlier image and its database together:
npm run upgrade:backend -- --rollback .boxhaven-upgrades/RECOVERY_DIRECTORY --accept-data-lossRollback discards database changes since the backup; it first archives the current data separately. VM files are not rolled back. If boxes were created or destroyed since that backup, reconcile cloud resources before resuming use. Changing cloud configuration requires a new reviewed Compose configuration; the saved active configuration deliberately preserves the deployed settings.
This command supports the core distribution with all persistent state in /data. It refuses unlabelled/custom distributions. Hosted billing and other modules must use their complete distribution's deployment and backup procedure. Source-based development continues to use docker-compose.backend.yml.
Backend relay capacity
Terminals and file transfers pass through the backend over WebSockets. Allow WebSocket upgrades on the API proxy and outbound TCP 22 / HTTPS to providers. Budget bandwidth and connections for this traffic. SSH contents remain encrypted between the client and guest. A backend restart disconnects attachments; remote work continues and users can reconnect. Private sandbox previews also use the backend and require BOXHAVEN_PREVIEW_BASE_DOMAIN with wildcard DNS/TLS.
