Skip to main content

Running on macOS

A Mac cannot give a build a container of its own, but a Linux VM can. You have two ways to get one. Either way, builds run in containers and record container, as on a Linux host. Both work on Apple silicon and on Intel Macs.
  • Docker Desktop and the runner image. Use this when the Mac already runs Docker Desktop. You start the runner with one docker run. Docker Desktop’s own Linux VM runs the builds. See Run the runner in a container.
  • A Lima VM. Use this only when the Mac has no Docker. outerlayer runner init offers to make a small Linux VM with Docker Engine, and the rest of this page covers it.

Docker Desktop and the runner image

Follow Run the runner in a container. The runner in the image reads Docker Desktop’s VM, not the Mac, so its build limits match the VM. Raise the VM’s CPUs and memory in Docker Desktop’s settings if the builds need more. A build’s container cannot reach the Mac. Its only network is the runner’s tunnel, which refuses host.docker.internal, localhost, Docker Desktop’s own address range and the Mac’s home network. A build mounts none of the Mac’s folders. The image build runs outside the tunnel, so it can reach the Mac’s local services and home network. See Security and limits. The engine behind Docker Desktop must be version 26 or later. Check it with docker version --format '{{.Server.Version}}'. A runner installed directly on the Mac does not use Docker Desktop: outerlayer runner init there offers only the Lima VM or process builds.
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 before you choose the image over Lima. Lima is free.

A Lima VM

Use Lima only when the Mac has no Docker. outerlayer runner init offers to run the runner in a small Linux VM with Docker Engine. Before you start with Lima:
  • Lima, installed with brew install lima.
  • A login: run outerlayer login on the Mac. It tells setup which factories you can use.
  • A runner key. Setup prints the link to the factory’s API keys page and asks you to paste a key made with the Runner preset. It checks the key and hands it to the VM. The key is never written on the Mac, and your login stays as it is. If you cannot create the key, put one a member made in a config file of its own with url, apiKey and appId; setup moves them into the VM instead.
If you have never run outerlayer runner init on this Mac, nothing else applies: go to the commands below. Otherwise, setup checks what is already there:
  • A runner block with builtin:process hooks moves into the VM, without its hooks. Setup refuses a block whose hooks are anything else. Remove that block and run setup again.
  • Setup refuses when the runner already lives in a VM: the config has the runnerVm key that a finished runner init --vm writes, or a Lima instance named outerlayer-runner exists. The refusal says how to start or delete that VM.
<org>/<factory> is the factory’s name in its dashboard address, app.outerlayer.ai/orgs/<org>/factories/<factory>. Leave --factory out if your login can use only one factory: setup uses it and says which. Setup then asks for the runner key. Paste it when asked; nothing is shown as you paste. --agent claude writes the commands that start Claude Code in the VM’s runner block, and stores the token in the VM. You do not open a shell in the VM. Leave --agent out to use another agent; see Using another agent. --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 the runner image and --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’s environment or files. Setup hands the token you pipe in to the VM the same way it hands over the runner key: on the stdin of the VM’s own runner init. That stores CLAUDE_CODE_OAUTH_TOKEN=<token> in ~/.outerlayer/claude.env in the VM, readable only by its owner, before the service starts. With --agent claude and nothing piped, setup refuses before it makes the VM.

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 gains a runnerVm note. A runner key created from your login was never on the Mac. A key that was in the config loses its place there: the config loses apiKey and runner. If you used that key for capture on the Mac, run outerlayer login there again. 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 --agent claude, the service starts with Claude’s commands and credential, and takes work. Check it from the Mac with limactl shell outerlayer-runner outerlayer runner check. Without --agent, the runner block has placeholder commands, and the service runs but claims nothing. The runner’s entry in the factory says why. Edit the commands, put your agent’s credential in runner.env, 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 the runner 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, then put it in the runner’s install layout:
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. Do not restart the service yourself. The runner restarts on the new version at once when its running builds carry on through a restart. Otherwise it waits for them to finish. See Upgrade by hand. 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:
That writes Claude’s commands and stores the credential in ~/factory-b/claude.env. Give it a runner key of its own: a runner key works only with the first host key that signs for 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 two fixes, in this order: run the runner image on Docker Desktop, or run outerlayer runner init --vm to move the runner block into a VM.