# How to build a custom sandbox image from a list of packages Call `runtime.images.build({ name, recipe: { pip, apt, npm } })` once, then create each sandbox with `image: name`; the build is free. **On Runtime building an image costs nothing and uses no trial hours, and the free trial stores your first three images free for as long as you keep them.** After that a stored image costs $0.08 per decimal GB per 30-day month on its whole file (rate in force since 23 September 2026, [pricing](/docs/pricing#snapshots-images-and-volumes)). Every sandbox started from it has the packages already installed, instead of spending its first minute on `pip install`. ## Build it and start from it ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); const image = await runtime.images.build( { name: "data", recipe: { pip: ["pandas"], apt: ["jq"] } }, { onLog: (line) => console.log(line.text) }, ); await using sbx = await runtime.sandboxes.create({ image: "data" }); console.log( image.version, (await sbx.exec("python3 -c 'import pandas; print(pandas.__version__)'")).stdout, ); ``` ```python check from withruntime import Runtime runtime = Runtime() image = runtime.images.build(name="data", recipe={"pip": ["pandas"], "apt": ["jq"]}, on_log=lambda line: print(line["text"])) with runtime.sandboxes.create(image=image["id"]) as sbx: print(sbx.exec("python3 -c 'import pandas; print(pandas.__version__)'").stdout) ``` ```bash no-run img=$(runtime image build --pip pandas --apt jq --name data) runtime sandbox create --image "${img}" ``` `build` streams the log, waits until the image is ready, and throws with the build's own error if a step fails. `images.create` queues the same build and returns at once, for a job that should not wait. ## What a recipe takes | Field | What it does | | ---------- | ----------------------------------------------------------------------------- | | `base` | `runtime` by default, Runtime's image with Python, Node and Bun; or any image | | `apt` | Ubuntu packages | | `pip` | Python packages | | `npm` | Node packages | | `commands` | Shell commands to run in the build | | `files` | Small files written into the image, 256 files and 1 MiB in all | | `env` | Environment variables set in the image | | `workdir` | The working directory | The CLI covers the common case with `--pip`, `--apt` and `--npm`; for `commands`, `files` or `env`, use an SDK or a [Dockerfile](/how-to/build-an-image-from-a-dockerfile). ## Make it start a server too An image can say what runs when a sandbox starts from it and when that sandbox counts as ready. The create then answers only once the server listens. ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build({ name: "api", recipe: { pip: ["fastapi", "uvicorn"] }, start: { command: "uvicorn main:app --port 8000", readyPort: 8000, readyTimeoutSeconds: 60 }, }); const sbx = await runtime.sandboxes.create({ image: "api" }); console.log(sbx.info.start); // { state: "ready", readyMs: ... } ``` `start.state` is `ready`, `started` (no ready check), `timeout` (the check did not pass within `readyTimeoutSeconds`, 60 by default, at most 300) or `exited` (the command ended first) ([start and ready commands](/docs/images#start-and-ready-commands)). ## Recipes worth baking - **R, Java or Go for the code interpreter.** Otherwise each sandbox installs them the first time it uses them: about 35 seconds for Java or Go and 90 for R, measured on 23 September 2026. Put them in `apt` to skip that. - **Docker.** `commands: ["enable-docker --no-start"]` gives every sandbox Docker Engine, Buildx and Compose, started on the first `docker` command ([Docker in a sandbox](/how-to/run-docker-in-a-sandbox)). - **Your agent's tools.** A coding agent's CLI, linters and test runners, so each task starts working at once ([coding agent sandbox](/use-cases/coding-agent-sandbox)). ## Limits | Limit | Value | | -------------- | ---------------------------------------------------------- | | Builds at once | 1 until the account has bought credit, then 4 | | Build machine | 1 to 8 vCPUs (2 by default), 1 to 16 GiB of memory (4 GiB) | | Scratch disk | 1 to 32 GiB (4 GiB by default) | | Build time | 60 seconds to 1 hour (30 minutes by default) | | Image size | 512 MiB to 20 GiB (8 GiB by default) | The build runs in its own Firecracker virtual machine, with the same web access as a sandbox ([limits](/docs/images#limits)). ## Versions, tags and rebuilds Each build of a name is its next version and takes the tag `latest`, or the `tags` you give. `image` on a create takes an id, `name`, `name:tag` or `name@version`, so production can pin `data@3` while tests follow `data`. A rebuild whose first steps match an earlier build of your account starts from the latest matching checkpoint, and an identical build is copied at once instead of built. Pass `cache: false` to build every step from scratch ([faster rebuilds](/docs/images#faster-rebuilds)). ## Mistakes to avoid - **Installing packages in every sandbox.** Each fresh sandbox then pays for the download and install in running time. Build once. - **Passing both `image` and `snapshot` to a create.** A sandbox starts from one or the other. A [snapshot](/how-to/snapshot-a-sandbox) keeps a whole machine, memory included; an image keeps files. - **Giving a recipe and a Dockerfile together.** A build takes exactly one source: a `recipe`, an `image` or a `dockerfile`. - **Forgetting old versions.** Each stored image is charged; remove versions you no longer need with `runtime image rm data@1`. Facts on this page were checked on 25 September 2026.