The Thornebridge GitHub Actions self-hosted runner. Lives on a dedicated Mac mini (Apple Silicon), serves all org repos — lend.works, banklyze, pm, ccs-marketing, anything tagged with the buildserv label.
🤖 Provisioning a new Mac mini? Paste
docs/setup-agent-prompt.mdinto a Claude Code session on the new machine and the agent will walk through the entire setup. That doc is the canonical entry point — start there.
The previous buildserv was set up by hand on a Mac that died, taking its configuration with it. This repo is the explicit, version-controlled answer: anyone with the disk, an env file, and ./setup.sh can rebuild the entire setup in 30 minutes.
One Mac mini, two GitHub Actions runner processes registered against the thornebridge org:
| Runner | Labels | Used for |
|---|---|---|
<hostname>-light |
self-hosted, macOS, ARM64, buildserv, light |
Lint, typecheck, deploys, notify — anything cheap |
<hostname>-heavy |
self-hosted, macOS, ARM64, buildserv, heavy |
Tests, RLS integration, Docker builds — resource-intensive |
Running both lets independent jobs in the same workflow execute in parallel (e.g. build-images heavy + deploy-staging light overlap by ~10-15 min on a typical lend.works PR).
On a fresh Mac mini logged in as a non-root user:
# 1. Clone this repo
git clone https://github.com/thornebridge/buildserv.git /opt/buildserv-repo
cd /opt/buildserv-repo
# 2. Configure env
cp env.example $HOME/.buildserv.env
# Edit $HOME/.buildserv.env — see header comments for each var
# 3. Generate a fresh runner registration token (expires in 1 hour)
gh api -X POST /orgs/thornebridge/actions/runners/registration-token | jq -r .token
# Paste the token into $HOME/.buildserv.env as RUNNER_TOKEN
# 4. Run setup — idempotent, safe to re-run on errors
./setup.shAfter the first run completes, both runners should appear as Idle at https://github.com/organizations/thornebridge/settings/actions/runners.
./setup.sh --phase system # Xcode CLT + Homebrew + workspace dirs
./setup.sh --phase tools # Node 22, pnpm 10, gh, jq, tmux, htop
./setup.sh --phase docker # Docker Desktop + GHCR login + buildx
./setup.sh --phase runner # Download + register both runners
./setup.sh --phase launchd # LaunchAgents for auto-start
./setup.sh --phase cache # Persistent pnpm + Buildx cache
./setup.sh --phase ops # Weekly Docker prune, disable sleep, SSH./scripts/status.sh # Health snapshot — runner state, disk, cache sizes
./scripts/restart.sh # Reload both LaunchAgents
./scripts/update.sh # Pull latest actions-runner binary, restart
./scripts/unregister.sh # Deregister from GitHub (use before retiring host)Logs live in /opt/buildserv/logs/:
runner-light.{out,err}— light runner outputrunner-heavy.{out,err}— heavy runner outputprune.{log,out,err}— weekly Docker prune
Tail them with tail -f /opt/buildserv/logs/runner-light.out.
A few Docker Desktop settings can't be set from the CLI. The first time you launch Docker.app, configure:
- General → Use Rosetta for x86_64/amd64 emulation on Apple Silicon: ON
Native amd64 cross-compilation; 2-3× faster than the default QEMU. - Resources → CPUs: max
- Resources → Memory: ≥ 8 GB (12 GB on a 16 GB mini)
- Resources → Swap: 2 GB
- Resources → Disk image size: ≥ 80 GB
- Resources → File sharing: add
/opt/buildservso Buildx cache works - Builders → containerd image store: ON
/opt/buildserv/
├── runners/
│ ├── light/ # GH Actions runner install (light labels)
│ └── heavy/ # GH Actions runner install (heavy labels)
├── cache/
│ ├── pnpm-store/ # Shared pnpm store across all builds
│ ├── buildx/ # Symlinked into /tmp/.buildx-cache-*
│ └── turbo/ # Turbo remote cache (if used)
└── logs/ # Runner + prune logs
/opt/buildserv-repo/ holds this repo (the source). The setup script keeps these two paths separate so you can git pull updates without touching live runner state.
# On the replacement mini, after logging in:
git clone https://github.com/thornebridge/buildserv.git /opt/buildserv-repo
cd /opt/buildserv-repo
cp env.example $HOME/.buildserv.env
# Fill in RUNNER_TOKEN, GHCR_TOKEN — see header
./setup.shIf the dead mini's disk is recoverable, restoring /opt/buildserv/cache/ to the new mini will give the first builds a warm cache (~10× faster Docker layer reuse).
GitHub Actions self-hosted runners are single-threaded: one runner = one concurrent job. Two runners on the same hardware means our build-images (heavy) and deploy-staging (light) can overlap in the same workflow, cutting wall-clock pipeline time meaningfully on every PR.
Proprietary — Thornebridge LLC.