holm

Runtimes

A runtime is where a box runs. All desktop operations are the same on every runtime. Startup time, isolation, image handling, networking, and lifecycle support are different.

Two properties

Each runtime has a place and an environment.

Place Meaning
Host The box runs on the same machine as the server. The server finds the runtime at start.
Remote The box runs at a cloud vendor. You configure the runtime, because it needs an API key.
Environment Meaning
Container The box shares the host kernel, in its own namespaces.
MicroVM The box has its own kernel. The isolation is stronger, and startup is slower.
VM The box is in a full virtual machine.
Unknown The provider does not say.

The two properties are independent. For example, Docker gives a container on runc, but a microVM when its default runtime is Kata.

Available runtimes

Runtime Place Environment Notes
docker Host Container The default.
podman Host Container
nerdctl Host Container containerd.
smolvm Host MicroVM libkrun: Hypervisor.framework on macOS, KVM on Linux.
microsandbox Host MicroVM libkrun, through the msb CLI.
e2b Remote MicroVM Firecracker. Needs an API key. A source build needs the e2b feature.
vercel Remote MicroVM Firecracker. Needs a token, a team, and a project. A source build needs the vercel feature.
daytona Remote Container Needs an API key. A source build needs the daytona feature.
modal Remote Container gVisor. Needs a token ID and secret. A source build needs the modal feature.

To see the runtimes a server has, and what each can do:

holm runtime ls
curl -s localhost:8080/v1/runtimes

An MCP agent calls list_runtimes.

Select a runtime

Name the runtime when you create a box. With no name, the box goes on the server's default, which is docker unless you change it.

holm new --runtime smolvm
{ "spec": {}, "placement": { "runtime": "smolvm" } }

MCP: launch_box with runtime: "smolvm".

The server refuses a name that it does not have, and gives the names that it has. A box stays on its runtime for its full life. A fork goes on the same runtime.

From the Rust library with no server, runtime() selects a container engine:

let computer = Computer::builder().runtime("podman").launch().await?;

For a microVM or a cloud sandbox, give the builder a machine(). See examples/microvm.rs, examples/e2b.rs, examples/vercel.rs, examples/daytona.rs, and examples/modal.rs.

Host runtimes

At start, holmd offers each host runtime whose program answers. To offer fewer, set HOLM_SERVER_RUNTIMES:

HOLM_SERVER_RUNTIMES=docker,smolvm holmd

Configure an engine with its own settings where you can:

  • DOCKER_HOST or docker context point Docker at a different host.
  • default-runtime in daemon.json puts all Docker containers on gVisor or Kata.

The runtimes file

Other settings go in a TOML file. Set HOLM_SERVER_CONFIG to its path. The server reads it at start, so a change needs a restart.

default = "hardened"

[runtimes.docker]
memory = "4g"

[runtimes.smolvm]
program = "/opt/smolvm/bin/smolvm"
memory = "4g"

[runtimes.hardened]
provider = "docker"
isolation = "runsc"

[runtimes.gpu-host]
provider = "docker"
context = "gpu-1"

[runtimes.podman]
enabled = false
  • A table with no provider changes the runtime with that name.
  • A table with a provider adds a new runtime on that engine. Use this to offer one engine two times with different settings.
  • default is the runtime for a box that names none.
Field Runtimes Effect
enabled All host false hides a runtime that the server found.
memory, cpus All The default for each box. A placement can override it.
lifetime All The lifetime of a box that does not give one. Default one hour.
max_lifetime All The longest lifetime a box can ask for.
isolation docker, podman, nerdctl The OCI runtime for each box, such as runsc for gVisor.
context docker A Docker context from docker context create.
program smolvm, microsandbox The hypervisor program, when it is not on the PATH.
build_with smolvm, microsandbox The engine that builds the image. Default docker.

Write times as 24h, 90m, 3600s, or a number of seconds. The server refuses an unknown field.

Remote runtimes

A remote runtime is a vendor account with a name. You can have more than one for a vendor, such as two E2B accounts.

There are three ways to add one.

Environment, for a runtime named after the vendor:

HOLM_SERVER_SANDBOXES=e2b E2B_API_KEY=... holmd

Runtimes file, to set its lifetimes:

[runtimes.e2b]
lifetime = "4h"
max_lifetime = "24h"

CLI or API, while the server runs:

printf '%s' "$E2B_API_KEY" | holm runtime add cloud --provider e2b --api-key
holm runtime set cloud --field max_lifetime_secs=86400
holm runtime rm cloud

