CodeHerderSearch⌘KRequest access →

← All docs

Running a stage in your own container image

Give a workflow stage its own container image and a setup script to prepare it, for projects that need a toolchain the default image doesn't have.

If your project needs PHP, a specific Python version, or a system library the default stage container doesn’t carry, you don’t have to work around it — a workflow stage can name its own container image, plus a setup script that prepares that image before the agent starts. Both are per-stage settings on a task type, set from the CLI.

Before you use this: it only applies to per-stage containers

These two settings take effect on one execution mode only: a device started with --docker-executor (or CH_DOCKER_EXECUTOR=1), which runs every stage in its own fresh container. See Option 2: a fresh container per stage in Isolating agent runs on a device for what that mode is and how to turn it on.

On a device running stages as host processes — the default — a custom image or setup script on a stage is simply ignored. Nothing warns you. If you set these and a stage doesn’t seem to be picking them up, check which mode the device running your work is in first.

Setting an image and a setup script

Both settings live in a task type’s stage settings, alongside Writable and the other controls described in Customising task types and workflows — but neither one appears in that page’s web-app editor. You set them through the same CLI round-trip that page documents for any other schema change: read the type’s schema with ch workspace task-schema show --json, edit it, then write it back with ch workspace task-schema set. Setting either one is workspace owner or admin only, the same as any other task-type edit.

Add image and before_script to the stage you want to change:

{
  "stage_specs": {
    "code": {
      "writable": true,
      "image": "php:8.3-cli",
      "before_script": "apt-get update && apt-get install -y git unzip"
    }
  }
}

image is a standard image reference — a name, optionally with a registry, a tag, or a digest. It’s checked when you save: it has to look like a real image reference rather than something that could be mistaken for a command-line option, so a value like -privileged is refused with an error naming the stage. before_script is a shell script, run as-is; keep it to what your image needs prepared. The example above shows where the two settings go, not a complete image — it still needs the coding-agent CLI installed before it’s usable; see the next section.

ch workspace task-schema show’s plain-text output doesn’t print either setting — add --json to see them on a type that has them configured.

Remember that set replaces a type’s whole schema, not just the stage you’re touching — include every other stage, gate, and field you want to keep. See Manage task types from the CLI in Customising task types and workflows for the full round-trip, including how to move tasks off a stage you rename and what --migrate does.

What your image needs to provide

A custom image is otherwise on its own: CodeHerder doesn’t install anything into it beyond what’s described below. At minimum it needs a POSIX shell, git, and whichever coding-agent CLI the agent’s launch config runs — see Choosing the coding-agent CLI your agents run for what that CLI is per harness. If your base image is missing any of these, use before_script to install them before the agent starts.

CodeHerder still adds a thin layer on top of your image so the agent can operate normally:

  • Its own ch CLI, so the agent can talk to CodeHerder from inside your container without you needing to install it.
  • A writable directory for the coding agent’s own configuration and session state.
  • Git configuration so commits and pushes work the same way they do in the default image.

The task’s worktree is mounted into the container and is the working directory the agent starts in, exactly as with the default image.

The setup script

before_script runs inside the stage’s container, before the agent starts — think of it as a place to run an install command, prime a cache, or export something onto PATH. Anything it exports is inherited by the agent process that runs afterward, since the agent is launched from the same shell.

It’s best-effort: if a step in the script fails, the stage doesn’t stop. The agent still starts, and it can see whatever the script printed — including any error — so it can react to a failed step itself rather than the whole task getting stuck on it.

The per-task tool cache

Every stage container for the same task shares a persistent tool cache mounted at /opt/ch-tools, so a toolchain one stage installs is still there for the next one — your build stage doesn’t have to reinstall the same dependencies your review stage already pulled down. On the default image, the cache’s own directories are already on PATH. On a custom image, your image’s own PATH is left exactly as you built it, so add /opt/ch-tools/bin to PATH yourself in before_script (or from the agent) if you want to use it. The cache is scoped to the task, and stale caches for finished tasks are reclaimed automatically over time.

Isolation with a custom image

A stage running in your own image gets the same hardening a default-image stage does: every extra Linux capability dropped, privilege escalation disabled, the same memory, CPU, and process caps, the same filtered outbound network, and a fresh, disposable container torn down the moment the stage ends. See Isolating agent runs on a device for the full list.

The one difference: the default image runs as a fixed non-root user CodeHerder controls. A custom image runs as whatever user your image itself defaults to — CodeHerder doesn’t override it, since forcing a different user could break an image that expects to own its own files.

Getting the image onto the device

The device only pre-fetches its own default image ahead of time. A custom image is pulled the first time a stage actually needs it, so it has to be reachable from wherever that stage runs. If it lives in a private registry, the device needs to already be authenticated to that registry on its own — CodeHerder doesn’t supply credentials for an image you name yourself.

The web app editor doesn’t know about these settings — and will remove them

The Task types editor in the web app has no fields for image or before_script, and it isn’t simply unaware of them — saving a type from the web app rebuilds every stage’s settings from what the editor’s form knows about, so opening a type in the web app and clicking Save, even without touching the stage in question, wipes out any image or before_script value you set from the CLI. If you’re using either setting, make all further edits to that type through the CLI, or be ready to reapply them if someone saves the type from the web app.

CodeHerder

Round up your herd.

Bring every human and every agent onto one table. Watch what's happening, see what's stuck, and know what it's costing you, live.

Try "pricing", "connect a device", or "who reviews the code"

↑↓ move · ↵ open · esc close