---
title: docker()
description: >-
  The Docker sandbox, its image options, how reeval tags and caches images, and
  the errors it raises.
---
> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reeval.ai/llms.txt
> Use this file to discover all available pages before exploring further.

`docker()` runs each attempt in its own local Docker container. It's the default sandbox, and the only one real harnesses run in.

```ts title="evals/python.eval.ts"
import { defineEnvironment, evaluate, expect } from '{{ pkg }}'
import { docker } from '{{ pkg }}/sandbox'

const python = defineEnvironment('python', {
  sandbox: docker({ from: 'python:3.12-slim', id: 'python' }),
})
const jq = defineEnvironment('jq', {
  sandbox: docker({
    install: [
      'apt-get update && apt-get install -y --no-install-recommends jq',
    ],
  }),
})

evaluate('writes fizzbuzz in python', { environment: python }, async (c) => {
  await c.send('Write fizzbuzz.py and unittest tests for it.')
  await expect(c.exec('python -m unittest')).toSucceed()
})

evaluate('counts orders with jq', { environment: jq }, async (c) => {
  await c.send('Write count.sh, which counts orders.json entries with jq.')
  await expect(c.exec('bash count.sh')).toSucceed()
})
```

Without options, `docker()` uses reeval's sandbox image for the harness, which `reeval image build` makes.

## Options

`image`, `from` and `dockerfile` choose the base; set at most one.

<Property name="from" type="string" optional>
  A Debian or Ubuntu based toolchain image, such as `python:3.12-slim`. On first use reeval bakes Node, git and the harness on top, and caches the result.
</Property>

<Property name="dockerfile" type="string | URL" optional>
  Your Dockerfile, relative to the file that calls `docker()` (or an absolute path or `URL`). reeval builds it from its directory with `--build-arg REEVAL_BASE=<reeval's sandbox image>`. Start it with `ARG REEVAL_BASE` and `FROM ${REEVAL_BASE}` to layer on reeval's image; from any other Debian or Ubuntu base, reeval bakes Node and the harness on top, as for `from`.
</Property>

<Property name="image" type="string" optional>
  An image that already has the harness baked in, used as is. reeval doesn't build or pull it.
</Property>

