lazy documentationv0.90

The agent container

Every task's agent runs in a Docker container of its own. This page is for anyone whose agents need tools the default image does not have, or who is diagnosing an agent that cannot start: what the default image contains, how to give your project its own image, and when lazy rebuilds it.

The default image

A project with no [docker] dockerfile in its lazy.toml runs agents in lazy's default image, lazy-runner, which is deliberately minimal. It is Debian (bookworm-slim) with:

Package Why
git, curl, jq, wget, unzip, less, openssh-client, gnupg, sudo Basic tools
Chromium ($CHROME_BIN) Screenshots for visual checks of UI work
Claude Code The agent binary

The agent runs as user, with passwordless sudo, so it can install what it needs for one task. Agents other than Claude Code get their own variant of this image with their binary added. Lazy itself is never baked into an image: it is mounted in when the container starts, so an image never pins you to an older lazy.

To see exactly what the default image is built from:

lazy system export-dockerfile --stdout

Giving your project its own image

When agents keep installing the same toolchain (a language runtime, a database client, your project's dependencies), build it into an image instead. Write a Dockerfile, usually starting from the default image, and point lazy.toml at it:

FROM lazy-runner
RUN sudo apt-get update && sudo apt-get install -y --no-install-recommends postgresql-client
[docker]
dockerfile = "Dockerfile.lazy"

The path is relative to the project root. If FROM lazy-runner names a base image that is not built yet, lazy builds it first. The build is done by the daemon on your machine, from the project root's copy of the file; an agent editing the Dockerfile on its task branch does not change the image your tasks run in. All [docker] settings, including build_inputs (rebuild when a lockfile changes), are in lazy.toml.

If your task containers need to be created with extra Docker flags (for example --cap-add=SYS_PTRACE), that is [docker] run_args — a container setting, not an image one, so no Dockerfile change can provide it.

Scripts that call node

bun install writes node_modules/.bin/* shims that start with #!/usr/bin/env node. If your image has no node (the default image has none), any package.json script that runs a dependency's CLI by name — tsc --noEmit, eslint . — fails with exit 127 before the tool runs. Either install node in your image, or run the tool's entry module through bun:

"typecheck": "bun node_modules/typescript/lib/tsc.js --noEmit"

Watch for this in [checks] post_turn: a check whose scripts all exit 127 checks nothing.

The headless browser

Agents find the browser through $CHROME_BIN (and chromium on PATH). If you build your own image from something other than lazy-runner, set CHROME_BIN too, so agents do not try to install a browser in every task. Rendering an HTML file to an image is one command:

"$CHROME_BIN" --headless --disable-gpu --no-sandbox --hide-scrollbars \
  --window-size=1280,1600 --screenshot=/tmp/shot.png file:///tmp/page.html

Chromium logs D-Bus and GPU warnings on every run in a container; they are noise, not failure. The exit code and a non-empty output file are the signals.

Rebuilding

Lazy names a custom image after the content of its Dockerfile (lazy-custom-<hash>), so editing the Dockerfile in the project root rebuilds it on the next task launch — no manual step.

A Dockerfile's text says nothing about what it pulls: the same file builds a different image a month later. So lazy also rebuilds on two schedules:

The image tag is lazy's major.minor version (for example lazy-custom-<hash>:0.22); it is an identity, not a freshness signal. lazy doctor lists older images left behind. What happens during a rebuild, and how to watch it, is in Image rebuilds on upgrade.

When an agent cannot start

lazy doctor checks the container runtime, the image and the agent binary, and says what to fix. The most common causes:

See also Troubleshooting.