> ## 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 builds on macOS

> On a Mac the runner lives in a small Linux VM, so builds still run in containers of their own.

## Running on macOS

On macOS, `outerlayer runner init` offers to run the runner in a small Linux
VM with Docker Engine. A Mac cannot give a build a container of its own, but
the VM can. Builds there run in containers and record `container`, as on a
Linux host. It works on Apple silicon and on Intel Macs.

Before you start:

* [Lima](https://lima-vm.io/docs/installation/), installed with
  `brew install lima`.
* A config file that holds `url`, `apiKey` and `appId`. Setup moves them
  into the VM. If you also use `outerlayer` on this Mac, put the runner key
  in [a config file of its own](/set-up-a-host#give-the-runner-its-key)
  and add `--config <file>` to each command below, so your own login in
  `~/.outerlayer/config.json` stays as it is.
* An existing runner block moves into the VM, without its hooks. Setup
  refuses a block whose hooks are anything other than `builtin:process`.
* Setup refuses when the config already has a `runnerVm` note, or a Lima
  instance named `outerlayer-runner` already exists.

```bash theme={"system"}
brew install lima
outerlayer runner init --vm
```

`--vm` answers yes without asking, and `--no-vm` answers no. With neither, a
terminal gets the question and a script gets a refusal that names `--vm`.

### What the VM needs

* **Lima.** `outerlayer runner init --vm` runs `limactl`. It refuses with
  `brew install lima` when `limactl` is not on your `PATH`. If a step in the
  VM fails, the Mac's config is unchanged; delete the half-built VM with
  `limactl delete --force outerlayer-runner`.
* **Room for the builds.** With the default limits the VM gets 5 CPUs,
  11 GiB of memory and 90 GiB of disk. The VM is sized from the runner's
  `concurrency` and `build` limits, and never gets more CPUs or memory than
  the Mac has.

  * CPUs: each build is pinned to `build.cpus` cores of its own, rounded
    up. The VM gets that count times `concurrency`, plus one.
  * Disk: the sum of `build.disk` for every build but the last, the larger
    of `build.disk` and `housekeeping.minFreeDisk`, the Docker build cache's
    cap, and 30 GiB for the OS and images. Lima allocates it as it is used.
  * Memory: `concurrency` times `build.memory`, plus 2 GiB for the runner,
    divided by 0.95 and rounded up. A Linux guest reports less memory than
    it is given.
  * Setup makes no VM when the builds do not fit in the memory the VM will
    report. On a Mac with too little memory, lower `runner.concurrency` or
    `runner.build.memory`.

  To change the size later, edit `~/.lima/outerlayer-runner/lima.yaml`, then
  run `limactl stop outerlayer-runner` and `limactl start outerlayer-runner`.
* **A network.** The first run downloads an Ubuntu image, Node and the CLI.
* **Your Claude credential, in the VM.** It is not copied from the Mac. In
  the VM, put `CLAUDE_CODE_OAUTH_TOKEN=<token>` in `~/.outerlayer/runner.env`,
  run `chmod 600 ~/.outerlayer/runner.env`, and restart a running service:
  `limactl shell outerlayer-runner sudo systemctl restart outerlayer-runner`.

### What lives where

The VM is a Lima instance named `outerlayer-runner`, with no directory of the
Mac mounted, so a build cannot read your home directory. It holds the CLI,
the runner and host keys, and a systemd service, `outerlayer-runner`.

The Mac's config loses `apiKey` and `runner`, and gains a `runnerVm` note.
If you use capture on the Mac, run `outerlayer login` there again with a key
that is not the runner key. A runner key bound to an old Mac host key is
refused until a member uses **Clear host key** on the API keys page.

A launch agent, `ai.outerlayer.runner-vm`
(`~/Library/LaunchAgents/ai.outerlayer.runner-vm.plist`), runs
`limactl start outerlayer-runner` when you log in.

### Enable the service

`outerlayer runner init --vm` enables and starts the service for you. It
runs `outerlayer runner install --service` inside the VM, which writes the
same unit as on Linux. You write no unit. The launch agent starts the VM at
login, and the VM starts the service. Before login, the runner does not
run.

With placeholder commands in the runner block, or no credential in
`runner.env`, the service runs but claims nothing. The host's entry in the
factory says why. Edit the commands and restart the service:

```bash theme={"system"}
limactl shell outerlayer-runner
$EDITOR ~/.outerlayer/config.json
sudo systemctl restart outerlayer-runner
```

To update the service after the config changes, run
`outerlayer runner install --service` on the Mac. It runs the same command
inside the VM and rewrites the launch agent. To remove the service and the
launch agent, run `outerlayer runner uninstall --service` on the Mac. See
[Run a host as a service](/run-a-host-as-a-service).

### Upgrade and operate the VM

The runner in the VM updates itself to the CLI version your factory names.
It does not follow upgrades of the CLI on the Mac. Setup leaves npm 9.5 or
later in the VM, which self-update needs to verify a release's provenance.
See [Keep the runner up to date](/run-a-host-as-a-service#keep-the-runner-up-to-date).

To upgrade by hand, install the new CLI in the VM, put it in the runner's
install layout, then restart the service:

```bash theme={"system"}
limactl shell outerlayer-runner
sudo npm install --global @outerlayer/cli@<version>
outerlayer runner install --service
sudo systemctl restart outerlayer-runner
```

`npm install --global` alone does not upgrade the runner. The service
starts the CLI under `~/.outerlayer/runner/current`, and only
`runner install --service` moves that link. Restart when no build is
running, or the restart interrupts it.

To try a CLI build that is not published, pack it with `npm pack` and give
the tarball to `outerlayer runner init --vm --vm-cli-package <file>`.

Run runner commands through `limactl shell outerlayer-runner`, for example
`outerlayer runner status` or `outerlayer runner logs <item>`. `outerlayer doctor` on the Mac reports the VM,
and says to run `limactl start outerlayer-runner` when it is stopped. It reads
only `~/.outerlayer/config.json` and has no `--config` option. With the runner
key in a config file of its own, `doctor` does not see the VM, so use
`limactl list` to check that it is running.

### A second factory on the same Mac

One Mac runs one VM, and `outerlayer runner init --vm` refuses to make a
second. The one VM can still run a runner for a second factory. Each runner
needs its own config file in a directory of its own, and its own service.
Do this inside the VM:

```bash theme={"system"}
limactl shell outerlayer-runner
mkdir -p ~/factory-b
( umask 077; printf '{"url":"https://api.outerlayer.ai","apiKey":"%s","appId":"<second-factory-id>"}\n' "$RUNNER_KEY_B" > ~/factory-b/config.json )
outerlayer runner init --config ~/factory-b/config.json --unit-name outerlayer-runner-b
```

Then edit `runner.commands` in that file, put the Claude credential in
`~/factory-b/runner.env`, and restart `outerlayer-runner-b`. Give it a
runner key of its own: a runner key works only from the first host that
uses it. The second runner keeps its host key and its install layout under
`~/factory-b/runner/`, so it updates itself apart from the first.

The VM was sized for one runner's builds. Both runners draw on its CPUs,
memory and disk, so raise them as
[What the VM needs](#what-the-vm-needs) shows. The Mac's launch agent starts
the VM, and so both runners, at login. `outerlayer doctor` on the Mac
reports only the first.

### What process hooks give up

Answer no, and the runner uses `builtin:process`. Builds run on the Mac as
the runner's user, record `shared-user`, and are **not isolated**. See
[The process fallback](/write-your-own-hooks#the-process-fallback).
`outerlayer doctor` names the fix: `outerlayer runner init --vm` moves the
runner block into the VM.


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