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/runtimesAn 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 holmdConfigure an engine with its own settings where you can:
DOCKER_HOSTordocker contextpoint Docker at a different host.default-runtimeindaemon.jsonputs 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
providerchanges the runtime with that name. - A table with a
provideradds a new runtime on that engine. Use this to offer one engine two times with different settings. defaultis 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=... holmdRuntimes 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 cloudThe 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 atHOLM_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/runtimesgives 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 lsLifetime
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 MINUTESand--idle MINUTES - REST:
expires_after_secsandidle_timeout_secsin the placement. Values under 60 seconds are refused. - MCP:
ttl_minutesandidle_minutesonlaunch_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.