The CLI reads the key from standard input, or from a variable with --api-key-env. The key does not go on the command line, where ps and the shell history can show it.

A key added through the CLI or API:

  • Is encrypted before the server stores it. The server key comes from HOLM_SERVER_SECRET_KEY, or from the file at HOLM_SERVER_SECRET_FILE, which the server makes if it does not exist. With neither, the server refuses to store a key.
  • Is never returned. GET /v1/runtimes gives only the names of the secrets, such as ["api_key"].
  • Moves to the running boxes when you replace it, so they continue to work.

If the server cannot decrypt a stored key, the runtime shows state: unavailable with the reason.

The API can add only remote vendors. It cannot name a host program, socket, or engine. A self-hosted vendor endpoint added through the API must use https and a public address. The runtimes file can name any endpoint.

MCP tools cannot add a runtime, because a key given to an agent goes through its context.

Lifecycle support

Runtime Pause (memory kept) Stop (files kept) Browser profile
docker, podman, nerdctl Yes Yes Yes
smolvm No Yes No
e2b Yes No No
vercel No No No
daytona No No No
modal No No No

After a stop, a start gives a new desktop with new ports and a new viewer URL.

A browser profile needs a Docker volume, so only container runtimes support --profile. On other runtimes, use save_state and load_state to move a login between boxes.

On E2B, memory and CPUs are set when the template is built, not when the box is created.

On E2B, every port of a box refuses a request without the sandbox's traffic token, so a browser cannot open the viewer URL directly: watch and take over through holmd. Add a runtime with --field public_traffic=true to open the ports and get direct viewer and takeover URLs back.

On E2B and Vercel, page tools reach Chromium through a bridge in the box. See Control modes.

On Vercel, each port has a public URL with no gate of its own, so the viewer and takeover URLs open in a browser and need their own token. A box has at most 6 screens, because Vercel publishes at most 14 ports. Vercel sets memory from the vCPU count: see Vercel Sandbox.

A runtime says what it can do in GET /v1/runtimes/{name} under can. The server refuses a request that the runtime cannot do before it starts the box.

Images

Each runtime gets its image in a different way:

  • Container engine: the server builds the image on the host from the bundled Dockerfile.
  • MicroVM: a hypervisor cannot read an engine's images. The server builds the image with build_with, then gives it to the hypervisor one time. Later boxes start from that copy.
  • E2B: the server makes an E2B template from the bundled image, uploads the files, and waits for the build. It makes one template for each box specification.
  • Vercel: the server builds the bundled image, or an image directory, in a builder sandbox at Vercel and pushes it to Vercel Container Registry. A later box finds the tag there and starts from it.
  • Daytona: the server sends the bundled image, or an image directory, to Daytona as a Dockerfile with the copied files written inline. Daytona keeps each build by the Dockerfile's content.
  • Modal: the same Dockerfile, sent as lines. Modal keeps each image by its recipe.

Each set of applications and packages is a different image. The first box with a new set waits for a build. To build before a box needs it:

holm image build smolvm --app vscode
holm image ls

Lifetime

A box lives one hour unless it asks for a different time. Set lifetime in the runtimes file to change the default, and max_lifetime to limit what a box can ask for. The server refuses a request above the limit, and gives both numbers.

Give a box its own lifetime:

  • CLI: --ttl MINUTES and --idle MINUTES
  • REST: expires_after_secs and idle_timeout_secs in the placement. Values under 60 seconds are refused.
  • MCP: ttl_minutes and idle_minutes on launch_box

On a cloud runtime, the vendor ends the box at its deadline, so no server has to be up to do it. With no idle timeout, the deadline is the lifetime of the box, and a box that nobody drives stays until then. With an idle timeout, each command through the server moves the deadline, and the vendor ends the box when no command came for that time. A person who only watches the screen does not move the deadline.

Restart

When holmd restarts, it takes back each box that it recorded, through the runtime in the record. Then it scans each runtime for boxes that an earlier server labeled. A box whose runtime is missing shows unreachable with the reason, and its record stays.

Display servers

Separate from the runtime, a box runs X11 (the default) or Wayland:

holm new --wayland
Display Components
X11 Xvfb, fluxbox, x11vnc, ImageMagick, xdotool
Wayland Headless sway, wayvnc, grim, and a virtual pointer and keyboard for each screen

The desktop API is the same for both. On Wayland, cursor cannot read the pointer after a person moves it. It returns Unsupported until the agent moves the pointer again.