> ## Documentation Index
> Fetch the complete documentation index at: https://docs.outerlayer.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Container and microVM builds

> What a build gets when the runner starts it in a container or a microVM, and how to size, limit and stop it.

A container build runs each item in a locked-down container of its own. The
runner starts it, serves its only network exit, and holds every credential
the build uses.

Set both the provision and the cleanup hook to `builtin:container`:

```json theme={"system"}
{
  "runner": {
    "commands": { "implement": "claude -p \"/build\" --permission-mode bypassPermissions" },
    "hooks": { "provision": "builtin:container", "cleanup": "builtin:container" },
    "build": { "memory": "8g", "cpus": 4 }
  }
}
```

Name both hooks or neither. The host needs Docker Engine 26 or later. If it
is older or Docker is not running, the build fails at provision and says
which.

## MicroVM builds

With both hooks set to `builtin:vm`, each build runs in a Firecracker
microVM with its own kernel. The host must be x86\_64 Linux, with Docker
Engine 26 or later and Docker Buildx 0.18 or later. The runner's account
must be able to open `/dev/kvm`. `runner init` names
`builtin:vm` on such a host; the full rule is in
[`outerlayer runner init`](/reference/cli-runner#outerlayer-runner-init).
`outerlayer doctor` fails `builtin:vm` hooks on any other host, and says
why.

The microVM is made from the recipe's image, with the same tunnel, tokens
and limits a container build has. The tunnel, the destinations, the branch
rule and the failure reasons below apply to it too. It has no network device: its connections leave over vsock to the
runner's tunnel. A recipe that adds the docker-in-docker feature gets its
own Docker daemon inside the microVM. That is how a build that needs Docker
runs.

The first microVM build downloads a pinned Firecracker release, a guest
kernel and a small guest helper from github.com. The runner checks each
file against a sha256 pinned in the CLI, and fails the build at provision
if one differs.

## What a build gets

The runner builds the repository's image, as
[`outerlayer runner image`](/reference/cli-runner#outerlayer-runner-image)
does, and starts one container from it. The container has no network but
loopback, a read-only root filesystem, and one volume for the clone. Your
home directory, other builds and the Docker socket are out of reach.

Commands run as the recipe's `remoteUser`, else as the image's own user.
When that is root, or neither names one, they run as `ol-agent`. A
lifecycle command that exits non-zero fails the build at `provision`, with
a reason naming the command.

## The `build` block

| Key | Default | Meaning |
| - | - | - |
| `memory` | `8g` | Memory limit, with no swap. |
| `cpus` | `4` | CPU limit. |
| `pids` | `4096` | Process limit. |
| `disk` | `40g` | The most the build's volume may hold. Checked every thirty seconds. |
| `variables` | `[]` | Names of host variables passed into the build. |
| `allowHosts` | none | The public hosts a build may reach. With none, every public host. See [Limiting the hosts a build reaches](#limiting-the-hosts-a-build-reaches). |

`variables` is the only way a host variable enters a build. Claude's
credential reaches it only as a placeholder. Repository tokens (`GH_TOKEN`,
`GITHUB_TOKEN`, `GH_ENTERPRISE_TOKEN`, `GITHUB_ENTERPRISE_TOKEN`) are never
passed; the runner logs each name it skipped. A name the runner sets itself,
such as `HOME`, `PATH` or a proxy variable, is refused.

To size the host for these limits, see
[Memory limits](/run-a-host-as-a-service#memory-limits).

## The gateway and Claude

Every build the runner provisions with a built-in hook (`builtin:vm`,
`builtin:container` or `builtin:process`) gets two built-in
[destinations](/destinations-and-secrets). The build holds neither the item
key nor Claude's credential.

| Destination | Forwards to | The runner sets | The build sees |
| - | - | - | - |
| `gateway` | The `url` in the runner's config file | `Authorization: Bearer <item key>` | A local `OUTERLAYER_URL` and a placeholder `OUTERLAYER_API_KEY` |
| `claude` | `https://api.anthropic.com` | The host's Claude credential | A local `ANTHROPIC_BASE_URL` and a placeholder credential |

The runner reads `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), or
else `ANTHROPIC_API_KEY`, from its own environment; restart it after a change.
A terminal `claude` login is not forwarded. With neither set, each build fails
at `provision` as `agent_credential_missing`. See
[Give the runner Claude's credential](/set-up-a-host#give-the-runner-claudes-credential).

A build your own hooks run gets no built-ins. It holds the item key as
`OUTERLAYER_API_KEY`; see [The build's variables](/write-your-own-hooks#the-builds-variables).

## The tunnel

The container's only way out is a tunnel the runner serves for that build.
The runner sets the standard proxy variables to it, `http://127.0.0.1:3128`.
A tool that ignores them cannot connect; set pip's `--proxy` or Maven's
`settings.xml` to that address in the recipe.

```mermaid theme={"system"}
flowchart LR
    subgraph B["Build: container or microVM"]
        T["Tools and agent"]
    end
    T -- "127.0.0.1:3128 proxy" --> TN["Runner's tunnel"]
    T -- "127.0.0.1:3126" --> G["gateway destination"]
    T -- "127.0.0.1:3127" --> C["claude destination"]
    T -- "127.0.0.1:3129 and up" --> D["Your destinations"]
    TN -- "allowHosts" --> P["Public hosts"]
    G --> GW["OuterLayer gateway"]
    C --> A["api.anthropic.com"]
    D --> U["Your services"]
```

A build cannot reach the host's own addresses, private or loopback addresses,
link-local addresses including the cloud metadata address `169.254.169.254`,
or Tailscale addresses. A refusal is a `403` naming the range. Listing such a
host in `allowHosts` does not change that. To reach a private service, make it
a [destination](/destinations-and-secrets).

## Limiting the hosts a build reaches

<Warning>
  By default a build can reach every public host, and can send anything it
  reads, such as a `build.variables` value or its read token, to any of them.
  Set `build.allowHosts` to close that.
</Warning>

```json theme={"system"}
{
  "runner": {
    "build": {
      "allowHosts": ["registry.npmjs.org", "registry.yarnpkg.com", "github.com", "*.githubusercontent.com"]
    }
  }
}
```

An entry is a host name, an IP address, or a pattern such as
`*.githubusercontent.com`, which matches names under the domain but not the
domain itself. An empty list refuses every public host. An unlisted name gets
a `403` with the range `not-allowed`. The list does not limit the gateway,
Claude, your destinations or the git remote.

To find the hosts your builds need, run a few without the list, then run
`outerlayer doctor` on the host. While the list is unset, its "Build hosts"
check warns and lists every host past builds reached, ready to paste into
`allowHosts`. Review it first: a host a build should never have reached is
in it too. Each build's own list is `hosts` in its `environment` (see
[What a build records](#what-a-build-records)).

## The git remote and the build's tokens

A container build holds no credential that can write to its repository. Git
and `gh` reach GitHub through the runner, which adds short-lived read and push
tokens the gateway issues
([GitHub App permissions and build tokens](/github-app-and-build-tokens)).
`gh` can read, such as `gh pr view`, but not write.

**The branch rule.** A push may update only the claim's branch. On an
`outerlayer/…` branch any update goes through, force-pushes included;
elsewhere only creating the branch and fast-forwards. A refused push updates
no ref, the runner logs `push refused: <refs>`, and git prints:

| Push refused | Git prints |
| - | - |
| Another ref | `this build may push only to <branch>` |
| Force-push or delete outside `outerlayer/` | `<branch> is not an outerlayer/ branch, so this build may only fast-forward it` |
| Over 2 GiB, outside `outerlayer/` | `the push is larger than the <bytes> bytes the runner holds to check it` |
| Check failed, outside `outerlayer/` | `the runner could not check whether this push fast-forwards <branch>` |
| Unreadable body | `the runner refused this push: <why>` |

<Warning>
  A gateway with no GitHub App configured refuses every read token with
  `repository_tokens_unavailable`, so every container build fails at
  provision. Configure the App first.
</Warning>

## A governed repository's context

When a control-plane repository governs the item's repository, a container
build starts with its context (`AGENTS.md`, `.claude/`, `.outerlayer/`) in
the checkout. The runner fetches it on the host; the build never commits it.
**The host's git login must read the control plane.** `outerlayer doctor`
checks this as `Control plane access`.

## Failure reasons

| Reason | Meaning | What to do |
| - | - | - |
| `recipe_needs_docker` | The recipe uses `dockerComposeFile`, which no built-in hook runs. | Use a single-container recipe. A build that needs Docker can use `builtin:vm` with the docker-in-docker feature. Otherwise use [your own hooks](/write-your-own-hooks). |
| `disk_limit_exceeded` | The volume grew past `disk`. | Raise `disk`, or make the build write less. |
| `context_unavailable` | The host's git login cannot read the control plane, or the gateway did not say which repository governs this one. | Give the host's git login read access to the named repository, then run `outerlayer doctor`. |
| `context_conflict` | Two control planes claim the repository. | Remove it from one control plane's `governs` list. |
| `context_adoption_failed` | The context could not be copied in; the reason carries the first error line. | Fix what that line names, and start the build again. |
| `context_adoption_conflict` | The repository already tracks `AGENTS.md`, `.claude` or `.outerlayer`. | Remove those files from the repository in a commit. |
| `workflow_permission_required` | A push changed a workflow file and the token lacks `workflows` permission. | Allow it in `.outerlayer/policy.yaml`; see [Letting a build change workflows](/github-app-and-build-tokens#letting-a-build-change-workflows). |
| The gateway's code, such as `repository_not_in_installation` | The gateway refused the read token; `failed` at provision. | See [GitHub App permissions and build tokens](/github-app-and-build-tokens). |
| `lease_lost` | A token renewal got `claim_expired` or `401`, so the build stopped. | Nothing. The item stays available. |

A read token that fails for a temporary reason, such as `github_unavailable`,
releases the claim with no outcome; the runner retries on its next poll. A
refused push token fails the push, and the build fails at its next health
check.

## Stopping a build

Stopping a build stops its container. Cleanup removes the container and
volume; a runner that crashed finishes it on restart. If the runner logs
`cleanup failed`, its next [housekeeping](/run-a-host-as-a-service#housekeeping)
pass removes what the build left, within the hour. To free the space
sooner, remove what it names with `docker rm --force` and
`docker volume rm`.

## What a build records

`outerlayer work status --json` shows each build's `environment` under
`claims`: the isolation, image, hosts reached and refused, variable and
destination names, and CLI versions. It never holds a secret.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.