Choose where sandboxes run

Run sandboxes on this machine, in your own AWS, Google Cloud, Hetzner or Modal account, or on E2B, Daytona or Modal. Check the host, pick a runtime, and set the default.

A sandbox runs in one place. Pick it with on= in the SDK (local= in cua-sandbox) or --on in the CLI. Leave it out and the sandbox runs on this machine.

WhereonWhat you set up
This machinelocal (the default)A container engine, QEMU or Lume, with cua runtime setup
Your own cloudaws, gcp, hetzner, modalcua cloud connect
Another providere2b, daytona, modalThe provider's API key, and a cua build with --features contrib
A machine that runs cua-spacesddirect:<host:port>Attach to or run cua-spacesd

To change where sandboxes run by default, see Change the default.

This machine#

Local sandboxes run on this machine with no account. The SDK picks the kind and runtime from the image and starts it itself.

ImageRuntime (auto)
Container image (any OCI rootfs, Image.linux(), python:3.12-slim)gvisor when installed, else runc, on Docker or Podman
Linux or Windows VM (-disk containerDisk, kind="vm", qcow2, ISO)qemu: KVM on Linux, HVF on macOS, WHPX on Windows, TCG otherwise
macOS VM (Image.macos())lume, Apple silicon only (the SDK starts lume serve)
Linux arm64 VM on Apple silicon without QEMUlume
Any OCI image as a microVM (kind="microvm", never picked by auto)firecracker on Linux with KVM; on a Mac vz (Apple silicon, macOS 14 or later) when installed, else containerization (macOS 26)

Check and set up the host#

cua runtime doctor              # what this host can run (read-only)
cua runtime setup --dry-run     # the install plan
cua runtime setup               # install what is missing; or name qemu, lume, container, runsc, firecracker

cua doctor checks the host, an image and a sandbox's guest in one report.

A runtime installed separately from cua (an external runtime provider) adds its own row to cua runtime doctor and its own --runtime word.

Not supported locally: macOS containers, macOS images that are not Lume images, and hosts with no container engine (install Colima or Docker Engine). Image.windows() is amd64 and runs under slow TCG emulation on Apple silicon.

Pick a kind or runtime#

cua sb create linux --name dev                       # a gVisor container
cua sb create linux --kind vm --name dev-vm          # its -disk variant under QEMU
cua sb create linux --runtime runc --name dev-runc   # a container without gVisor
cua sb create macos:tahoe --name mac                 # a Lume VM
cua sb rm dev

--runtime takes auto, gvisor or runc for containers, qemu or lume for VMs, and firecracker, vz or containerization for microVMs (--kind microvm); a runtime implies its kind. See Space kinds. A combination that does not exist fails with InvalidPlacement and lists the valid values:

invalid placement: runtime qemu runs vm sandboxes, not container ones locally; valid runtime: auto, gvisor, runc

In code, pass kind= and runtime= (Sandbox.create(image, kind="vm", runtime="qemu")); cua-sandbox still accepts a Runtime object (runtime support).

What differs locally#

  • The host's architecture. Others run emulated and slowly.
  • Services and public URLs are served on loopback (public URLs by the cua daemon, which the SDK starts).
  • Sidecars need runtime="runc" where gVisor would run.
  • Image layers build into your container engine before boot. VM images apply them after boot through cua-spacesd.
  • A sandbox runs until you delete it. keep_alive raises.
  • Any OCI image is a full Space on containers and microVMs. An image that carries no cua-spacesd (alpine:3.20, node:20, python:3.12-slim) gets a static one put in, started beside the image's own command with its user and working directory, so exec, files and ports work. spacesd=False (--no-spacesd on the CLI) runs the image as it is. A VM needs a Cua disk image: for a bare OCI image use kind container or microvm, or cua build FROM a Cua image.
  • VM images without cua-spacesd fall back to the runtime's agentless path (QMP, VNC, SSH or ADB) where there is one.

Windows on ARM#

A Windows 11 on ARM guest (os="windows", arch="aarch64") runs on the QEMU runtime of an arm64 macOS or Linux host. Windows 11 needs 4 GB: a guest started with less is given 4096 MB.

TPM. The guest gets a persistent software TPM 2.0 automatically (not on a Mac). It needs swtpm on the host's PATH:

