Baremetal documentation
Everything you need to point the Macs and Linux machines you own at GitHub and start running ephemeral CI in minutes.
Overview
Baremetal runs your GitHub Actions jobs on Macs you own, as ephemeral, single-use macOS VMs. You bring a GitHub org or repo and one or more Apple Silicon Macs; Baremetal connects them, scales runners to demand, and tears each VM down after its job.
The moving parts: a workspace (your account), one or more connected orgs (your GitHub Apps), machines (your Macs, running a worker), and pools (how many runners of which image to keep warm).
Requirements
macOS runners need:
- An Apple Silicon Mac (M1 or newer) running a recent macOS.
- Homebrew installed (for Tart and Node, which the agent uses).
- The Mac plugged into power — sleep is disabled so runners stay available.
Linux runners need any Linux box with KVM (hardware virtualization, /dev/kvm) and QEMU — the installer provisions these. Each job runs in a fresh, ephemeral KVM virtual machine with its own kernel, so Docker, service containers, and nested KVM all work inside it (no Apple hardware needed).
You connect either a whole GitHub organization or a single repository (including a private repo on a personal account) that you can administer — that’s where the GitHub App is created. Intel Macs aren’t supported (Tart requires Apple Silicon).
1 · Connect a GitHub account
- Sign in and open your workspace, then go to Connect.
- Choose your runner target — an organization (runners shared by every repo) or a single repository — then follow the guided flow. It creates a GitHub App scoped to exactly that target with only the permissions needed to register self-hosted runners.
- Install the app on your org or repo when prompted.
The app’s private key is stored encrypted at rest — never on your machines, never in our database. You can rotate or revoke it from GitHub at any time. See the security model for details.
2 · Add a machine
In your workspace, open Add machine and mint a one-time join token. Then run the one-line installer — the same command on macOS and Linux; it detects the platform automatically.
curl -fsSL https://baremetal.run/install.sh | bash -s -- cjt_your_token
macOS (Apple Silicon): runs the agent under launchd. It clones an ephemeral Tart VM per job, runs the runner inside with a single-job config, and destroys it afterwards. Requires Tart and Node; disables idle sleep on power so runners stay online.
Linux: runs the agent as a systemd service, which boots an ephemeral KVM virtual machine per job (a copy-on-write clone of a cloud image), runs the runner inside it, and destroys it afterwards. Requires KVM (/dev/kvm) and QEMU, which the installer provisions.
The machine appears on your dashboard within a minute. Join tokens are single-use and expire after 15 minutes — mint a fresh one per machine.
3 · Pools & images
A pool tells Baremetal how many runners of a given image to keep ready for an org. Set the image (for example a macOS Tahoe base image), the VM size (CPU and memory), and the desired count.
A pool is demand, not hardware. You don’t attach machines to a pool — your machines are shared capacity for the whole workspace, and Baremetal schedules each pool’s runners onto whichever machines have a free slot. One machine can run runners for two different pools or orgs at once; a pool’s runners can land on any of your machines of the matching platform.
Each pool has a platform, chosen when you create it: macOS (an ephemeral Tart VM per job, on your Macs) or Linux (an ephemeral KVM virtual machine per job, on your Linux boxes).
- Desired count — how many warm runners to maintain. Baremetal scales up to this and down to zero when there’s no demand.
- Labels — the runner labels your workflows target with
runs-on. - Image — a macOS pool takes any Tart-compatible macOS image; a Linux pool takes a cloud image (an Ubuntu cloud
qcow2URL). Images are cached on your machines so jobs start fast.
Sizing & capacity
macOS allows up to two VMs per host, so each Mac contributes up to two concurrent runners. Capacity is counted in slots (2 per Mac), not RAM — Baremetal does not currently check that a pool’s VM size fits your machines, so it’s on you to size the VM so that two of them fit in the host’s memory, leaving ~3–4 GB of headroom for macOS itself.
On Linux there’s no per-host VM cap — concurrent KVM runners are bounded only by the host’s CPU and RAM, so size a pool’s VM to leave room for the number you want running at once.
Example: a 16 GB Mac comfortably runs 2 × 6 GB VMs, or 1 × 8 GB. If you ask for 8 GB VMs on a 12 GB Mac, the scheduler will still place two of them (it only counts slots) — but 16 GB of guests on 12 GB of RAM means heavy swapping and possibly a VM that fails to start. Either drop the VM memory, or use a Mac with more RAM.
4 · Run a workflow
Point a workflow at your pool’s labels and push:
jobs:
build:
runs-on: [self-hosted, macos, arm64]
steps:
- uses: actions/checkout@v4
- run: xcodebuild test ...Each run is handed a fresh VM. When the job ends, the VM — and everything in it — is destroyed. Watch live workers, VMs, and capacity on your dashboard.
How it works
The flow for a single job:
- A workflow needs a runner with your pool’s labels.
- Baremetal mints a just-in-time, single-job runner registration token using your GitHub App key (which never leaves the backend).
- The Baremetal agent on one of your machines boots a fresh runner environment — a Tart VM on macOS, a KVM VM on Linux.
- It registers as an ephemeral runner and executes the job.
- The environment is destroyed; the registration is cleaned up.
Everything is outbound: your machines dial the cloud; nothing reaches in, so there are no inbound ports to open. macOS runners are built on the open-source Tart VM engine; Linux runners on QEMU/KVM and the official actions/runner.
Public repositories
A workflow triggered by a pull request on a public repo runs the contributor’s code — anyone on the internet can open a PR, so treat it as untrusted. Baremetal contains this on the runner side: every VM runs on an isolated, egress-filtered network (Linux via nftables, macOS via Tart Softnet), so a job can reach the public internet and DNS but not your LAN, internal services, the cloud metadata endpoint, the host, or other runners. It’s single-use and destroyed after one job.
A few controls live on GitHub’s side, not Baremetal’s — set these on any public repo that uses self-hosted runners:
- Require approval for fork PRs. Settings → Actions → “Fork pull request workflows” → require approval for all outside collaborators (ideally all external contributors), so a runner only starts after a maintainer eyeballs the PR.
- Restrict the runner group. Keep public-repo runners in their own group so a public repo can’t grab a runner meant for private work.
- Default
GITHUB_TOKENto read-only —permissions: contents: read— and grant more only where a job needs it. - Never combine
pull_request_targetwith a checkout and build of the PR head — that runs untrusted code with write-scoped secrets.
Removing a machine
To take a machine out of your fleet, run the installer's uninstall mode on that machine:
curl -fsSL https://baremetal.run/install.sh | bash -s -- uninstall
This removes the Baremetal agent (its systemd or launchd service, saved identity, and any in-flight runners) and deregisters the machine from your workspace — it disappears from the dashboard and its GitHub runner registrations are cleaned up. QEMU, Tart, and Node are left installed.
No longer have access to the host? You can also revoke a machine from Overview → Machines in the dashboard — the agent stops itself on its next check-in. Running uninstall on the host is still the way to remove the local agent and its files.
Troubleshooting
- My Mac isn’t showing up
- Re-run the installer with a fresh join token (they’re single-use and expire in 15 minutes). Check the agent logs — on macOS at
~/.baremetal/agent.log, on Linux withjournalctl -u baremetal-agent. - Jobs queue but never pick up
- Make sure your workflow’s
runs-onlabels match the pool’s labels, the pool’s desired count is above zero, and at least one Mac is online. - “Integration not found” when connecting
- The GitHub App wasn’t fully created or installed. Re-run Connect and complete both the create and install steps.
- Runners stop overnight
- The Mac is sleeping. Keep it on power; the installer disables idle sleep, but a closed lid can still suspend it.