outerlayer runner image builds a repository’s image ahead of time, so a
build never waits for it. The image comes from the repository’s devcontainer
file. A build reuses the image, or builds it if it is missing.
Which repository’s file it reads
A repository governed by a control plane builds on the control plane’s devcontainer. A governed repository’s own devcontainer is never read. A repository’s own devcontainer is used only when it governs itself: a factory with one repository, or the control plane itself. The runner reads the control plane at the commit the build’s instructions came from. A control plane with neither devcontainer file below gets the default image. The runner does not fall back to another repository’s file. So when a factory’s control plane is a separate repository, keep the devcontainer its builds rely on in the control plane. When a governed repository has its own devcontainer file, the build log names the file and says the control plane’s devcontainer is used instead.outerlayer runner image --repository <owner/name> follows the same rule. For a governed repository it builds the control plane’s image. Repositories governed by one control plane share one image, built once.
Which file it reads
For a repository that governs itself, the runner reads the head of the default branch, never a build’s branch. It reads.devcontainer/devcontainer.json, or else .devcontainer.json. A
repository with neither gets a default Ubuntu 24.04 image with git, curl, CA
certificates and Node.
That default has no compiler or SDK for any other language. A repository
that builds with anything else needs a recipe that installs it.
Devcontainer features add many common
toolchains to a recipe. To check that a recipe builds before any work item
waits on it, run outerlayer runner image on the host.
The image has two layers, each rebuilt only when its inputs change:
- The recipe layer is rebuilt when any file under
.devcontainer/changes. For a root.devcontainer.json, that file and.devcontainer-lock.jsoncount too. - The runner’s tools layer is rebuilt when the CLI changes, or the Claude
Code, Node,
ghortiniversion it pins. So the first build after a CLI upgrade rebuilds that layer.
Setting up several repositories
In a build with more than one repository, every repository is at/build/work/repos/<owner>/<repo>. The owner and repository are in lower case, even when GitHub spells them with capitals. The primary repository is also at /build/work/checkout.
The control plane’s lifecycle commands run once, in the control plane’s checkout. The runner sets up no other repository. A control plane’s setup names each repository it prepares by its fixed path:
Properties the runner uses and ignores
Used:image,buildandfeaturesshape the image.containerEnv,remoteUser,onCreateCommand,updateContentCommand,postCreateCommandandpostStartCommandapply when a build’s container starts.
initializeCommand, runArgs, mounts, privileged, capAdd,
securityOpt, build.options and values such as ${localEnv:HOME}. So one
file can still serve VS Code, Codespaces and the runner.
Why Compose recipes are refused
A recipe withdockerComposeFile runs its services in Docker, which a
build does not have, so the runner refuses the recipe and builds nothing.
A repository whose tests start Docker needs builtin:vm hooks and a recipe
that adds the docker-in-docker feature, or hooks of its own. See
MicroVM builds.
Where setup that needs root belongs
Setup that needs root, such as system packages or a browser, goes inRUN
lines of .devcontainer/Dockerfile, named by build.dockerfile. Lifecycle
commands run as remoteUser and cannot install system packages. With hooks
of your own, setup on the host itself belongs in your provision hook. See
Run work on your own machines.
Private registries
A private base image or feature uses the host’s owndocker login. Log in on
the runner host as the user that runs outerlayer runner image.
The image builds in the runner’s own builder, with a memory limit of
runner.build.memory. See Image builds.
The runner passes its HTTP_PROXY, HTTPS_PROXY and NO_PROXY variables,
in upper or lower case, to Docker and to its builder. So base images and
features are pulled through the proxy. A RUN line that needs the proxy
gets it from Docker’s own client configuration, the proxies key in
~/.docker/config.json.
No other host variable reaches the recipe. A value that names one, such as
${localEnv:SSH_AUTH_SOCK}, is dropped and listed in the report.
Errors
A recipe the runner cannot build fails with a code, such asrecipe_ambiguous for a repository with only named configurations, or
recipe_needs_docker for a Compose recipe. Every code and its meaning is in
outerlayer runner image.
What to do about each is in
Outcomes and reasons.
Each build’s log, image.json and recipe.json live under
<config dir>/runner/images/<owner>/<name>/. A custom provision hook can
name a recipe.json in provision.out as recipe=<path>; see
The provision hook.
outerlayer doctor reports each repository’s recipe, warns about a Compose
recipe, and checks that the devcontainer CLI can run.
See Container builds for how a build uses the image.