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 file it reads

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. 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.

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.