A small HTTP server that wraps microsandbox and exposes its microVMs over a clean REST API.
microsandbox is a local microVM runtime — fast, hardware-isolated sandboxes booted from OCI images via libkrun. It's terrific, but the SDK is in-process and Go-only.
msbd puts a small daemon and a REST API in front of it, so any language can drive microsandbox over plain HTTP. Run msbd once on a host that has /dev/kvm, then curl it (or generate a client from the OpenAPI spec) from wherever.
- Simple. ~12 endpoints, OpenAPI 3.1 spec, JSON in/out, bearer auth.
- MicroVMs survive restarts. Sandboxes are created detached; msbd reconnects them by name on boot.
- Native primitives. Real exec sessions for async jobs, real file IO over the guest filesystem.
- Interactive terminals. A real kernel-PTY shell over WebSocket — colors, line editing, window resize, and full-screen TUIs (vim, top) all work.
docker run --rm \
--device /dev/kvm \
-p 8099:8099 \
-e MSBD_API_KEY=devkey \
-v msbd-data:/root/.microsandbox \
ghcr.io/mark3labs/msbd:latestThe first start downloads the microsandbox runtime (~30 MB) into the mounted volume. Subsequent starts skip it. Wait for /readyz to return 200:
curl -fsS localhost:8099/readyz # → readycurl -s -H "Authorization: Bearer devkey" \
-X POST localhost:8099/v1/sandboxes \
-d '{"image":"alpine:3.19","resources":{"memory_mb":512,"cpu":1}}'{
"id": "sbx_1ea598fdaabd2a46",
"image": "alpine:3.19",
"state": "running",
"workdir": "/",
"uptime_seconds": 0,
"labels": null
}ID=sbx_1ea598fdaabd2a46
curl -s -H "Authorization: Bearer devkey" \
-X POST localhost:8099/v1/sandboxes/$ID/exec \
-d '{"cmd":"uname -a && whoami"}'{
"exit_code": 0,
"stdout": "Linux sbx_1ea598fdaabd2a46 6.12.68 ... x86_64 GNU/Linux\nroot\n",
"stderr": ""
}curl -s -H "Authorization: Bearer devkey" -X DELETE localhost:8099/v1/sandboxes/$IDBrowse the full API interactively at
http://localhost:8099/docs(Swagger UI), or fetch the raw spec from/openapi.yaml. Both are unauthenticated.
msbd is packaged as a flake. cgo is enabled at build time, but the only thing
the C side links is libdl — the microsandbox Rust FFI library is dlopen'd at
runtime, so no Rust toolchain is needed to build msbd.
# Build the binary
nix build github:mark3labs/msbd
# Run it (uses the FHS-wrapped variant — works on NixOS too)
nix run github:mark3labs/msbdWhy the FHS wrapper? msbd itself is a normal Nix-built binary, but the msb
supervisor it downloads on first run and the embedded FFI .so it extracts are
vanilla glibc binaries that expect a dynamic loader at /lib64/ld-linux-*.so.2
and libcap-ng.so.0 on a standard path. Plain NixOS has neither, so the
msbd-fhs package (what nix run and the NixOS module use) provides that FHS
layout. On a regular glibc distro (Debian/Ubuntu/Fedora) the plain msbd
package is enough.
Flake outputs:
| Output | What |
|---|---|
packages.default / packages.msbd |
The bare cgo binary (good on any glibc distro). |
packages.msbd-fhs |
FHS-wrapped binary for NixOS hosts. |
devShells.default |
Go + gcc + the runtime libs, CGO_ENABLED=1. |
nixosModules.default |
services.msbd — runs it as a hardened systemd service with /dev/kvm access. |
As a NixOS service:
{
inputs.msbd.url = "github:mark3labs/msbd";
# in your system config:
imports = [ msbd.nixosModules.default ];
services.msbd = {
enable = true;
listen = ":8099";
apiKeyFile = "/run/secrets/msbd.env"; # systemd EnvironmentFile with MSBD_API_KEY=...
openFirewall = true;
};
}The module joins the service to the kvm group, allows /dev/kvm, and keeps the
runtime + image cache under /var/lib/msbd.
msbd boots real microVMs, so the host machine must have working hardware virtualization:
| Host | Needs |
|---|---|
| Bare-metal Linux | /dev/kvm present (almost always) |
| Linux VM | Nested virtualization enabled by the parent hypervisor; /dev/kvm exposed |
| Docker container | Run with --device /dev/kvm (or --privileged). The host kernel still has to expose KVM. |
| macOS / Windows | Use the upstream microsandbox SDK directly; msbd is Linux-only by design. |
Quick host check:
ls -l /dev/kvm # device exists
egrep -c '(vmx|svm)' /proc/cpuinfo # CPU virt flag present
cat /sys/module/kvm_{intel,amd}/parameters/nested 2>/dev/null # Y/1 if VMAll via environment variables.
| Var | Default | Description |
|---|---|---|
MSBD_LISTEN |
:8099 |
HTTP listen address. |
MSBD_API_KEY |
(empty) | Bearer token(s) required on every request; comma-separated to accept several (zero-downtime rotation). Empty = unauthenticated (dev only). |
MSBD_API_KEY_FILE |
(empty) | Read the bearer token from a file instead of the env (Docker/K8s secrets). Takes precedence over MSBD_API_KEY. |
MSBD_DEFAULT_IMAGE |
microsandbox/python |
OCI image used when create omits image. |
MSBD_MAX_SANDBOXES |
0 (unlimited) |
Hard cap on concurrent sandboxes; rejects new creates above this with 507 capacity. Admission is serialized (no overshoot). |
MSBD_CREATE_TIMEOUT_SECS |
300 |
Boot deadline (covers cold OCI pulls). |
MSBD_SHUTDOWN_TIMEOUT_SECS |
60 |
Graceful-drain deadline on SIGTERM/Ctrl-C. A drain overrun warns and exits 0 (no spurious restart failure). |
MSBD_HOST_PATHS |
(empty) | Comma-separated allowlist of host path prefixes the host-transfer endpoints (copy-from-host, copy-to-host, snapshot export/import) may touch. Empty = all host transfers denied (403). Symlinks are resolved to block escapes. |
MSBD_LOG_LEVEL |
info |
Log verbosity: debug, info, warn, error. Invalid values fail fast. Output is colorized on a TTY, plain otherwise. |
MSBD_DASHBOARD |
true |
Serve the web dashboard at /dashboard. Set false to disable. |
MSBD_DASHBOARD_USER |
(empty) | Dashboard HTTP Basic auth username. Setting user or pass turns auth on. |
MSBD_DASHBOARD_PASS |
(empty) | Dashboard HTTP Basic auth password. Both empty = dashboard is unauthenticated. When an API key IS set but dashboard auth is not, the dashboard is refused (it would bypass the API token) unless MSBD_DASHBOARD_ALLOW_INSECURE=true. |
MSBD_DASHBOARD_ALLOW_INSECURE |
false |
Override the safety refusal above and serve the dashboard without auth even when an API key is set (unsafe). |
Request bodies are size-capped (1 MiB control-plane, 64 MiB file writes) and reject unknown JSON fields (a typo'd field is a 400, not a silent no-op). The server sets IdleTimeout and echoes an X-Request-Id on every response.
Flags mirror every var (--dashboard, --host-paths, --shutdown-timeout, --api-key-file, …); flag › env › default.
A self-contained web UI lives at /dashboard that manages everything the REST API does — sandboxes (create, start/stop/delete, inspect, run commands, live logs & metrics, a file browser, and a real kernel-PTY terminal), volumes, images and snapshots.
# Behind HTTP Basic auth:
MSBD_DASHBOARD_USER=admin MSBD_DASHBOARD_PASS=s3cret msbd serve
# → open http://localhost:8099/dashboardIt is server-rendered with templ + templui components, styled with Tailwind, and made reactive with Datastar (SSE-driven DOM patching). Everything — the compiled CSS, the Datastar runtime, xterm.js and the component JavaScript — is embedded in the binary (//go:embed); there are no external assets to deploy. Auth is independent of MSBD_API_KEY: the API stays bearer-gated while the dashboard gets its own optional Basic auth. The terminal page never embeds the API key — it uses a short-lived, single-use ticket — and if you set an API key but no dashboard auth, msbd refuses to mount the dashboard (override with MSBD_DASHBOARD_ALLOW_INSECURE=true).
| Method & path | Purpose |
|---|---|
GET /healthz · GET /readyz |
Liveness · readiness (runtime loaded + /dev/kvm accessible). |
GET /docs · GET /openapi.yaml |
Swagger UI · raw OpenAPI spec (unauthenticated). |
GET /dashboard |
Web management UI (optional Basic auth — see Web dashboard). |
GET /v1/version |
Default image + runtime/SDK versions (diagnostics). |
GET /metrics |
Prometheus text-exposition operational metrics (sandbox counts, jobs, terminals, request classes). |
POST /v1/terminal-tickets |
Mint a short-lived single-use terminal ticket (browser WS auth without exposing the API key). |
POST /v1/sandboxes · GET /v1/sandboxes · GET/DELETE /v1/sandboxes/{id} |
Lifecycle. Create accepts user, hostname, network_policy, ports, secrets, mounts. |
GET /v1/sandboxes/{id}/inspect |
Sandbox metadata + raw SDK config blob. |
POST /v1/sandboxes/{id}/stop · .../start |
Pause / ensure-running. |
POST /v1/sandboxes/{id}/exec · .../run |
Synchronous exec — exec is short, run is long-safe and ensures-running. |
GET /v1/sandboxes/{id}/terminal |
Interactive kernel-PTY terminal over WebSocket (binary stdin/stdout; text control frames for resize/signal). Colors, line editing, resize, vim/top all work. Auth via header, ?key=, or a single-use ?ticket= (see POST /v1/terminal-tickets). |
POST /v1/sandboxes/{id}/jobs · GET /v1/sandboxes/{id}/jobs/{job} |
Async (background) jobs. Output is a bounded ring buffer (1 MiB/stream); poll reports truncated + stdout_bytes/stderr_bytes, and finished jobs are evicted after a TTL. |
POST /v1/sandboxes/{id}/jobs/{job}/stdin · .../signal |
Write to a job's stdin (launch with stdin:true) · send a signal (≤0 = kill). |
POST /v1/sandboxes/{id}/files/read · .../files/write |
Native file IO, base64-encoded. |
POST /v1/sandboxes/{id}/files/{list,stat,exists,mkdir,remove,copy,rename} |
Extended filesystem operations. |
POST /v1/sandboxes/{id}/files/{copy-from-host,copy-to-host} |
Copy between an allowlisted host path (MSBD_HOST_PATHS) and the sandbox. Denied (403) when the allowlist is empty or the path escapes it. |
GET /v1/metrics · GET /v1/sandboxes/{id}/metrics |
Point-in-time per-sandbox resource metrics (all / one). For scrapeable ops telemetry use GET /metrics. |
GET /v1/sandboxes/{id}/logs |
Read persisted stdout/stderr/system logs (?tail=, ?sources=). |
POST/GET /v1/volumes · GET/DELETE /v1/volumes/{name} |
Named persistent volumes. |
POST /v1/volumes/{name}/files/{read,write,mkdir,remove,exists} |
Volume file IO. |
GET /v1/images · GET /v1/images/inspect · POST /v1/images/pull · DELETE /v1/images · POST /v1/images/prune |
Cached OCI image inventory. pull fetches an image into the cache (long-running; boots a throwaway microVM). |
POST/GET /v1/snapshots · GET/DELETE /v1/snapshots/{name} · .../verify |
Sandbox rootfs snapshots. |
POST /v1/snapshots/{export,import,reindex} |
Export/import snapshot archives · rebuild the index. |
Full schemas: see openapi.yaml.
- Detached by default. Every sandbox is created detached, so the microVM keeps running when msbd restarts.
- Reconnect at boot. On startup msbd lists all known sandboxes and re-attaches by name. A sandbox that existed before the restart is still callable through the same id.
- Transparent resume.
run,launch, andfiles/*all ensure-running first — a paused box silently resumes on the next call.exec(one round-trip helpers) deliberately does not, so it stays cheap. - Jobs and terminals are in-memory. A job that was running when msbd restarts polls as
gone, and an open terminal's WebSocket simply closes (the VM survives; the streaming attach does not). Re-launch / reconnect from the client side. - Names are ids. Sandbox names (≤128 bytes UTF-8) ARE the provider id. msbd generates them as
sbx_<16hex>; you can also pass your own.
✅ A simple way to expose microsandbox over HTTP so any language can drive it. ✅ A single-host, single-tenant device server. Auth your real users upstream.
❌ Not a multi-host scheduler. Capacity = the one host. ❌ Not a multi-tenant platform with quotas, billing, RBAC. (Bring your own.) ❌ Not a re-implementation of microsandbox's own cloud backend.
# Build (or `task build`)
go build -o ./bin/msbd ./cmd/msbd
# Run (these are equivalent — the bare binary defaults to `serve`)
MSBD_API_KEY=devkey ./bin/msbd
./bin/msbd serve --api-key devkey --listen :8099
# Explore the CLI (styled help, version, shell completions)
./bin/msbd --help
./bin/msbd serve --help
./bin/msbd --version
# Lint, format, test (or `task lint` / `task fmt` / `task test`)
golangci-lint run ./...
gofmt -w .
go test ./...The CLI is built on cobra and styled with
charmbracelet/fang. Every MSBD_* env
var has a matching serve flag (flag overrides env overrides default), and
Ctrl-C / SIGTERM trigger a graceful drain of in-flight requests.
cmd/msbd/main.go # entrypoint — EnsureInstalled, reconcile, serve
assets.go # //go:embed openapi.yaml (served at /docs)
internal/api/router.go # HTTP router + middleware (auth, recover, log)
internal/api/handlers.go # core lifecycle/exec/jobs/files handlers
internal/api/handlers_ext.go # inspect, metrics, logs, fs, volumes, images, snapshots
internal/api/terminal.go # interactive terminal WebSocket handler
internal/api/docs.go # Swagger UI (/docs) + raw spec (/openapi.yaml)
internal/api/dto.go # wire shapes
internal/core/service.go # SDK-facing business logic (lifecycle/exec/jobs/files)
internal/core/terminal.go # interactive PTY terminal: Session interface + OpenTerminal
internal/core/terminal_agent.go # kernel-PTY backend over the raw agent protocol (CBOR)
internal/core/fs.go # extended filesystem ops + host transfer
internal/core/metrics.go # point-in-time resource metrics
internal/core/logs.go # persisted log reads
internal/core/volume.go # named persistent volumes + volume file IO
internal/core/image.go # cached OCI image inventory
internal/core/snapshot.go # sandbox rootfs snapshots
internal/core/registry.go # live handle cache + workdir cache + reconcile
internal/core/jobs.go # async job registry (+ stdin/signal)
internal/core/version.go # SDK / runtime version helpers
internal/dashboard/dashboard.go # web UI: routes + optional Basic auth (Mount on the api mux)
internal/dashboard/handlers_*.go # Datastar SSE handlers (sandboxes, volumes, images, snapshots)
internal/dashboard/views/*.templ # templ pages/fragments (templui components + Datastar attrs)
internal/dashboard/components/ # vendored templui components (via `templui add`)
internal/dashboard/assets/ # input.css + committed output.css, datastar/xterm, component JS (embedded)
openapi.yaml # the contract
VERSION # release version (single source of truth)
Taskfile.yml # dev + release tasks (go-task)
flake.nix # Nix package + dev shell + NixOS module
Dockerfile # build from source
Dockerfile.release # used by goreleaser
docker-compose.yml # example compose deploy
The git tag is the source of truth for the version. Use the release task so the
VERSION file and the tag are bumped atomically (you type the version once):
task release NEW_VERSION=1.2.3 # bump VERSION, commit, tag locally
git push origin HEAD v1.2.3 # push to trigger the release workflow
# or in one shot:
task release:push NEW_VERSION=1.2.3The task refuses to run on a dirty tree, validates semver, and won't clobber an
existing tag. The release workflow then verifies v$(cat VERSION) equals the
pushed tag and fails on mismatch.
GoReleaser injects the version from the tag (-X main.version); the Nix flake
reads the same number from VERSION (flakes can't see git tags), so nix build
off a tagged checkout reports an identical version. commit/date are filled
from the tag's revision in both paths.
Apache-2.0 — see LICENSE and NOTICE.
msbd wraps the microsandbox Go SDK (also Apache-2.0). The microVM runtime it drives — msb + libkrunfw (LGPL) — is not bundled with msbd; the SDK downloads it to ~/.microsandbox/ on first run. See NOTICE for details.