Skip to main content
The runner ships as a container 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 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. The image refuses to start with an older engine and names its version. See Docker Desktop.
  • A runner key. It is an API key with the Runner preset. Create it as described in Set up your first host.
  • 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. A build’s container cannot reach the Mac. The image build runs outside the runner’s tunnel, so it can reach the Mac’s local services. See Security and limits. 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 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. For a Mac that has no Docker, see Run builds on macOS.

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.
runner.env
Then start the container:
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: OUTERLAYER_AGENT is optional. It names the agent the builds run, and it defaults to claude.
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.

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.

Check that it runs

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:
Then name the image by its digest in docker run:
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:
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:
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 lease only while a runner renews it. A recreate that takes longer than the lease 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:
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.