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 recordcontainer, 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 initoffers 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 refuseshost.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 loginon 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,apiKeyandappId; setup moves them into the VM instead.
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:processhooks 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
runnerVmkey that a finishedrunner init --vmwrites, or a Lima instance namedouterlayer-runnerexists. 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 --vmrunslimactl. It refuses withbrew install limawhenlimactlis not on yourPATH. If a step in the VM fails, the Mac’s config is unchanged; delete the half-built VM withlimactl 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
concurrencyandbuildlimits, and never gets more CPUs or memory than the Mac has.- CPUs: each build is pinned to
build.cpuscores of its own, rounded up. The VM gets that count timesconcurrency, plus one. - Disk: the sum of
build.diskfor every build but the last, the larger ofbuild.diskandhousekeeping.minFreeDisk, the Docker build cache’s cap, and 30 GiB for the OS and images. Lima allocates it as it is used. - Memory:
concurrencytimesbuild.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.concurrencyorrunner.build.memory.
~/.lima/outerlayer-runner/lima.yaml, then runlimactl stop outerlayer-runnerandlimactl start outerlayer-runner. - CPUs: each build is pinned to
- 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 storesCLAUDE_CODE_OAUTH_TOKEN=<token>in~/.outerlayer/claude.envin the VM, readable only by its owner, before the service starts. With--agent claudeand nothing piped, setup refuses before it makes the VM.
What lives where
The VM is a Lima instance namedouterlayer-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:
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, andouterlayer 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:
~/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 usesbuiltin: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.