Local GitHub runners (macOS VM)
Running GitHub Actions on your own macOS VM is one of the best ways to avoid “renting your own infrastructure”.
This guide shows a practical setup using CiderStack + GitHub’s self-hosted runner, and an ephemeral runner workflow using snapshots/clones.
What you’ll build
- A macOS VM that registers as a self-hosted GitHub Actions runner
- Optional: a golden image + ephemeral clones (one clean runner per job)
Before you start
- Apple limitation: only 2 macOS VMs can run concurrently per physical Mac (Apple platform rule). To scale runners, you scale by adding Macs.
- Security: treat self-hosted runners as sensitive—prefer org-level runners with tight permissions, and strongly prefer ephemeral runners for untrusted repos/PRs.
Option A (simple): one “always-on” runner VM
1) Create and boot a VM
Using the CLI:
# Create a VM (use a template or image you have)
cider vm create gh-runner --template "Xcode-15"
# Or: cider vm create gh-runner --image sonoma-arm64 --cpu 4 --memory 8
# Start headless (no window; good for CI)
cider vm start gh-runner --headless
# Wait for an IP, then SSH in
cider vm ip gh-runner --wait 120
cider ssh gh-runner2) Install and configure the GitHub runner inside the VM
In your GitHub repo (or org), go to:
- Settings → Actions → Runners → New self-hosted runner
Then follow GitHub’s instructions inside the VM (they provide the exact download + config commands).
Recommended runner labels:
macosciderstackapple-siliconxcode-15(or whatever matches your VM image)
3) Run it as a service
Use GitHub’s “run as a service” instructions so the runner survives reboots and comes up automatically.
4) Target it from workflows
In your workflow:
runs-on: [self-hosted, macos, ciderstack]Option B (recommended): ephemeral runners with snapshots/clones
This pattern gives you a clean runner per job and makes teardown easy.
1) Build a “golden runner” VM
Create a VM, install macOS if needed (cider install myvm --latest), then inside the VM install Xcode and the GitHub runner prerequisites. Do not permanently register the runner if you plan to clone.
Create a snapshot so you can restore or clone from it:
cider snapshot create runner-base "runner-clean"
# Or use a template: cider template create "CI Runner Base" --vm runner-base2) For each job: clone → register → run → destroy
High-level flow:
- Clone from your golden VM:
cider vm clone runner-clean runner-job-123 - Start and get IP:
cider vm start runner-job-123 --headless, thencider vm ip runner-job-123 --wait 120 - SSH in, register with GitHub using a short-lived registration token, run the job
- Teardown: de-register the runner, then
cider vm destroy runner-job-123 -f
This is the safest approach for PRs and minimizes state drift. For automation, use cider vm create ... --intent ci --ttl 2h so VMs can auto-retire.
3) Scaling out
Because Apple limits concurrency per host, scaling is typically:
- Add more Macs
- Spread runners across the fleet (Fleet Manager helps here; it’s currently in beta with early access for Small Batch)
Common gotchas
- Docker inside the macOS VM: Docker Desktop needs nested virtualization; macOS doesn’t support it today.
- Keychain / codesigning: signing identities and notarization credentials need extra care—keep secrets scoped, prefer ephemeral VMs, and avoid baking secrets into images.
- Updates: update Xcode and macOS in the golden image, then re-snapshot.
Next
- CLI Reference —
cider vm create,cider ssh,cider snapshot,cider vm clone/destroy - Quick Start
- Key Concepts
- Use Cases