Skip to main content
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.json count too.
  • The runner’s tools layer is rebuilt when the CLI changes, or the Claude Code, Node, gh or tini version 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:
The agent installs anything more it needs. Host checks run the same lifecycle commands, read at the same commit, with every pushed branch checked out at the same paths. See Container builds.

Properties the runner uses and ignores

Used:
  • image, build and features shape the image.
  • containerEnv, remoteUser, onCreateCommand, updateContentCommand, postCreateCommand and postStartCommand apply when a build’s container starts.
Everything else is ignored and listed in the report, including 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 with dockerComposeFile 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 in RUN 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 own docker 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 as recipe_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.