Skip to main content

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, 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 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.
--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:
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.

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. To upgrade by hand, install the new CLI in the VM, put it in the runner’s install layout, then restart the service:
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:
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 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. outerlayer doctor names the fix: outerlayer runner init --vm moves the runner block into the VM.