Deploy on Kubernetes
Run Hiver on any Kubernetes cluster with the official Helm chart. The chart deploys the control plane (controller + Envoy gateway) and a pool per sandbox image.
Prerequisites
- A running Kubernetes cluster (1.27+)
kubectlconfigured to target your clusterhelm(v3+) installed
MicroVM images (the default) require nested virtualization on the nodes. See MicroVM runtime. Use the plain container images if your nodes don't support it.
Install with Helm
Add the Hiver chart repository and install the chart:
helm repo add hiver https://hiver-sh.github.io/hiver
helm repo update
helm install hiver hiver/hiverThe chart creates its own hiver and hiver-sandbox namespaces, so you don't
need --namespace or --create-namespace. Every image is pinned to an immutable
digest, so a chart version always installs the exact images it shipped with.
Pin a specific version (chart versions track the CLI/client version):
helm search repo hiver/hiver --versions
helm install hiver hiver/hiver --version <x.y.z>Available images
Each entry becomes a sandbox pool addressable by its name (e.g.
image: "claude"). Both a container and a microVM variant are published.
| Name | Description | Container | MicroVM |
|---|---|---|---|
claude | Claude Code | hiversh/claude:latest | hiversh/claude:latest-microvm |
codex | OpenAI Codex | hiversh/codex:latest | hiversh/codex:latest-microvm |
copilot | GitHub Copilot | hiversh/copilot:latest | hiversh/copilot:latest-microvm |
openclaw | OpenClaw | hiversh/openclaw:latest | hiversh/openclaw:latest-microvm |
antigravity | Antigravity | hiversh/antigravity:latest | hiversh/antigravity:latest-microvm |
browser | Resident Chrome, driven over CDP | hiversh/browser:latest | hiversh/browser:latest-microvm |
python | Python 3.13 on Alpine | hiversh/python:3.13-alpine | hiversh/python:3.13-alpine-microvm |
node | Node.js on Alpine | hiversh/node:alpine | hiversh/node:alpine-microvm |
Configuration
Override the defaults with your own values.yaml. The key knob is
sandboxServices — a map keyed by service name. Each entry generates a
Deployment + Service for the pool and the gateway's Envoy route to it.
Because it's a map (not a list), Helm deep-merges your overrides into the defaults, so you can change a single field of a single pool without redeclaring the others. Warm claude's pool to 3 pods:
helm upgrade hiver hiver/hiver --set sandboxServices.claude.replicas=3replicas: 0 keeps a pool defined but scales its prewarmed pods to zero
(sandboxes still launch on demand); raise it to keep hot pods ready. In a values
file:
sandboxServices:
python:
replicas: 2
maxConcurrentLaunches: 4
resources:
requests:
cpu: "1"
memory: 512Mi
limits:
cpu: "4"
memory: 4Gihelm upgrade hiver hiver/hiver --values values.yamlIsolation (microVM vs container)
Each service ships both image variants and picks one with its own isolation
field (microvm | container, default microvm). There is no global switch —
set it per service:
helm upgrade hiver hiver/hiver --set sandboxServices.claude.isolation=containerRun helm show values hiver/hiver to see the full defaults. Other common
overrides: controller.image / gateway.image (pin different control-plane
images), gatewayUrl (the callback URL injected into sandboxes), and
proxyPassthroughAll (disable TLS MITM in the egress proxy).
Memory backend
Controls how a resumed microVM gets its guest memory back (see Resume from a snapshot). They only matter for pools that resume from a snapshot — a pool that only ever cold-boots has nothing for either knob to speed up.
memBackend: uffd— serve guest memory from a userfaultfd handler that populates it in the background, instead of letting Firecracker map the snapshot's memory file and demand-page it in 4KiB at a time. This is the knob to reach for first: measured on a real pool it traded ~15ms of extra snapshot-load time for ~725ms less first-token latency (~15,545 page faults down to 15). It costs residency — the guest becomes fully resident on resume instead of paging in its working set — so budget the whole guest's memory per concurrent VM, not just its working set.hugePages: "2M"(or"1G") — back guest memory with hugetlbfs pages instead of 4KiB ones, shrinking that same fault count roughly 300x at 2MiB granularity. ImpliesmemBackend: uffdon its own — Firecracker cannot map a hugetlbfs-backed snapshot through the plain file-mapped backend — so there's no need to setmemBackendalongside it.hugePagesLimit— overrides the size of the pod'shugepages-2Mi(or-1Gi) resource limit, which otherwise defaults toresources.requests.memory(one guest's worth). Hugepage memory is a hard, non-reclaimable allocation kept separate from the pod's regularmemorylimit, and a pool pod runs several microVMs concurrently — so the derived default under-sizes any pool with concurrency greater than one. Set it explicitly to cover peak concurrent VMs per pod, or Firecracker fails later VM boots once the allocation is exhausted (there's no fallback to 4KiB pages).
sandboxServices:
claude:
hugePages: "2M" # implies memBackend: uffd
hugePagesLimit: 2Gi # e.g. 4 concurrent VMs x 512Mi guest eachEnabling hugePages also requires two things outside the chart, or the guest
fails to boot:
- A preallocated hugetlb pool on the node. The chart only sets the
container's resource limit — it doesn't provision the node, and a guest
fails to boot with ENOMEM until a matching pool exists. This has to be
configured at node boot (on GKE, via the node pool's hugepage node
config): kubelet enumerates hugepages at startup, so a pool added
afterward shows up in
/proc/meminfobut stays0inkubectl get node -o jsonpath='{.status.capacity}', and pods can never request it. - Any existing snapshot re-captured.
hugePagesis boot-time guest state baked into the VM snapshot at capture time, so an existing base snapshot must be re-captured before a resume sees any benefit — or boots correctly at all.
memBackend: uffd alone for the common case: a warmed,
frequently-resumed pool that wants faster resume without a node-level
hugepage carve-out to manage. Reach for hugePages on top of it only for
high-concurrency, resume-heavy pools where the remaining fault overhead
matters enough to justify permanently reserving node memory that can't be
reclaimed for anything else.Adding your own service
You can add a pool for your own image. It takes two steps:
-
Build the image as a Hiver bundle — a plain container can't serve sandboxes. Turn a Dockerfile directory (or an existing image) into one with
hiver bundle:hiver bundle ./docker/mytool --entrypoint="tail -f /dev/null" \ --tag myrepo/mytool:1.0.0 --push --platform linux/amd64,linux/arm64Add
--microvmfor the microVM variant (e.g.myrepo/mytool:1.0.0-microvm); build only the variant(s) your pool'sisolationuses, and push to a registry the cluster can pull from. -
Add a key under
sandboxServices:
sandboxServices:
mytool:
image:
microvm: myrepo/mytool:1.0.0-microvm
container: myrepo/mytool:1.0.0
isolation: microvm
replicas: 1
maxConcurrentLaunches: 4
resources:
requests:
cpu: "1"
memory: 512Mi
limits:
cpu: "4"
memory: 4Giimage may be the variant map above or a plain string for a single-variant
image. Clients then address the pool by name (image: "mytool") — the gateway
routes on that name, so nothing else is registered client-side. See the
chart README
for the full walkthrough.
Verify
Hiver uses two namespaces:
hiver— control-plane services: the controller and gatewayhiver-sandbox— pods for running sandboxes
kubectl get pods -n hiver
# NAME READY STATUS AGE
# controller-7d9f6b8c4-xkqzp 1/1 Running 2m
# gateway-5b8f9d6c3-rplwk 1/1 Running 2mSandbox pools scale from their configured replicas (0 by default for most, so
sandboxes launch on demand); any pool with replicas > 0 shows prewarmed pods:
kubectl get pods -n hiver-sandboxGet the gateway address
The gateway is a LoadBalancer Service; the cloud assigns it an external IP
(a <pending> value just means it's still provisioning):
kubectl get svc gateway -n hiver \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}'Connect a client
Point the SDK at the gateway address:
import { getOrCreateSandbox } from "@hiver.sh/client";
const sandbox = await getOrCreateSandbox("my-sandbox", { image: "python" }, {
gatewayUrl: "http://<gateway-ip>",
});The clients also read HIVER_GATEWAY_URL from the environment, so you can leave
the URL out of code and set it once instead.
Next: GKE