<Property name="install" type="Array<string | { run: string, timeout?: string | number }>" optional>
  Shell commands baked onto the base in one cached layer. They run in order, as root with `HOME=/home/reeval` (what they put there is handed to the `reeval` user), from `/`, with bash, and never see secrets or the workspace: what needs either belongs in the environment's `setup`. A step's `timeout` kills it (there's none by default). Not with `dockerfile`: put the commands in it as `RUN` lines.
</Property>

<Property name="id" type="string" optional>
  The sandbox's id in cell keys and filters. Default `docker`; `docker:<last path segment of the image>` with `image` or `from`; `docker:<directory>` with a file named `Dockerfile`, or `docker:<file name>` otherwise.
</Property>

<Property name="maxConcurrent" type="number" optional>
  The most Docker sandboxes at once across the run, whatever their image.
</Property>

These set what the container runs with, not what's in its image, so changing them never rebuilds one. See [Sandbox security](/guide/sandboxes#sandbox-security).

<Property name="memory" type="string | number" optional>
  The container's memory: a size such as `'4g'` or `'512m'`, or bytes. Default `'4g'`. Past it, the container pages out to [`swap`](#swap); a process that goes past both is killed with exit 137, and the container keeps running. The attempt is then an infra error, whatever its checks said: at teardown reeval reads the container's cgroup `oom_kill` count (or Docker's `OOMKilled` without one) and records it in `error.resource`, with a message such as `out of memory: 1 process killed at the 4g limit (+4g swap); raise docker({ memory })`.
</Property>

<Property name="swap" type="string | number" optional>
  Swap the container may use on top of `memory`, in the same units. Default: as much as `memory`, so `docker({ memory: '4g' })` runs with `--memory 4g --memory-swap 8g`. `0` makes `memory` the hard ceiling (`--memory-swap` equal to `--memory`). RAM stays at `memory` either way, and a host without swap has none to give, so there nothing changes.
</Property>

<Property name="cpus" type="number" optional>
  CPUs the container may use; fractions such as `1.5` are fine. Default `2`, and never more than the Docker host has.
</Property>

<Property name="pids" type="number" optional>
  Processes and threads at once in the container. Default `1024`.
</Property>

<Property name="root" type="boolean" optional>
  Run the agent, setup steps and checks as root instead of the `reeval` user, with the capabilities `apt-get` needs added back (`CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID`, `SETGID`). Default `false`. For evals that install system packages during the attempt; baking them in with `install` needs no opt-in. Every run that uses it prints a `WARNING:` line (see [Privilege warnings](#privilege-warnings)).
</Property>

<Property name="capabilities" type="string[]" optional>
  Linux capabilities to add back, such as `['NET_RAW']`. Every other one is dropped, except the four reeval's own root commands need (see [Inside the container](#inside-the-container)). Default none. Every run that uses it prints a `warning:` line.
</Property>

## Privilege warnings

`root` and `capabilities` stay opt-in, and reeval says so whenever a run uses them:

```text
plan  2 evals · 2 cells · 2 attempts · ~2s serial (~1s at concurrency 4)
WARNING: running as root in the sandbox: evaluate("installs deps") (docker({ root: true }))
warning: extra capabilities in the sandbox: reeval.config.ts (docker({ capabilities: ['NET_RAW'] }))
```

There's one line per environment that set the sandbox: the config's `environment`, a profile's `defaults.environment`, a `group` or an `evaluate`, however many evals inherit it. A sandbox with both root and capabilities gets both lines.

| Where | What you see |
|---|---|
| `reeval run` and `reeval run --dry-run` | The lines under the plan, before any attempt starts, in every `--ui` |
| GitHub Actions | Also a `::warning` annotation each, on the eval file for a group or eval |
| `reeval run --dry-run --json` | `sandboxWarnings`: `kind` (`root` or `capabilities`), `level` (`config`, `profile`, `group` or `eval`), `where`, `file`, `capabilities` and `message` |
| The run record | The same list in `plan.sandboxWarnings`, shown in `summary.md` and on the studio's run page |

## Images and caching

Each harness has its own sandbox image. `reeval image build` builds the images the config's evals need that are missing or stale; `reeval image build --dry-run` lists them and how each step stands (`✓` ready, `+` to build, `~` stale, `✗` missing), building and pulling nothing:

```text
images  7 sandbox images (nothing built: --dry-run)
docker · cursor · 23 evals
  + base  reeval-sandbox:cursor-0.1.6  (to build: not built yet)
docker · claude-code · 1 eval
  + base  reeval-sandbox:claude-code-1.0.160-patch.b17cde50  (to build: not built yet)
…
docker+0ab0e263 · cursor + install · 1 eval
  + base               reeval-sandbox:cursor-0.1.6  (to build: not built yet)
  + install 1 command  reeval-install:<base id + commands>  (to build: not built yet)
python · cursor · 1 eval
  + from python:3.12-slim  reeval-sandbox:cursor-0.1.6-python-3-12-slim-375f4a61  (to build: on python:3.12-slim (a6e34c598f24))
…
docker:sqlite · cursor · 1 eval
  + dockerfile docker/sqlite/Dockerfile  reeval-dockerfile:sqlite-<context + reeval image id>  (to build: not built yet (reeval-dockerfile:sqlite-9efec1fbf61588d3))
  + harness                              reeval-dockerfile:sqlite-<context + reeval image id>  (to build: reeval-sandbox:cursor-0.1.6 not built yet)
```

| Image | Tag | Rebuilt when |
|---|---|---|
| reeval's sandbox image | `reeval-sandbox:<harness>-<adapter version>` | Missing, or reeval's base build context or the way it bakes the harness changed (its `ai.reeval.context` label); its toolchain layer is `reeval-sandbox-base:latest` |
| `from` | `reeval-sandbox:<harness>-<version>-<image slug>-<hash>` | First use, or the way reeval bakes the harness changed (the hash has it), on the digest the tag names then (its `ai.reeval.from-digest` label, and `image.from.digest` in the record); `reeval image build` rebuilds it when the tag has moved upstream |
| `install` | `reeval-install:<hash of base id and commands>` | The base image's id or a command changes |
| `dockerfile` | `reeval-dockerfile:<name>-<hash>` | The Dockerfile or its context (less `.dockerignore`) changes, or, `FROM ${REEVAL_BASE}`, reeval's image is rebuilt (the hash has its id) |
| A plugin's `build` | `reeval-plugin:<image id>-<hash of commit, source tree and build>` | The image under it, the plugin's commit, its files (less `.dockerignore` and `.git`) or its commands change |

A [plugin](/reference/helpers#plugin) with a `build` adds a layer on top of the attempt's image, after `install`, one per plugin. The layer gets the plugin's whole source and runs the commands with bash, as root with `HOME=/home/reeval` and no secrets, from the plugin's `path` (or `cwd` from the source's root). Only `path` is kept in the image, and the attempt copies it to `~/.reeval/plugins/<name>/`. A `plugin.git` or `plugin.npm` source is fetched when the layer builds, so `--dry-run` lists its tag unresolved:

```text
docker · cursor + 1 plugin build · 1 eval
  ✓ base                                  reeval-sandbox:cursor-0.1.6  (ready: built (96cb8a76212d))
  + build plugin.local('plugins/review')  reeval-plugin:96cb8a76212d-f2d3c864c458c316  (to build: not built yet)
```

A failing build command fails the attempts that use the plugin as infra errors (exit 3), like a failing `install` step: `Building the build of plugin.local('plugins/review') failed: build 2/2 exited 7: exit 7`. So does a `cwd` that isn't a directory in the source (`build cwd tools isn't a directory in the source`), before any command runs. `fakeSandbox()` has no image, so the plan refuses a plugin with a `build` there.

A plugin's content is fixed when its image is prepared, and the record's `plugins[]` shows which content the attempt used. A re-resolved npm spec, or a local plugin you edit mid-run, isn't picked up until the next run; only a moved git ref is noticed.

The build doesn't see the source's `.git` directory. A build that needs git metadata (`git describe`, husky) has to get it another way, such as a version passed in through the build's commands or environment.

`reeval image build --force` rebuilds every image and pulls each base again. A `from` pinned to a digest (`node:22@sha256:…`) never moves.

A rebuild removes the image its tag pointed at before, unless a container or another tag still uses it, and a `from` build drops its intermediate `…-base` tag once the harness is committed, so rebuilds leave no dangling images behind.

| Harness | Adapter version in the tag |
|---|---|
| Cursor | `0.1.6` (`@reeval/harness-cursor-sdk`, which installs `@cursor/sdk` 1.0.40) |
| Claude Code | `1.0.160-patch.b17cde50` (1.0.160, patched) |
| Antigravity | `0.1.12` |
| OpenCode | `0.2.10` |
| Goose | `0.1.6` |
| pi | `0.2.8` |
| Codex | `0.1.9` |
| fx | `0.1.1` |
| Grok Build | `0.1.5` |

A fake harness with a `layout` uses that agent's image; without one, Cursor's.

What reeval adds on top of a `from` or foreign `dockerfile` base: `bash`, `ca-certificates`, `curl`, `git`, `procps`, Node 22, npm and pnpm, the `reeval` user, the workspace at `/reeval/workspace`, and `/etc/profile.d/00-reeval-path.sh` (see [Inside the container](#inside-the-container)). That needs `apt-get`, hence Debian or Ubuntu. Build output goes to `.reeval/cache/images/<tag>.log`.

Image builds don't count toward the eval timeout.

## Inside the container

| Runs | As | With |
|---|---|---|
| The agent, setup steps and checks | `reeval` (uid 10001, home `/home/reeval`, where `~/` paths go) | No capabilities, `no-new-privileges` and the limits above |
| The egress gateway | root | Node, which every image reeval builds has |
| reeval's workspace snapshot (the diff) | root, in `/var/lib/reeval` | `CHOWN`, `DAC_OVERRIDE`, `DAC_READ_SEARCH` and `FOWNER`, the container's seccomp and AppArmor profiles, and `git` and `/bin/sh` from the system's `PATH`, never `~/.local/bin` |

Every container drops every capability but `CHOWN`, `DAC_OVERRIDE`, `DAC_READ_SEARCH` and `FOWNER`, which only root's processes hold: with `no-new-privileges`, the `reeval` user can't gain them, setuid binaries included.

Every attempt gets a fresh container with the workspace at `/reeval/workspace`. The gateway enforces the [egress policy](/reference/egress), records traffic, and swaps brokered secrets and the harness's key into allowed requests.

The snapshot that turns' `diff` is taken against lives in `/var/lib/reeval`, owned by root and mode `0700`, so the agent can't read it. It's always one commit with nothing unreachable left in it, so it never holds a file's content from before `c.writeFile` replaced it. With `root: true` the agent can read the directory, but it finds only the workspace as it already sees it.

Every command reeval runs in the container, the harness's bootstrap included, starts with umask `022`. The files and directories it makes aren't group- or world-writable, whatever umask the engine gives `docker exec`: Docker 29.6 and later give 0000 under Docker-in-Docker and rootless Docker.

```bash
# the agent's shell in docker({ from: 'dart:stable' })
$ sh -lc 'echo $PATH; dart --version'
/home/reeval/.local/bin:/usr/lib/dart/bin:/root/.pub-cache/bin:…
Dart SDK version: 3.13.5 (stable) … on "linux_x64"
```

Login shells keep the image's `PATH`. Cursor and Codex run the agent's commands through one, and Debian's `/etc/profile` resets `PATH` there, dropping what an `ENV PATH` added, such as `dart:stable`'s `/usr/lib/dart/bin`. Every image reeval builds has `/etc/profile.d/00-reeval-path.sh`, which puts the `PATH` the shell started with back in front, so the agent finds the same tools as your checks.

Every image reeval builds has the `reeval` user. `docker({ image })` with an image that doesn't fails with `<image> has no reeval user to run the sandbox as. …`: rebuild it with reeval, or pass `root: true`.

## Errors

| Mistake | Message |
|---|---|
| Two bases | `docker() takes one base image, but got image and from. Use image (has the harness), from (a toolchain image reeval bakes the harness onto) or dockerfile.` |
| `dockerfile` and `install` | `docker() takes dockerfile or install, not both: put the install commands in the Dockerfile as RUN lines, so there's one recipe for the image.` |
| A missing Dockerfile | `docker({ dockerfile: "./Dockerfile" }): <full path> doesn't exist. Paths are relative to the file that calls docker().` |
| `env` on the sandbox | `Unknown docker() option "env". The sandbox is what's baked into the image; variables and secrets belong in defineEnvironment({ env }).` |
| A near miss | `Unknown docker() option "base". Did you mean "from"?` (also `steps`, `run`, `setup` → `install`; `file`, `build` → `dockerfile`; `user`, `privileged` → `root`; `capAdd`, `caps` → `capabilities`; `mem`, `memoryLimit` → `memory`; `memorySwap`, `memswap` → `swap`; `cpu` → `cpus`; `pidsLimit` → `pids`) |
| An empty `install` | `docker({ install }) takes a non-empty list of shell commands, e.g. install: ['apt-get update && apt-get install -y jq'] (got [])` |
| A bad install step | `docker({ install }) commands are strings or { run, timeout? } objects (got …)` |
| `root` isn't a boolean | `docker({ root }) is true or false (got "yes")` |
| A bad capability list | `docker({ capabilities }) takes Linux capability names, e.g. capabilities: ['NET_RAW'] (got "NET_RAW")` |
| A bad `memory` | `docker({ memory }) takes a size such as '4g' or '512m', or bytes (got "4GB")` |
| A bad `swap` | `docker({ swap }) takes a size such as '2g' or '512m', bytes, or 0 for none (got -1)` |
| A bad `cpus` or `pids` | `docker({ cpus }) takes a number of CPUs above zero, e.g. 1.5 (got 0)`, `docker({ pids }) takes a whole number of processes above zero (got 10.5)` |
| A bad install timeout | `docker({ install }) command "x": Invalid duration "5 min". Use e.g. "90s" or "15m".` (zero or negative: `A duration is more than zero`) |
| An empty `image` | `docker({ image }) must be a non-empty string (got "")` |
| A `from` with spaces | `docker({ from }) takes an image reference with no spaces or line breaks, e.g. from: 'python:3.12-slim' (got "node:24 AS tools")` |

Before any attempt starts, reeval builds every image the plan needs that isn't there, including its own image for each harness, three at a time, once each (see [Sandbox providers](/reference/sandbox/providers)). An image a group or eval environment sets that fails fails only its attempts; one the config's `environment` sets stops the run.

| Situation | Message |
|---|---|
| Docker isn't running | `Docker is not running. Start Docker and try again.` (exit 2); on Windows, `… Start Docker Desktop (with the WSL 2 backend and Linux containers) and try again.` |
| No `docker` command | `Docker isn't installed: there's no docker command on the PATH. Install Docker Engine (https://docs.docker.com/engine/install/) or Docker Desktop, and try again.` (exit 2); macOS and Windows link their Docker Desktop installers |
| Docker runs Windows containers | `Docker is running Windows containers, but reeval's sandboxes are Linux containers. Switch Docker Desktop to Linux containers (its tray menu: Switch to Linux containers) and try again.` (exit 2) |
| An `image` that isn't there | `Docker image … not found. docker({ image }) is used as is, so pull or build it first …` |
| The config's image fails | The failure, then `The sandbox docker+0ab0e263 + install is set by reeval.config.ts for every eval, so nothing ran.` (exit 3) |
| A non-Debian base | `Building the toolchain layer FROM alpine:3.20 failed: reeval needs a Debian or Ubuntu based image (glibc and apt-get)` |
| A failing install step | `Building the install layer (N commands) failed: install i/N exited <code>: <command>` |
| A failing Dockerfile | `Building evals/x.Dockerfile failed: a RUN step exited 42`, or BuildKit's error (`dockerfile parse error on line 2: unknown instruction: COPYY …`) |
| A harness bootstrap that fails | `Baking the Cursor CLI onto <base> failed: Bootstrap command failed …` |

Every build failure starts with that headline. A named cause follows when reeval recognises one, then the failing step's own last lines (`  | …`, never reeval's generated Dockerfile), then `(full log: .reeval/cache/images/<tag>.log)`:

```text
Building the install layer (1 command) failed: install 1/1 exited 1: dd if=/dev/zero …
out of disk space (ENOSPC): free space for Docker (docker system df, reeval clean --images) and rerun
  | dd: error writing '/dev/shm/fill': No space left on device
(full log: .reeval/cache/images/reeval-install-4f0c2a1e9b7d3c55.log)
```

| Cause | Named when |
|---|---|
| `out of disk space (ENOSPC)` | The output says `No space left on device` or `ENOSPC` |
| `killed (exit 137): usually out of memory` | A step exits 137 (SIGKILL, usually the kernel's out-of-memory killer) |

A registry or network error (`no such host`, `i/o timeout`, `TLS handshake timeout`, `EAI_AGAIN`, a registry `5xx` or `429`) is tried twice more, after 2 s and 8 s. A failing `RUN` or `install` step, a Dockerfile syntax error, a denied pull and a non-Debian base fail at once: they'd fail the same way again.
| An `image` without Node | `Can't keep CURSOR_API_KEY out of the sandbox: credential brokering could not start (…). The egress gateway needs Node in the image: …` |

## Related

- [podman()](/reference/sandbox/podman), the same sandbox on Podman
- [Sandboxes guide](/guide/sandboxes)
- [reeval image build and reeval clean](/reference/cli/image-clean)
- [Egress](/reference/egress)
- [fakeSandbox()](/reference/sandbox/fake)
