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

# Run the runner in a container

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

The runner image is a container image that runs the runner. You start it
with one `docker run`, on a 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 the runner running.

Builds run as containers beside the runner's container, on the host's
Docker. The runner image cannot run microVM builds. For those, see
[Set up your first host](https://docs.outerlayer.ai/set-up-a-host.md).

## Before you start

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. On a Mac, read
  [Docker Desktop](#docker-desktop) first.
* **A runner key.** It is an API key with the **Runner** preset. Create it as
  [Give the runner its key](https://docs.outerlayer.ai/set-up-a-host.md#give-the-runner-its-key) shows.
* **The factory's id.** It is the **Factory ID** on **Settings → General**.
* **A Claude credential.** Create a token with `claude setup-token` on a
  machine where Claude Code is installed, or use an Anthropic API key.

## Start the runner

> [!WARNING]
>
> The runner holds the host's Docker socket, so it controls the host's Docker
> daemon. Builds never get the socket. See
> [The Docker socket in a runner container](https://docs.outerlayer.ai/build-security.md#the-docker-socket-in-a-runner-container).

1. Put the settings in a file, so they stay out of your shell history. The
   file holds the runner key, so make it readable only by your account.

   ```text title="runner-image.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>
   ```

2. Start the container. Use `--env-file`, not `-e`: a value in `-e` stays in
   your shell history.

   ```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-image.env \
     ghcr.io/outerlayerai/runner:latest
   ```

Anyone who can run `docker inspect` on the host can read the runner key and
the Claude credential. That person already controls the Docker daemon.

### The variables

The file must set these variables:

| Variable | What it is |
| - | - |
| `OUTERLAYER_URL` | The gateway's address. On OuterLayer Cloud it is `https://api.outerlayer.ai`. |
| `OUTERLAYER_APP_ID` | The factory's id. |
| `OUTERLAYER_API_KEY` | The runner key. |
| `OUTERLAYER_RUNNER_NAME` | The runner's name, which the factory shows. |
| `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY` | One Claude credential. The runner passes it to Claude's API and never to a build. The runner does not use `ANTHROPIC_AUTH_TOKEN`. |

These variables are optional:

| Variable | What it is |
| - | - |
| `OUTERLAYER_AGENT` | The agent the builds run: `claude` or `opencode`. `claude` is the default. `opencode` needs `ANTHROPIC_API_KEY`, with `CLAUDE_CODE_OAUTH_TOKEN` not set. |
| `OUTERLAYER_BUILD_MEMORY` | The memory limit of each build, such as `6g`. When it is not set, the runner fits the limit to the machine, as [Docker Desktop](#docker-desktop) describes. |
| `OUTERLAYER_CONCURRENCY` | How many builds run at once, as a positive integer. The default is 1. |
| A variable that a destination's header reads, as `${NAME}` | That destination's secret. The runner refuses to start while it is not set. See [Destinations and secrets](https://docs.outerlayer.ai/destinations-and-secrets.md). |

The container refuses to start, and names the problem, when a required
variable is missing or a value is not valid.

### The image tags

Each release has these tags:

* `<version>`, such as `0.31.0`. It always names that exact release.
* `<major>.<minor>`, such as `0.31`. It names the newest patch release of
  that minor version.
* `latest`. It names the same version as the `latest` tag of
  `@outerlayer/cli` on npm.

A tag does not move a running container. Docker reads a tag only when you
pull or run, so a container keeps its version until you recreate it.

## 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's **Settings → Runners** page lists the runner under the name in
`OUTERLAYER_RUNNER_NAME`.

## What the volume holds

The volume `outerlayer-runner` holds everything the runner keeps: its config
file, its host key and the state of its builds.

> [!WARNING]
>
> Keep the same volume when you recreate the container. A new volume makes a
> new host key, and the factory then refuses the runner key. To fix that, see
> [Moving a runner to a new host](https://docs.outerlayer.ai/run-a-host-as-a-service.md#moving-a-runner-to-a-new-host).

The first start writes the config file from the variables. A later start
uses the config file in the volume, and these rules apply:

* **The factory, key and name cannot change.** The runner refuses to start
  when `OUTERLAYER_URL`, `OUTERLAYER_APP_ID`, `OUTERLAYER_API_KEY` or
  `OUTERLAYER_RUNNER_NAME` disagrees with the volume. To move a runner to
  another factory, key or name, use a new volume.
* **`OUTERLAYER_BUILD_MEMORY` and `OUTERLAYER_CONCURRENCY` replace the value
  in the volume** at each start. A variable you remove leaves its last value
  there. See [Stop setting the build memory](#stop-setting-the-build-memory).
* **The Claude credential is never written to the volume.** To replace it,
  change it in the env file and recreate the container with the same volume.

Other settings, such as queues and the time limit, are in the config file in
the volume, `/root/.outerlayer/config.json`. Edit it with `docker exec`. The
runner reads the file again when it changes. See
[The config file](https://docs.outerlayer.ai/reference/config-file.md).

### Stop setting the build memory

A line you remove from the env file does not remove its value from the
volume. The value stays in `runner.build.memory`, so the runner does not fit
the build memory to the machine again.

To let the runner fit the build memory again:

1. Remove the `OUTERLAYER_BUILD_MEMORY` line from the env file.

2. Delete `runner.build.memory` from the config file in the volume:

   ```bash theme={"system"}
   docker exec outerlayer-runner node -e '
     const fs = require("fs"), path = "/root/.outerlayer/config.json";
     const config = JSON.parse(fs.readFileSync(path, "utf8"));
     delete config.runner.build.memory;
     fs.writeFileSync(path, JSON.stringify(config, null, 2) + "\n");'
   ```

3. Recreate the container with the same volume.

`OUTERLAYER_CONCURRENCY` works the same way. To go back to one build at a
time, delete `runner.concurrency` with the same command.

## Docker Desktop

On a Mac, the runner image runs in Docker Desktop's Linux VM, on Apple
silicon and on Intel. Builds run as containers in that VM and record
`container`. What a build there can reach is in
[Security and limits](https://docs.outerlayer.ai/build-security.md#docker-desktop).

> [!NOTE]
>
> Docker Desktop needs a paid Docker subscription for some companies. 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 the runner on a Mac](https://docs.outerlayer.ai/run-builds-on-macos.md).

Docker Desktop on Windows passes the same start check. OuterLayer has not
tested it end to end. See [Run the runner on Windows](https://docs.outerlayer.ai/run-builds-on-windows.md).

### Size the VM

By default each build is limited to `8g` of memory, 4 CPUs and `40g` of
disk. Docker Desktop's default VM can be smaller. A small Linux machine gets
the same fit.

* **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 10 GiB. When nothing sets the build memory and `8g` does
  not fit, the runner lowers it to fit, down to `4g`, and logs the value. A
  VM with 8 GB gives each build `5g`. A VM too small for `4g` claims no
  work.
* **CPUs.** A VM with fewer than 4 CPUs gives each build all of its CPUs.
* **Disk.** The runner claims work only when the VM's disk has
  `runner.housekeeping.minFreeDisk` free, 40 GB by default. The VM's disk
  also holds every other image and volume on the Mac.

To give the VM more memory, CPUs and disk, open Docker Desktop's
**Settings → Resources**. Then restart the container.

To fit a small VM instead, lower the limits:

* Set `OUTERLAYER_BUILD_MEMORY` or `OUTERLAYER_CONCURRENCY` in the env file,
  and recreate the container.
* Set `runner.build.cpus`, `runner.build.disk` and
  `runner.housekeeping.minFreeDisk` in the config file in the volume.
* Set `runner.housekeeping.savedStateMax` lower. The runner keeps up to that
  much, 20 GiB by default, in saved states from
  [setup scripts](https://docs.outerlayer.ai/build-setup.md).

### Start the runner 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
  above does.

Docker Desktop stops every container when it restarts, builds included. A
build that had not finished ends as `interrupted`, so the item can be built
again.

## Operate the runner

### Drain before maintenance

To let the running builds finish before you stop the runner, drain it:

```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. To see what is left,
run:

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

Run `docker stop outerlayer-runner` when no build is left.

### Stop the runner

`docker stop outerlayer-runner` stops the runner and leaves every build
running. The container that starts next, with the same volume, takes the
builds over.

Start the next container soon. A build keeps its claim only while a runner
renews it, and the claim expires after 15 minutes. A build's network
connections also fail while no runner runs.

### Update the runner

A runner in the image does not update itself, whatever the factory's
**Runner updates** setting says. An update is a new image. The runner's log,
`outerlayer runner status` and `outerlayer doctor` name the tag to pull.

To update, follow [The runner image](https://docs.outerlayer.ai/update-a-runner.md#the-runner-image) on
the update page. It keeps the container's mounts, so the new container is
the same runner. Builds survive the change.

Update before the [support window](https://docs.outerlayer.ai/run-a-host-as-a-service.md#the-support-window)
ends, 30 days after OuterLayer names a newer version. A runner outside the
window claims nothing.

### Pin the image by digest

The `latest` and `<major>.<minor>` tags move with each release. A digest
does not move. Pin the image when a host must run one tested release until
you change it.

1. Find the digest of the version you tested:

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

2. Name the image by its digest in `docker run`:

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


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