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

# Run the runner in a container

> Start the runner from the published image with one docker run, and keep it running with Docker's restart policy.

The runner ships as a container image, the runner image. You start it with one `docker run`
on any Linux machine with Docker Engine, or on a Mac with Docker Desktop. You install no CLI and you write no
config file. Docker's restart policy keeps it running, and its builds survive
the container restarting.

The service runner in [Run the runner as a service](/run-a-host-as-a-service)
stays the choice for a runner that builds in microVMs.

## Before you begin

You need:

* **A Linux machine with Docker Engine 26 or later, or a Mac with Docker
  Desktop.** The engine behind Docker Desktop must also be version 26 or
  later. Check it with `docker version --format '{{.Server.Version}}'`. The
  runner image refuses to start with an older engine and names its version.
  See [Docker Desktop](#docker-desktop).
* **A runner key.** It is an API key with the **Runner** preset. Create it as
  described in [Set up your first host](/set-up-a-host#give-the-runner-its-key).
* **The factory's id.** It is the **Factory Id** on **Settings → General**.
* **A Claude credential.** Create a token with `claude setup-token`, or use an
  Anthropic API key.

Builds run as containers beside the runner's container, on the host's Docker.
They do not run inside it. MicroVM builds are not available in the image,
because they need `/dev/kvm` and a cgroup for each build on the host.

## Docker Desktop

On a Mac, the runner in the image runs on Docker Desktop's Linux VM. The
runner sees that VM, not the Mac. Builds run as containers in the VM and record `container`. What a build there can
reach is in [Security and limits](/build-security#docker-desktop). This works
on Apple silicon and on Intel Macs.

<Note>
  Docker Desktop is free for personal use, education and non-commercial open
  source. It is also free for companies with fewer than 250 employees and
  less than \$10 million in annual revenue. Larger companies need a paid Docker
  subscription. Read
  [Docker's terms](https://www.docker.com/legal/docker-subscription-service-agreement/)
  before you choose the runner image over Lima. Lima is free; see
  [Run builds on macOS](/run-builds-on-macos).
</Note>

The VM must be large enough for the runner's default build limits. Each build
is limited to `8g` of memory, 4 CPUs and `40g` of disk. Docker Desktop's
default VM can be smaller.

* **Disk.** The runner claims work only when the VM's disk has
  `runner.housekeeping.minFreeDisk` free. The default is 40 GB. The VM's disk
  also holds every image and volume on the Mac.
* **Memory.** The runner claims work only when the VM's memory is at least
  `runner.concurrency` times `runner.build.memory`, plus 2 GiB. With the
  defaults, that is about 10 GiB.
* **CPUs.** A build never gets more CPUs than the runner can use. A VM with
  fewer than 4 CPUs gives each build all of its CPUs.

To give the VM more memory, CPUs and disk, open Docker Desktop's
**Settings → Resources**. To make the limits smaller instead, set
`runner.build.memory`, `runner.build.cpus`, `runner.build.disk` and
`runner.housekeeping.minFreeDisk` in the config file in the volume. Edit it
with `docker exec`, as [What the volume holds](#what-the-volume-holds)
describes.

Docker Desktop stops every container when it restarts, builds included. The
runner then ends each build with the exit code it recorded. A build with no
recorded exit code ends as `interrupted`, so the item can be built again.

After a reboot, two settings bring the runner back with no one starting it:

* In Docker Desktop's settings, turn on **Start Docker Desktop when you sign
  in**.
* Start the runner container with `--restart unless-stopped`, as the command
  below does.

Docker Desktop on Windows passes the same check. Nobody has verified it end
to end. See [Run builds on Windows](/run-builds-on-windows).

## Start the runner

Put the settings in a file, so they stay out of your shell history. The file
holds the runner key, so give it to your account only.

```text title="runner.env" theme={"system"}
OUTERLAYER_URL=https://api.outerlayer.ai
OUTERLAYER_APP_ID=<factory-id>
OUTERLAYER_API_KEY=<runner-key>
OUTERLAYER_RUNNER_NAME=build-host-1
CLAUDE_CODE_OAUTH_TOKEN=<token>
```

Then start the container:

```bash theme={"system"}
docker run -d --name outerlayer-runner --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v outerlayer-runner:/root/.outerlayer \
  --env-file runner.env \
  ghcr.io/outerlayerai/runner:<version>
```

Replace `<version>` with a CLI version, such as `0.22.2`. There is no `latest` tag.
A run command names its version, so a runner never changes under a running
factory.

The file must set these variables:

| Variable | What it is |
| - | - |
| `OUTERLAYER_URL` | The factory's gateway address. |
| `OUTERLAYER_APP_ID` | The factory's id. |
| `OUTERLAYER_API_KEY` | The runner key. |
| `OUTERLAYER_RUNNER_NAME` | The runner's name, which the factory shows. `OUTERLAYER_HOST` is the earlier name of this variable. It still works as an alias. Setting both to different values is refused. |
| `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY` | One Claude credential. The runner passes it to Claude's API and never to a build. `ANTHROPIC_AUTH_TOKEN` passes the image's start check, but the runner does not forward it, so it claims nothing with that variable alone. |

`OUTERLAYER_AGENT` is optional. It names the agent the builds run, and it
defaults to `claude`.

<Warning>
  Use `--env-file`, not `-e`. A value in `-e` stays in your shell history.
  Anyone who can run `docker inspect` can read the runner key and the Claude
  credential. Anyone who can do that already controls the Docker daemon.
</Warning>

## What the volume holds

The volume `outerlayer-runner` holds everything the runner keeps:

* the config file, written from the variables on the first start;
* the host key, which binds the runner key to this host;
* the state of running builds, and the mirrors of repositories;
* the metadata of the runner's image builder.

Keep the same volume when you recreate the container. A new volume makes a
new host key. The runner key then fails to claim, because the factory bound it
to the first host key.

On a later start the runner uses the config in the volume. It refuses to start
when `OUTERLAYER_URL`, `OUTERLAYER_APP_ID`, `OUTERLAYER_API_KEY` or
`OUTERLAYER_RUNNER_NAME` disagrees with that config. The message names the factory by
URL and app id, the key by its last four characters, and the runner by name. It
never prints a key in full. To move a runner to another factory, key or
name, use a new volume.

Other settings, such as queues, concurrency and the time limit, live in the
config file in the volume. Edit it with `docker exec`. The runner reads the
file again when it changes. See the [config file reference](/reference/config-file).

## Check that it runs

```bash theme={"system"}
docker logs outerlayer-runner
docker exec outerlayer-runner outerlayer runner status
docker exec outerlayer-runner outerlayer doctor
```

The factory shows the runner under the name you set in `OUTERLAYER_RUNNER_NAME`.

## Pin the image by digest

A tag can move. A digest cannot. To pin the image, find the digest of the
version you tested:

```bash theme={"system"}
docker buildx imagetools inspect ghcr.io/outerlayerai/runner:<version>
```

Then name the image by its digest in `docker run`:

```text theme={"system"}
ghcr.io/outerlayerai/runner@sha256:<digest>
```

Each release also records a build provenance attestation for the image.
Verify it with `gh attestation verify oci://ghcr.io/outerlayerai/runner@sha256:<digest> --owner outerlayerai`.

## Drain before maintenance

`docker stop` leaves the builds running and exits the runner at once. To wait
for the builds instead, drain the runner first:

```bash theme={"system"}
docker exec outerlayer-runner outerlayer runner stop
```

The runner then takes no more work and keeps managing its builds. The command
returns at once, and the container stays up. Run `outerlayer runner status` in
the container to see what is left:

```bash theme={"system"}
docker exec outerlayer-runner outerlayer runner status
```

Run `docker stop outerlayer-runner` when no build is left. Do not expect the
container to exit by itself: with `--restart unless-stopped`, an exit would
start a runner that claims work again.

## Stop the runner

`docker stop outerlayer-runner` takes no more work, leaves every build running
and exits with status 0, before Docker's 10-second kill. The container that
starts next, with the same volume, picks the builds up.

A build keeps its claim only while a runner renews it. A recreate that takes
longer than the claim lasts ends the build. A build's open connections fail while no
runner runs. A new connection waits up to 30 seconds for a runner, and then
fails.

## Update the runner

A runner in the image does not update itself. The image tag names the version
that runs, so an update would hide in the volume, where `docker images` cannot
show it. An update is a new image.

The runner tells you when the factory wants another version. It logs the tag
once. `outerlayer runner status` and `outerlayer doctor` name it too. If the
factory refuses the runner as too old, the runner claims nothing until you
update it.

To update:

```bash theme={"system"}
docker pull ghcr.io/outerlayerai/runner:<version>
docker stop outerlayer-runner
docker rm outerlayer-runner
docker run -d --name outerlayer-runner --restart unless-stopped \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v outerlayer-runner:/root/.outerlayer \
  --env-file runner.env \
  ghcr.io/outerlayerai/runner:<version>
```

Use the same volume. Builds survive the recreate. The factory's "follow"
setting for runner versions cannot move a runner in a container by itself.

## What the container can reach

The runner holds the Docker socket, so it controls the host's Docker daemon.
Builds never get the socket. See
[The Docker socket in a runner container](/build-security#the-docker-socket-in-a-runner-container).


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