Custom images
Build an image once with your code and dependencies, then start every sandbox
from it. An image is built from any Dockerfile, from any public or private
container image, or from a short recipe of packages. Each build of a name is
its next version, tags such as latest and prod point at versions, and an
image can say what a sandbox from it runs and when that sandbox is ready.
Build from a Dockerfile
Point the SDK or the CLI at a folder with a Dockerfile. The folder is the build
context, exactly as with docker build: every file its .dockerignore leaves
in is packed, and only the parts that changed since your last build are
uploaded.
Terminalruntime image build . -t web:v1runtime sandbox create --image web:v1TypeScriptimport { readFile } from "node:fs/promises";import { Runtime } from "withruntime";const runtime = new Runtime();const image = await runtime.images.build( { name: "web", dockerfile: await readFile("Dockerfile", "utf8"), contextDir: "." }, { onLog: (line) => console.log(line.text) },);await using sbx = await runtime.sandboxes.create({ image: "web" });console.log(image.version, sbx.id);Pythonfrom withruntime import Runtimeruntime = Runtime()with open("Dockerfile") as handle: image = runtime.images.build(name="web", dockerfile=handle.read(), context_dir=".", on_log=lambda line: print(line["text"]))sandbox = runtime.sandboxes.create(image="web")What a Dockerfile can use:
- Multi-stage builds.
FROM <stage>,COPY --from=<stage>andCOPY --from=<image>all work, and only the stages the image needs are built.--target(the SDKs'target) builds a named stage instead of the last one. ARGbefore and afterFROM, build arguments, and the platform arguments (TARGETARCHisamd64).- Heredocs:
RUN <<EOF,RUN python3 <<EOF, a script with its own#!, andCOPY <<EOF /path. ADDfrom anhttporhttpsURL, checked when you give--checksum=sha256:..., andADDof a local.tar,.tar.gz,.tgz,.tar.bz2or.tar.xzarchive, which is unpacked.FROM scratch, any public image, a private image (see below), orFROM runtimefor Runtime's own base image with Python, Node, Bun and the usual tools.COPY --chown,COPY --chmod,WORKDIR,ENV,USER,SHELLandRUN --mount=type=cache, which runs without the cache mount.CMD,ENTRYPOINTandHEALTHCHECKbecome the image's start and ready commands (see below).
A few forms are refused with a message that says what to do instead:
RUN --mount=type=secret, type=ssh and type=bind, ONBUILD, ADD from a
git repository, and COPY --exclude. Only the last stage can start
FROM runtime; start earlier stages from a public image such as
ubuntu:24.04.
The build runs in its own Firecracker virtual machine with the same web access
as a sandbox. USER applies to the build's RUN steps; commands in a sandbox
run as the sandbox user, uid 1000.
Build from an image or a recipe
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "py", image: "python:3.12-slim" });await runtime.images.build({ name: "data", recipe: { pip: ["pandas"], apt: ["jq"] } });A recipe takes base (runtime by default, or any image), apt, pip,
npm, commands, files, env and workdir. The CLI builds one with
--pip, --apt and --npm, and uses an image as it is with --from.
Private registries
Store a credential for a registry once, and every build of your account pulls from it. The secret is sealed to Runtime's servers the moment it arrives. No call returns it, and the API that stored it cannot read it back. A build opens it only to pull, inside its own build machine.
Terminalecho "$GHCR_TOKEN" | runtime image registry set ghcr.io --username my-userecho "$AWS_SECRET_ACCESS_KEY" | runtime image registry set 123456789012.dkr.ecr.us-east-1.amazonaws.com --access-key-id AKIA...runtime image registry ls| Registry | Give |
|---|---|
Docker Hub (docker.io) |
your user name and an access token |
GitHub (ghcr.io) |
your user name and a token that can read packages |
Google (gcr.io, *-docker.pkg.dev) |
_json_key and the service account key's JSON, or oauth2accesstoken and an access token |
Amazon ECR (<account>.dkr.ecr.<region>.amazonaws.com) |
an access key id and secret that may call ecr:GetAuthorizationToken and pull; each build asks ECR for a fresh token |
| Any other | a user name and a token or password |
The same credentials cover FROM, COPY --from=<image> and a recipe's base.
In the SDKs: runtime.images.registries.set(...), .list() and .delete(...).
Names, versions and tags
Every build of a name gets the next version number. When a build is ready, it
takes the tags you gave it, or latest when you gave none. A tag points at one
version and moves when another version takes it; a later version keeps a tag
even if an earlier build finishes after it.
Wherever an image is named, including image when you create a sandbox, it can
be its id, name (its latest tag), name:tag or name@version.
Terminalruntime image build . -t web:v2 -t web:latestruntime image versions webruntime image tag web@2 prodruntime image untag web@1 prodruntime image rm web@1Deleting a version removes its tags too. Sandboxes already started from it keep running.
Start and ready commands
An image can say what runs when a sandbox starts from it, and when that
sandbox counts as ready. A Dockerfile's CMD and ENTRYPOINT become the start
command and its HEALTHCHECK becomes the ready check. You can set or change
both with start:
TypeScriptimport { 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: ... }The start command runs once, in the background, as the sandbox user. The create
call answers when the ready check passes: readyPort is being listened on, or
readyCommand exits 0. It answers with start.state set to ready,
started (no ready check), timeout (the check did not pass within
readyTimeoutSeconds, 60 by default and at most 300, within the SDKs' own five-minute call deadline), or exited (the start
command ended first). start: null drops what the Dockerfile said.
Faster rebuilds
A build keeps up to three checkpoints of its filesystem: after the base image
is pulled, and after the last commands before later steps. A later build of your
account that begins with the same steps starts from the latest matching
checkpoint instead of from the beginning, so changing your code reruns only the
steps after the COPY that brings it in. The build log says which image's
checkpoint it started from, and cache on the image says what it keeps.
Checkpoints stay on the server with the image that made them and go when it is
deleted. They count toward your image disk quota and are not charged. Pass
cache: false (--no-cache) to build every step from scratch and keep none.
An identical build of your account, same plan and same files, is copied at once instead of built.
Build logs
images.build streams the log to onLog (on_log) as it is written. The CLI
prints it while it builds, and runtime image logs <image> --follow streams it
again. Over HTTP, GET /v1/images/{id}/logs?follow=true answers with one JSON
event per line: each line, then done with the image.
Limits
| Limit | Value |
|---|---|
| Builds at once | 1 until your account has bought credit, then 4 |
| Build machine | 1 to 8 vCPUs (2 by default), 1 to 16 GiB of memory (4 GiB), 1 to 32 GiB of scratch disk (4 GiB) |
| Build time | 60 seconds to 1 hour (30 minutes by default) |
| Image size | 512 MiB to 20 GiB (8 GiB by default) |
| Build context | 100 MiB compressed, 20,000 files, 2 GiB unpacked |
| Context upload | 1 MiB chunks, kept 24 hours after last use; 200 MiB held and 1 GiB uploaded a day per account |
Inline files |
256 files and 1 MiB in all |
| Dockerfile | 256 KiB, 200 steps across all stages, 16 earlier stages |
| Registry credentials | 20 per account |
Pricing
A stored image is charged on its whole file (pricing). Building an image is free and does not use trial hours. A free trial keeps its first three images free.