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+)
  • kubectl configured to target your cluster
  • helm (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/hiver

The 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.

NameDescriptionContainerMicroVM
claudeClaude Codehiversh/claude:latesthiversh/claude:latest-microvm
codexOpenAI Codexhiversh/codex:latesthiversh/codex:latest-microvm
copilotGitHub Copilothiversh/copilot:latesthiversh/copilot:latest-microvm
openclawOpenClawhiversh/openclaw:latesthiversh/openclaw:latest-microvm
antigravityAntigravityhiversh/antigravity:latesthiversh/antigravity:latest-microvm
browserResident Chrome, driven over CDPhiversh/browser:latesthiversh/browser:latest-microvm
pythonPython 3.13 on Alpinehiversh/python:3.13-alpinehiversh/python:3.13-alpine-microvm
nodeNode.js on Alpinehiversh/node:alpinehiversh/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=3

replicas: 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:

values.yaml
sandboxServices:
  python:
    replicas: 2
    maxConcurrentLaunches: 4
    resources:
      requests:
        cpu: "1"
        memory: 512Mi
      limits:
        cpu: "4"
        memory: 4Gi
helm upgrade hiver hiver/hiver --values values.yaml

Isolation (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=container

Run 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. Implies memBackend: uffd on its own — Firecracker cannot map a hugetlbfs-backed snapshot through the plain file-mapped backend — so there's no need to set memBackend alongside it.
  • hugePagesLimit — overrides the size of the pod's hugepages-2Mi (or -1Gi) resource limit, which otherwise defaults to resources.requests.memory (one guest's worth). Hugepage memory is a hard, non-reclaimable allocation kept separate from the pod's regular memory limit, 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).
values.yaml
sandboxServices:
  claude:
    hugePages: "2M" # implies memBackend: uffd
    hugePagesLimit: 2Gi # e.g. 4 concurrent VMs x 512Mi guest each

Enabling hugePages also requires two things outside the chart, or the guest fails to boot:

  1. 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/meminfo but stays 0 in kubectl get node -o jsonpath='{.status.capacity}', and pods can never request it.
  2. Any existing snapshot re-captured. hugePages is 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.
i
Use 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:

  1. 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/arm64

    Add --microvm for the microVM variant (e.g. myrepo/mytool:1.0.0-microvm); build only the variant(s) your pool's isolation uses, and push to a registry the cluster can pull from.

  2. Add a key under sandboxServices:

values.yaml
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: 4Gi

image 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 gateway
  • hiver-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   2m

Sandbox 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-sandbox

Get 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:

index.ts
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