Guide

How to set up GitLab Runner on a Mac for iOS builds

To run GitLab CI jobs on a Mac, install the official gitlab-runner binary and register it with the shell executor. Create the runner in your project's CI/CD settings first, which gives you a glrt- token. Then install it as a user LaunchAgent and turn on automatic login, so it returns after a reboot. Run the install from a terminal on the Mac's desktop, not over SSH, as GitLab's docs require.

What you need first

  • An Apple silicon Mac with admin access and Xcode installed.
  • Xcode's first launch done. Run sudo xcodebuild -runFirstLaunch once if you are not sure.
  • Permission to manage runners in your GitLab project or group.
  • A desktop session on the Mac, through screen sharing or a monitor. GitLab's macOS install page says to use a local GUI terminal, not an SSH session.
  • The macOS account that will run the jobs. Sign in to the desktop as that user.

1. Download the runner binary

GitLab says it does not maintain the Homebrew formula and recommends the official binary. On Apple silicon, use the arm64 build. A fresh Mac may not have /usr/local/bin yet, so create it first.

sudo mkdir -p /usr/local/bin
sudo curl --output /usr/local/bin/gitlab-runner \
  "https://s3.dualstack.us-east-1.amazonaws.com/gitlab-runner-downloads/latest/binaries/gitlab-runner-darwin-arm64"
sudo chmod +x /usr/local/bin/gitlab-runner
gitlab-runner --version

2. Create the runner in GitLab

Runner registration tokens are deprecated. GitLab plans to remove them in GitLab 20.0. The current flow creates the runner in the UI first, then gives you a runner authentication token. That token starts with glrt-.

  • In your project, open Settings, then CI/CD, then expand Runners.
  • Select Create project runner and choose macOS.
  • In Tags, enter macos, xcode. Leave Run untagged jobs off, so only jobs that ask for a Mac land here.
  • Select Create runner and copy the token. It is shown only briefly.

Tags now live on the runner in GitLab. GitLab's docs say settings like --tag-list and --run-untagged can only be set when the runner is created, in the UI or with the API. Change tags later from the runner's Edit page.

3. Register with the shell executor

GitLab's macOS install page points to the shell executor for iOS and macOS builds. Jobs run directly on the Mac, as your user, with Xcode and the simulators. Register without prompts like this:

export RUNNER_TOKEN="glrt-paste-your-token-here"
gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --token "$RUNNER_TOKEN" \
  --executor "shell" \
  --description "mac-mini-m6"

On self-managed GitLab, use your instance URL instead. The settings land in ~/.gitlab-runner/config.toml. GitLab notes the shell executor is in maintenance mode. It still gets security fixes, and it is still what the macOS page recommends for Xcode work.

4. Install and start the service

cd ~
gitlab-runner install
gitlab-runner start
gitlab-runner status

This writes ~/Library/LaunchAgents/gitlab-runner.plist. On macOS the runner is a user LaunchAgent, and GitLab says that is the only supported mode. It runs as you, not root. It can reach your keychain and your login session, which the iOS Simulator and code signing need. Logs go to ~/Library/Logs/gitlab-runner.out.log and gitlab-runner.err.log.

5. Make it survive a reboot

A LaunchAgent starts when its user logs in and stops at logout. So the runner only comes back after a reboot if that user logs in on its own. GitLab's docs say to turn on automatic login for this reason. Do it in System Settings, under Users and Groups. Then stop the Mac from sleeping and test with a real reboot.

sudo pmset -a sleep 0
sudo shutdown -r now
# after it comes back, over SSH:
gitlab-runner status

Headless auto-login has a few silent failure modes, including one new in macOS 27. We wrote them up in auto-login on a headless Mac.

6. A .gitlab-ci.yml for an iOS app

A job runs on a runner only if the runner has every tag the job lists. This job asks for macos, runs the tests, and keeps the result bundle even when tests fail.

stages:
  - test

ios_tests:
  stage: test
  tags:
    - macos
  script:
    - xcodebuild -version
    - rm -rf build/TestResults.xcresult
    - xcodebuild test -project MyApp.xcodeproj -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5' -resultBundlePath build/TestResults.xcresult
  artifacts:
    when: always
    paths:
      - build/TestResults.xcresult
    expire_in: 1 week

The rm -rf line matters on a machine that keeps its working copy. xcodebuild refuses to write over an existing result bundle. We checked this on Xcode 26.6.

Common errors and fixes

These are from GitLab's macOS troubleshooting section.

  • "launchctl" failed: Could not find domain for. You ran install or start over SSH. Open Terminal on the Mac's desktop and run them there.
  • FATAL: Failed to start gitlab-runner: exit status 134. The service is not installed correctly. Run gitlab-runner uninstall, then install, then start, from the desktop.
  • killed: 9 on Apple silicon. The log folders named in the plist must exist and be writable by your user.
  • Failed to authorize rights (0x1) with status: -60007. Run DevToolsSecurity -enable and sudo security authorizationdb remove system.privilege.taskport is-developer.
  • git fetch hangs. A Homebrew Git can add a keychain credential helper. Run git config --global --add credential.helper '' as the runner user.
  • A job sits stuck. Its tags do not match the runner's tags, or the job has no tags and the runner does not take untagged jobs.

Why a dedicated Mac helps

The shell executor reuses the same machine for every job. Xcode's DerivedData, Swift package checkouts and CocoaPods caches stay on disk between pipelines. That is where the time goes. In our benchmark with the Wikipedia iOS app on Xcode 26.6, we timed a typical job after a small change. It took 27 seconds on a warm M6. The same job took 269 seconds on a fresh GitHub-hosted macos-26 runner. A clean build was 86 seconds against 183.

When GitLab's hosted Mac runners are enough

GitLab runs its own macOS runners. Its docs, read in October 2026, list them as beta. They are for Premium and Ultimate customers and open source programs. The sizes are an M1 with 4 vCPUs and 8 GB, and an M2 Pro with 6 vCPUs and 16 GB. If you are on one of those plans and run a few pipelines a day, hosted runners save you all of the steps above.

Do not pick a dedicated Mac with a shell executor for a public project that runs untrusted merge requests. GitLab warns that shell jobs can read other projects' code on the same machine. Also skip MacRun if you need a static IP for allowlisting, an SLA, or more than one region. We offer none of those.

Ready to try it on our hardware? Our other CI systems page covers the MacRun side, and pricing lists every tier.

Frequently asked questions

Should I install GitLab Runner on macOS with Homebrew?

+

GitLab recommends the official binary. Its docs say GitLab does not maintain the Homebrew formula.

Why does gitlab-runner install fail over SSH?

+

The runner is a user LaunchAgent and needs a graphical login session. Run install and start from a terminal on the Mac's desktop.

Can GitLab Runner run as a LaunchDaemon on macOS?

+

No. GitLab says the user-mode LaunchAgent is the only supported mode, because jobs need the user's keychain and session for signing and the Simulator.

Where do I set runner tags with the new token flow?

+

In GitLab, on the runner's create or edit page. GitLab's docs say tags can only be set when the runner is created in the UI or with the API, not by the register command.

Related guides