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