Run Docker Compose stacks in a VM
The docker tier of the Linux image, a VM with dockerd up at boot, and `cua sb bake`, which saves a running Compose stack as a checkpoint that forks start from.
Some workloads are a whole Docker Compose stack, for example an agent container, MCP servers and a
grader. They need a real Linux kernel: dockerd, kernel modules, iptables. The docker tier of the
Linux image is a VM built for that, and cua sb bake turns a running stack into a checkpoint. Every
fork of the checkpoint starts with the containers already running.
The docker tier#
ghcr.io/trycua/linux:24.04-docker-disk is the slim Linux image plus the Docker engine, as a VM.
It contains:
docker-ce,containerdand the compose and buildx plugins, from Docker's signed apt repository at pinned versions.docker-composeis the compose plugin under its standalone name.- dockerd and containerd, which start at boot.
docker-readywaits until dockerd answers and prints how long after boot that took. - The
overlayandbr_netfilterkernel modules, loaded at boot.modprobeworks for other modules. iptablesandip6tables(the nf_tables backend) for Docker's rules and your own, includingnatREDIRECTand-m physdev.- The slim tier's desktop and cua-spacesd, so
exec, file transfer, port forwards and screenshots work as in any Linux Space.
Name it by tier, or pass the reference:
cua sb create linux --tier docker --name dockerhost
cua sb exec dockerhost docker-ready
cua sb exec dockerhost docker run --rm mirror.gcr.io/library/busybox:1.37 echo hellofrom cua import Image
image = Image.linux(tier="docker") # the docker tier's VM diskThe tier is VM-only: a container Space cannot run dockerd. Its container rootfs (24.04-docker)
exists only as the source the disk is built from.
Build it locally with libs/images/build.sh linux --target docker --outputs rootfs,disk. That
writes ~/.cache/cua-images/linux-docker/<arch>/disk.img, and cua sb create <that path> --kind vm
runs it.
Bake a Compose stack into a checkpoint#
cua sb bake (the CLI) and Sandboxes.bake (the SDK) do the following:
- Creates a local VM of the docker tier and waits for dockerd.
- Uploads the project directory (and any
--loadimage tarballs). - Runs
docker compose up --wait, which waits for every healthcheck. - Checks the stack: every service is running, and every container is healthy (or running, when it
has no healthcheck) and has not restarted. It then runs each
--healthcommand until it exits 0. - Runs each
--runcommand. - Saves a live memory checkpoint of the VM, with the containers running.
- With
--verify-fork, forks the checkpoint once and checks the stack again in the fork. - Deletes the bake Space (
--keepkeeps it).
cua sb bake ./world --checkpoint world-v1 --memory 4GB --verify-fork \
--health 'curl -sS -o /dev/null http://127.0.0.1:18000/mcp'
cua sb fork world-v1 rollout-1
cua sb exec rollout-1 docker psThe same bake from the SDK, in any language (Sandboxes.bake):
import cua
c = cua.embedded()
sbx = c.sandboxes()
baked = await sbx.bake(cua.BakeOptions(
project_dir="./world",
checkpoint="world-v1",
memory_mb=4096,
health=["curl -sS -o /dev/null http://127.0.0.1:18000/mcp"],
verify_fork=True,
))
print(baked.dockerd_ready_s, baked.compose_up_s, baked.checkpoint_s, baked.containers)
rollout = await sbx.fork("world-v1", "rollout-1")Forks restore the saved memory, so their containers are already up: the processes, open ports and in-memory state are those of the moment of the checkpoint. Each fork is still a new machine, with its own token, keys and machine id.
| Option | What it does |
|---|---|
--file, -f | Compose files, relative to the project (repeatable). Default: compose's own lookup. |
--project-name | The compose project name. Default: the directory name. |
--dest | Where the project lands in the guest. Default: /opt/cua-bake/<project>. |
--load TAR | A docker save tarball to load before compose up, for images the guest cannot pull. |
--health CMD | A shell command that must exit 0 before the checkpoint (retried until --health-timeout). |
--run CMD | A shell command to run after the stack is healthy, for state that forks should start with. |
--image, --tier | Another image. Default: linux --tier docker. |
--cpu, --memory | The bake Space's size, which forks keep. |
--verify-fork | Fork once and check that the stack is live in the fork. |
--json | Print the summary as JSON: timings, the containers and the checkpoint. |
Each CLI option is a field of BakeOptions in the SDK (compose_files, project_name, dest,
image_tarballs, health, health_timeout_s, run, image, cpus, memory_mb, verify_fork,
keep).
Guest commands run as root in the project directory. The summary reports the time from boot until dockerd was ready, the time of each step, and the containers.
Measured#
On a Mac Studio (M2 Max, HVF), baking libs/images/linux/tests/compose-world (an agent container,
an MCP server built in the guest, and a grader) in a 4 GB, 2-vCPU VM:
| Step | Time |
|---|---|
Boot to dockerd ready (guest kernel start to docker.service ready) | 4.1 s |
compose up --wait (pull python:3.12-slim, build the MCP image, healthchecks) | 14.5 s |
| Stack check | 0.15 s |
| Checkpoint | 0.8 s |
| Fork of the checkpoint, containers already running | 1.4 s |
The checkpoint takes 1.6 GB on disk: 1.37 GB of memory and device state, and a 253 MB disk layer.
A VM Space of the tier starts in about 1.9 s once the image's first boot has left its golden snapshot (a cold first boot takes about 14.5 s).