A cloud sandbox runs a box at a vendor, not on your host. The vendor supplies the machine, and you need no container runtime. This guide sets up E2B. For the other vendors that are built in, see Vercel Sandbox, Daytona, and Modal. At the end, this guide shows how to add a different vendor.
For how remote runtimes compare with host runtimes, see Runtimes.
What works
| Works | Not yet supported |
|---|---|
| Screenshots, mouse, keyboard, and waits | Browser profiles (--profile) |
| Windows, applications, and native widgets | Stop. Pause works. |
| Files, commands, and clipboard | Memory and CPUs for each box. They are set when the template is built. |
Page tools and holm cdp, with a template built from the current image, also after a server restart. See Control modes. |
|
Human control, through the MCP Apps live screen. Viewer and takeover URLs open in a browser only when the runtime sets public_traffic. |
|
| Pause and resume | |
Network policy (--no-network) |
1. Get an API key
Make an account at E2B and create an API key. Keep it out of shell history and out of files that go into git.
Release builds of holm and holmd include E2B. For a source build, add the e2b feature:
cargo install --path . --locked --features e2b2. Add E2B to the server
There are two ways. Use one.
From the environment. The runtime is named e2b:
E2B_API_KEY=... HOLM_SERVER_SANDBOXES=e2b holmdAt run time, with the CLI. You choose the name. The server keeps the runtime in its store:
printf '%s' "$E2B_API_KEY" | holm runtime add cloud --provider e2b --api-key
holm runtime lsThe CLI reads the key from standard input, or from a variable with --api-key-env E2B_API_KEY. The key does not go on the command line. To keep the key, the server needs HOLM_SERVER_SECRET_KEY or HOLM_SERVER_SECRET_FILE, and durable storage to keep the runtime after a restart. See Remote runtimes.
| Environment | CLI or API | |
|---|---|---|
| Name | e2b |
Any |
| More than one account | No | Yes |
Fields such as image and public_traffic |
No | Yes |
| Lifetimes | In the runtimes file | --field |
| Change the key | Restart the server | holm runtime set NAME --api-key |
3. Create a box
BOX=$(holm new --runtime cloud)MCP: launch_box with runtime: "cloud". REST: "placement": {"runtime": "cloud"}.
To make E2B the default, set default = "cloud" in the runtimes file.
The server refuses --memory and --cpus for an E2B box, because E2B sets them when it builds the template.
Templates
E2B starts a sandbox from a template, not from a container image.
Automatic templates
When a box needs a template that does not exist, the server makes one:
- It converts the built-in Dockerfile, with the spec's applications and packages, into E2B build steps.
- It uploads the files that the Dockerfile copies.
- It starts the build at E2B, with 2 CPUs and 2048 MiB of memory, and waits for it to finish.
- It records the template. Later boxes with the same spec use it.
Each different set of applications and packages is a different template. The first box with a new set waits for the build. To build it before a box needs it:
holm image build cloud --app vscode
holm image ls cloudA template that you build
Build a template yourself when you want to keep it, change its size, or see what E2B receives. Write the build context, then use the E2B CLI:
python3 crates/holm-core/images/context.py \
crates/holm-core/images/desktop \
/tmp/e2b-ctx \
--for e2b
e2b template create holm-desktop \
-p /tmp/e2b-ctx \
-d Dockerfile \
-c "/usr/local/bin/holm-desktop" \
--ready-cmd "true" \
--cpu-count 2 \
--memory-mb 2048The conversion removes the Dockerfile instructions that E2B refuses or replaces.
Then tell the runtime to use your template for all boxes:
holm runtime set cloud --field image=<template-id>Lifetimes
E2B limits how long a sandbox can live. The limit depends on your E2B plan. The server does not know your plan, so set the limit on the runtime:
[runtimes.e2b]
lifetime = "1h"
max_lifetime = "24h"For a runtime added with the CLI, use --field lifetime_secs=3600 --field max_lifetime_secs=86400.
The server refuses a box that asks for a lifetime above max_lifetime, and gives both numbers. While a box is in use, the server extends its deadline at E2B.
Viewers
E2B publishes each port of a sandbox at its own URL. The server creates each sandbox as secure, so each port needs E2B's traffic token in a request header. A browser cannot send that header.
| Viewer | Works |
|---|---|
| MCP Apps live screen | Yes. It goes through holmd, which sends the traffic token. |
| Viewer and takeover URLs in a browser | Only when the runtime has public_traffic=true |
With public_traffic=true, anyone who has a port's URL can reach that port without the traffic token. The viewers still need their own token: the server always sets token access on a remote box with no other viewer access.
holm runtime set cloud --field public_traffic=trueThe environment runtime (HOLM_SERVER_SANDBOXES=e2b) takes no fields, so it cannot set public_traffic. Add the runtime with the CLI instead.
Self-hosted E2B
For E2B that runs on your own infrastructure:
- Environment runtime and the library: set
E2B_DOMAINto your domain. The default ise2b.app. - CLI or API runtime: set the
endpointfield. Through the API, it must behttpsand a public address.
printf '%s' "$E2B_API_KEY" | holm runtime add onprem --provider e2b --field endpoint=https://e2b.example.com --api-keyUse E2B from Rust
The library uses E2B with no server. Build with the e2b feature, and set E2B_API_KEY.
use holm::sandboxes::e2b::{self, cloud::Cloud};
use holm::{Auth, Computer, X11Profile};
let (machine, profile) = e2b::pair(Arc::new(Cloud::from_env()?), Arc::new(X11Profile));
let computer = Computer::builder()
.machine(Arc::new(
machine
.public_viewer(true)
.expiring_after(Duration::from_secs(15 * 60)),
))
.profile(profile)
.auth(Auth::Token)
.launch()
.await?;| Setting | Effect |
|---|---|
.image("<template-id>") |
Start from this template. With no image, the library builds a template from the built-in image, as the server does. |
public_viewer(true) |
Give the box a viewer URL. The launch needs Auth::Token or Auth::Password. |
public_traffic(true) |
Let a browser open the ports without the traffic token. |
expiring_after(duration) |
How long the sandbox lives with no activity. Activity extends it. |
Cloud::at(key, domain) |
Use a self-hosted E2B. |
Run the examples:
export E2B_API_KEY=...
cargo run --features e2b --example e2b -- <template-id>
cargo run --features e2b --example e2b_takeover -- <template-id> "search text"Add a different vendor
A vendor adapter implements the RemoteApi trait. Its required methods create, find, and kill a sandbox, run a command, and read and write a file. The other methods have defaults that claim nothing, such as pause and keep_alive. Override them when the vendor can do more, and say so in can().
RemoteMachine adds the lifetime, naming, and keep-alive behavior that all vendors share. RemoteProfile maps the vendor's port URLs and removes what a remote sandbox cannot expose.
use holm::sandboxes::remote::{self, RemoteApi};
use holm::{Computer, X11Profile};
let (machine, profile) = remote::pair(Arc::new(MyVendor::new()), Arc::new(X11Profile));
let computer = Computer::builder()
.machine(Arc::new(machine))
.profile(profile)
.launch()
.await?;examples/custom_sandbox.rs is a complete adapter that runs sandboxes as local Docker containers. Run it before you connect a real API:
cargo run --example custom_sandboxFor tests with no account or network, use holm::testing::ScriptedRemote.
holmd builds only the vendors that it knows. To offer your own vendor from a server, build a server with your own implementation of the holm_server::runtimes::Vendors trait, and pass it to AppState::serving.