
For an ARM Buildkite agent that builds and signs iOS apps, request a Figment M-series Mac starting at $95/month — the exact chip generation, RAM, and disk are confirmed in the quote, and physical plans are request-based rather than instant. If you still need an Intel toolchain (for example, a project pinned to Xcode 26 or earlier on x86_64), the $29/month Figment virtual Intel macOS VM (2 vCPU, 8 GiB RAM, 80 GiB disk; access via the browser console, no GPU or audio) can host an Intel Buildkite agent. It is a real Intel macOS environment that runs compatible toolchains directly. Start a request at /mac-hosting/start.
The two primary sources you will keep open while reading this are the Buildkite macOS install docs and the Buildkite pipeline step reference.
The two models solve different problems:
| Factor | GitHub-hosted macOS runner | Self-hosted Buildkite agent on a Mac |
|---|---|---|
| Billing model | Per-minute, metered against your Actions allotment | Flat monthly rate for the host |
| Xcode versions | Whatever GitHub's image ships | Any version you install |
| Caches between jobs | Discarded | Preserved on disk |
| Signing identity | Re-imported each job | Installed once in a persistent keychain |
| Concurrency | Shared pool, queued | As many executors as the Mac can sustain |
If your team runs more than a handful of iOS builds per day, flat-rate hardware generally beats metered minutes once you factor in warm caches and signing setup. The crossover depends on your per-minute rate and build length — work the math with your own numbers before choosing.
Open a terminal session on the Mac. On the Figment Intel VM, use the browser console's desktop terminal; on a physical Mac host, use whichever access path your quote sets up. Then:
# Install the agent from the official Buildkite tap
brew install buildkite/buildkite/buildkite-agent
# Write your agent token into the installed config
sed -i '' "s/xxx/INSERT-YOUR-AGENT-TOKEN-HERE/g" \
"$(brew --prefix)/etc/buildkite-agent/buildkite-agent.cfg"
Grab the token from your Buildkite organization under Agents → New Agent. Config paths differ by architecture (see the install docs):
| Architecture | Config path |
|---|---|
| Apple silicon | /opt/homebrew/etc/buildkite-agent/buildkite-agent.cfg |
| Intel | /usr/local/etc/buildkite-agent/buildkite-agent.cfg |
Smoke-test the connection:
buildkite-agent start
# Look for: "Starting buildkite-agent" followed by "successfully connected"
Stop it with Ctrl-C and move on to tags.
Tags are how Buildkite picks an agent. A step that declares agents: { xcode: "16" } will only run on an agent that advertises xcode=16. All keys on the step must match.
A working buildkite-agent.cfg for an M-series Mac:
# buildkite-agent.cfg — Apple silicon CI node
token="INSERT-YOUR-AGENT-TOKEN-HERE"
# Agent display name; %n differentiates spawned siblings
name="mac-ci-%n"
# Routing tags consumed by pipeline.yml
tags="xcode=16,os=macos,arch=arm64,queue=ios-builds"
# Build workspace (Apple silicon default)
build-path="/opt/homebrew/buildkite-agent/builds"
# Hooks directory (pre-command, post-command, etc.)
hooks-path="/opt/homebrew/etc/buildkite-agent/hooks"
# Run two concurrent agents from one config
spawn=2
Pick the Xcode value that matches what you actually install. Intel hosts can run Xcode 26 or earlier (see Apple's porting notes for Apple silicon); Xcode 27 requires Apple silicon. visionOS simulators also require Apple silicon. For anything newer than Xcode 26, choose the M-series host.
If you are migrating from an older agent config, note that cancel-grace-period from agent v3 is replaced in v4 by --cancel-signal-timeout and --cancel-cleanup-timeout; check the v3-to-v4 upgrade guide before copying older snippets.
Restart the service to pick up the changes:
brew services restart buildkite/buildkite/buildkite-agent
Both spawned agents should appear in the Buildkite UI under Agents.
Put the following at .buildkite/pipeline.yml in your repository. Replace the command paths and the scheme name with values that exist in your project — do not copy an invented scheme.
# .buildkite/pipeline.yml
steps:
- label: ":hammer_and_wrench: Build iOS"
key: build-ios
command: ".buildkite/scripts/build-ios.sh"
agents:
xcode: "16"
os: macos
arch: arm64
timeout_in_minutes: 30
artifact_paths: "build/**/*.ipa"
- label: ":test_tube: Unit tests"
key: test-ios
command: ".buildkite/scripts/test-ios.sh"
agents:
xcode: "16"
arch: arm64
timeout_in_minutes: 20
- label: ":rocket: Upload to TestFlight"
key: upload-testflight
command: "bundle exec fastlane beta"
depends_on: build-ios
agents:
xcode: "16"
queue: ios-builds
timeout_in_minutes: 15
Inside build-ios.sh, invoke xcodebuild with flags that actually exist — -project or -workspace, -scheme, -configuration, -destination, and -derivedDataPath. Let the script exit on failure (set -euo pipefail) so Buildkite marks the job red instead of recording a fast-but-broken run.
Agent hooks live in hooks-path. Store the unlock password in Buildkite's pipeline Environment Variables and reference it by name; never commit it to the hook file, and do not export the agent's full environment to disk — real secrets leak that way.
hooks/pre-command — unlock the signing keychain only when a step needs it:
#!/usr/bin/env bash
set -euo pipefail
if [[ "${BUILDKITE_LABEL:-}" == *"Build iOS"* || \
"${BUILDKITE_LABEL:-}" == *"TestFlight"* ]]; then
security unlock-keychain -p "$KEYCHAIN_PASSWORD" \
"$HOME/Library/Keychains/CI.keychain-db"
echo "--- keychain unlocked"
fi
Provisioning profiles are plain files, not keychain items — install them to ~/Library/MobileDevice/Provisioning Profiles/ separately. Signing identities (certificate + private key) belong in an encrypted .p12 that you import into the CI keychain during machine bootstrap, not on every build.
hooks/post-command — keep disk use predictable:
#!/usr/bin/env bash
set -euo pipefail
rm -rf "$HOME/Library/Developer/Xcode/DerivedData/ModuleCache.noindex"
echo "--- DerivedData module cache cleared"
Mark both executable: chmod +x hooks/pre-command hooks/post-command.
If a build has to reach your own artifact store, binary cache, or a corporate TestFlight upload proxy, update the allowlist on that specific service. Do not add rules for invented App Store Connect IP ranges — Apple does not publish a static one for upload traffic.
spawn=2 launches two agent processes from one config; each takes one executor slot. How many an individual Mac can sustain depends on the exact chip, core layout, and RAM in your quote — we do not publish a benchmark. A conservative starting point is one executor per 8 GiB of installed RAM, then measure and adjust. While a parallel build is running:
top -l 5 -s 2 -o cpu | head -30
If wall-clock time jumps materially, drop spawn or ask for a larger configuration.
Treat this as an entry-price comparison. Chip, RAM, and disk differ across these plans, and physical Figment plans collect requests until supply is confirmed, so verify specifics in the actual quote.
| Provider / plan | Headline rate | 12-month spend | What's listed on the vendor page |
|---|---|---|---|
| Figment M-series (entry) | $95/mo | $1,140 | M-series Mac; exact chip/RAM/disk confirmed in quote (mac-hosting) |
| Figment Managed Build Mac | $249/mo | $2,988 | One M-series Mac plus CI setup, backups, monitoring, 30 min/mo support |
| CloudMac monthly | $99.99/mo | $1,199.88 | Mac mini M4, 16 GB, 256 GB (256 GB "Reserve now"; 512 GB/1 TB "Fully reserved") |
| CloudMac yearly | $999/yr | $999 | Same M4 base config, prepaid annual |
| MacStadium M4.S | $149/mo | $1,788 | M4, 16 GB, 256 GB; support and 99.99% uptime claim on the pricing page |
A few honest observations from those numbers:
agents: block.Related guides