sudo apt-get install swtpm         # Debian, Ubuntu

Secure Boot firmware. The guest needs AAVMF_CODE.secboot.fd and AAVMF_VARS.ms.fd, both in one directory (else QEMU's generic edk2 firmware). cua looks, in order, at CUA_QEMU_AAVMF_DIR (when set, nowhere else), ~/.cua/firmware/aavmf, the QEMU prefix's share/AAVMF, then /usr/share/AAVMF. On Linux:

sudo apt-get install qemu-efi-aarch64

Homebrew's QEMU does not ship this pair. On macOS, download Debian's architecture-independent qemu-efi-aarch64 package, unpack it, and copy the two files (following links) into the cua directory:

mkdir -p ~/.cua/firmware/aavmf
cp -L usr/share/AAVMF/AAVMF_CODE.secboot.fd usr/share/AAVMF/AAVMF_VARS.ms.fd ~/.cua/firmware/aavmf/

Network. Windows 11 on ARM has no inbox virtio-net driver, so a guest you install by hand has no network in Setup. Finish OOBE offline, then install NetKVM from the virtio-win ISO (NetKVM\w11\ARM64).

Fork and checkpoint need the VM stopped, so its TPM state is copied consistently. A checkpoint with TPM state stays on the host that made it: it is not exported or imported.

Many VMs on one machine#

Local QEMU VMs (kind="vm") are admitted by what they really use, not by their reserved size. VMs started from one snapshot (warm starts and forks) share its unwritten memory, which is counted once. A new VM starts when:

  • the VMs together stay under CUA_VM_MAX_RSS_FRACTION of physical memory (default 0.8);
  • the machine keeps CUA_VM_MIN_AVAILABLE_MB available for everything else (default an eighth of memory, 1 to 8 GiB) and its memory pressure is not critical;
  • their vCPUs stay within CUA_VM_VCPU_OVERCOMMIT times the machine's CPUs (default 8, because rollouts and agents mostly wait).
  • on a Mac, the hypervisor has a free VM slot. macOS runs at most 128 VMs at once (sysctl kern.hv.max_address_spaces), counting every QEMU and Apple Virtualization VM on the machine.

Past a limit, the create fails with HostCapacity and nothing starts. The message names the limit and by how much it was exceeded. With on="auto", the next machine is tried. Hosts report the same measurements to placement, so on="auto" sends a VM where it fits.

For RL rollouts and agent episodes, pass profile="rollout" (cua sb create --profile rollout): 1 vCPU, 1 GiB and no desktop. A VM then gets no display or input hardware. Exec, files, ports and processes work as usual, but screenshots and input do not. Explicit cpus and memory_mb still apply.

On Linux hosts, a VM booted without a snapshot also reports the memory its guest frees (virtio-balloon free page reporting), and the host takes it back. On macOS, QEMU's discard leaves freed pages charged to QEMU, so this is off there (CUA_QEMU_FREE_PAGE_REPORTING=1 turns it on).

Images, VM disks and logs take disk space under ~/.cua. See Disk usage and cleanup.

The Linux runtime on a Mac#

A Mac with no container engine runs Linux Spaces and jobs in one Linux VM that cua sets up and boots itself. It is shared by every Linux Space of your cua home. By default it may use up to 75% of the Mac's memory (at most 32 GiB, at least 4 GiB) and up to 4 CPUs; the Mac backs that memory only as the guest touches it. cua config set runtime.linux_memory_gib and runtime.linux_cpus (or CUA_RUNTIME_LINUX_MEMORY_GIB and CUA_RUNTIME_LINUX_CPUS) set a size; it applies when the VM next starts. It stops when none of its Spaces runs:

  • After the last Space. The VM stops about two minutes after the last Linux Space is deleted or stopped (CUA_RUNTIME_LINUX_IDLE_SECS, default 120), and boots again on the next one.
  • With the daemon. cua daemon stop stops the VM too. If Linux Spaces still run, the VM stays up and the command says so: the Linux runtime keeps running for 2 Spaces; it stops when they do. A daemon that starts and finds the VM running with no Space in it stops it, and checks again every five minutes. A Space the host keeps warm on purpose (pool.warm) holds the VM up.
  • Open files. A VM that shares host folders holds a host file descriptor for each file its guest has cached, and macOS has one table of about 262,000 for the whole machine. cua keeps that bounded: the VM gives the guest's cached files back after each job and when the count grows, so the count follows the work in progress instead of every file ever read. Before it starts a VM or a job, cua checks that at least 10 % of the table is free (CUA_FILE_TABLE_MIN_FREE_PCT, 0 turns the check off). When less is free it first stops this home's idle runtime VM, and if that is not enough it refuses with HostCapacity, naming the processes that hold the most descriptors. Fix it by stopping Spaces or VMs you do not use.

The Linux runtime on Windows#

A PC with no container engine runs Linux Spaces in a WSL 2 distribution that cua imports and starts itself; nothing is installed first, not Docker Desktop. The first Linux Space downloads a pinned Ubuntu 24.04 base rootfs (30 MB, SHA-256 checked) and Ubuntu's Docker packages (about 120 MB), shown as the create's progress.

  • Its own distribution. cua-runtime-<tag> (one per cua home, so a released Cua Spaces and Cua Spaces Local never share), with its disk under <cua home>\runtimes\linux\wsl. cua never starts, stops or changes any other distribution or Docker Desktop. It cannot see your Windows drives, so a Space cannot mount a Windows folder yet.
  • A pipe, not a port. Windows reaches Docker at the named pipe \.\pipe\cua-engine-<tag>, served by the cua daemon for your user only.
  • Idle stop. The runtime stops about two minutes after the last Linux Space (CUA_RUNTIME_LINUX_IDLE_SECS). Its memory and processors are WSL's: set them in %UserProfile%\.wslconfig, which cua never edits.
  • When WSL 2 cannot run. cua runtime doctor and New Space say what to turn on: the Windows features Windows Subsystem for Linux and Virtual Machine Platform (wsl --install --no-distribution as administrator, then restart), or virtualization in the firmware.
  • Settings. runtime.linux works as on a Mac (auto, builtin, system). Remove host setup deletes the distribution and its disk.
  • Network policy. WSL 2's kernel has no nftables bridge family, so policy is enforced on routed traffic only (internet, LAN, the PC, cloud metadata, other trees); Spaces of one tree can reach each other directly, and a deny of another Space of the same tree is refused. cua runtime doctor says so.
  • Today. Spaces run under runc (the Mac adds gVisor).

microVMs on Windows#

--kind microvm runs in QEMU on the Windows Hypervisor Platform: hardware acceleration only, never software emulation. cua runtime doctor and New Space say what is missing: the Windows feature (cua runtime setup microvm turns it on through one administrator prompt, then restart), QEMU 9 or later (installed with winget), or virtualization in the firmware. Checkpoints, forks and warm pools work on the VM's disk: Windows does not let QEMU save a running VM's memory, so a restore or fork boots the saved disk (about four seconds, or half a second from a ready standby of the checkpoint); claiming a pooled VM is as fast. QEMU's network enforces no rules, so a microVM has no network.

Your own cloud#

Sandboxes and Spaces can run in a cloud account you own: --on aws (EC2), --on gcp (Compute Engine), --on hetzner (Hetzner Cloud servers) or --on modal (Modal Sandboxes). The same images, SDK calls and cua-bench runs work there, each sandbox dials out to the cua.ai relay as a machine of your account, and you pay the cloud directly. Connect one with cua cloud connect; Use your own cloud covers what you need, what Cua creates there, costs, notes for each cloud and cleanup.

Other sandbox providers#

The same images (the canonical ghcr.io/trycua/linux, or any registry image) also run on third-party sandbox platforms. The location is one more --on word: --on e2b, --on daytona, --on modal. Everything above the location works unchanged: services, sb.spacesd(), cua doctor and cua-bench.

Your own cloud needs no provider key, and its sandboxes join the cua.ai relay. Providers are opt-in. The default cua build knows the words and tells you how to get the provider. A build with --features contrib includes them. Modal works both ways: connect it as your own cloud and its sandboxes join the relay; with only a Modal key, the sandbox uses Modal's own tunnels.

Set up a provider#

cargo install --locked --path libs/cua/crates/cua-cli --features contrib
cua auth provider set e2b          # reads E2B_API_KEY from stdin, never echoed
cua auth provider ls               # where each key comes from, never the value

An environment variable wins over a stored key: E2B_API_KEY, DAYTONA_API_KEY, or MODAL_TOKEN_ID and MODAL_TOKEN_SECRET.

Modal documents no HTTP API, so --on modal goes through Modal's official Go SDK in a small helper, cua-modal-helper. Build it once and put it next to cua or on PATH (or point CUA_MODAL_HELPER at it):

(cd libs/cua/crates/cua-contrib/modal-helper && go build -o ~/.local/bin/cua-modal-helper .)

Create a sandbox on a provider#

cua sb create linux --on e2b --name demo
cua doctor e2b:demo --strict
cua sb delete e2b:demo

The ref is <provider>:<name> (e2b:demo, daytona:demo). --ttl sets the platform's lifetime backstop (default one hour).

from cua_sandbox import Image, Sandbox
 
async with Sandbox.ephemeral(Image.linux(), on="daytona") as sb:
    await sb.files.write_text("/tmp/hello.txt", "hello from daytona")
    print(await sb.files.read_text("/tmp/hello.txt"))  # hello from daytona

cua-bench takes the same word: cb run task <task> --on e2b.

What happens to the image#

The SDK resolves and pins the image first, then maps it onto the platform:

ProviderImage stepCached byEntrypoint
E2Ba template built fromImagedigest, start command, shaperuns once at build (E2B snapshots it); the SDK installs the cua-spacesd token
Daytonaa snapshot from imageNamedigest, entrypoint, shaperuns in every sandbox, with the token in its environment
ModalFROM the pinned image, ENTRYPOINT []Modal's image cachethe image's ENTRYPOINT and CMD as the command, with the token in its environment

A moved tag gets a new template. Requests a provider cannot run fail before any API call, with a typed error: VM variants (--vm), images without an amd64 build, Windows and macOS images, network="none", sidecars, and (Daytona, Modal) private registries. Modal tunnels are fixed at create: declare every port with --port.

Ports and services#

Declared services and cua-spacesd (3211) are reachable through the provider's HTTPS URLs. sb.service(name).url() and sb.tunnel.forward(port) return them. Ports are public on the provider's proxy. cua-spacesd authenticates with its own token, so keep your other services authenticated too.

Support matrix#

Each provider's lane runs nightly: it creates ghcr.io/trycua/linux:24.04, runs cua doctor <ref> --strict --json inside it, and generates this table from the latest reports. A regression shows up here on the next run.

ProviderKindVerdictdisplayinputstreamingaudiofilesprocessteleporttunnelsmcpauthMeasured
e2bcontainernot measured----------never
daytonacontainernot measured----------never
modalcontainernot measured----------never

full: the desktop works (display, input through cua-driver, streaming, files, auth). container-only: cua-spacesd answers but the desktop does not. headless-only: no cua-spacesd answered. not measured: no run yet.

Change the default#

Without --on (or on= / local=) a sandbox runs on this machine. To default to your own cloud instead, set default.on:

cua config set default.on aws   # written to ~/.cua/config.toml
cua config list                 # each setting, its value and its source

The first match wins:

  1. --on, on= or local=
  2. CUA_DEFAULT_ON (one shell or CI job)
  3. default.on in $CUA_HOME/config.toml
  4. local

default.kind and default.runtime (CUA_DEFAULT_KIND, CUA_DEFAULT_RUNTIME) work the same way, and cua doctor shows each default with its source. Switch back with cua config unset default.on.

Choose what runs each kind#

Each kind of Space has its own setting: runtime.container, runtime.vm and runtime.microvm. Each is auto (the default), builtin (Cua's own, set up the first time it is needed) or system (only what is already installed on this machine; nothing is downloaded). With system, runtime.<kind>_path names the program to use instead of the one found on PATH, for example another Docker:

cua config set runtime.container system
cua config set runtime.container_path /opt/homebrew/bin/podman
cua config unset runtime.container_path   # back to the one found

The Cua app's Settings, Advanced shows the same settings, one row per kind, with the programs it found and a check of the path. cua and the daemon read the same values, so they use the same program. The older runtime.linux and runtime.lume still work: they are runtime.container and runtime.vm.