Your macOS jobs are waiting in the queue, signing credentials are visible to too many projects, or the runner disappears after a reboot.
The fastest fix is to deploy a GitLab Runner macOS build machine as a dedicated trusted node, validate one complete pipeline, and expand only when queue time, concurrency, and recovery data justify another machine.
Who should read this guide?
This guide is for platform engineers moving iOS projects into GitLab CI/CD and adding macOS runners.
It also serves enterprise IT, security owners, CTOs, and technical directors who must control signing credentials, network access, recovery procedures, and Mac infrastructure costs.
Before installation: define the node boundary
Do not install GitLab Runner on the first available Mac and then allow every repository to use it. A macOS runner using the Shell executor runs job commands directly on the host. The job can interact with installed tools, local files, keychains, simulators, and network resources.
That makes the machine a security boundary, not just a build endpoint.
GitLab describes the Shell executor as having limited isolation and recommends using it only for trusted builds. Review the official Shell executor security guidance before placing any signing material on the host.
Define these boundaries before registration:
- Which repositories are trusted?
- Which branches and tags may start signing jobs?
- Which jobs need Xcode, Simulator access, or a local keychain?
- Which jobs can run without release credentials?
- Which networks must the runner reach?
- Who owns recovery after a reboot, certificate failure, or Xcode update?
Three hidden costs usually appear when this planning step is skipped.
Credential exposure: A job with shell access may read files, environment variables, local configuration, or keychain material available to the runner account.
Cross-project residue: A persistent workspace or cache may retain source files, dependencies, logs, generated archives, or temporary credentials from an earlier job.
Toolchain coupling: A change made for one project can break another. Xcode updates, Ruby versions, CocoaPods, package managers, simulator runtimes, and signing profiles can all create conflicts.
Use the narrowest runner scope that fits the pilot:
- A project runner is the safest starting point for one repository.
- A group runner is suitable when several repositories share the same trust level and toolchain.
- An instance runner exposes the host to a much wider set of projects and should require explicit approval.
GitLab documents these runner scopes and registration patterns in its runner setup documentation.
Decision condition: If the node stores Apple signing material, choose a project runner or a tightly controlled group runner. Otherwise, move unsigned builds to a separate node.
First step: choose the pilot shape
The first deployment should answer a narrow question: can one real pipeline check out code, build, test, sign, archive, upload artifacts, and recover after a restart?
Start with one dedicated node when:
- The pilot uses one main Xcode version.
- The first repositories are trusted.
- Release jobs are not continuous.
- One owner can manage certificates and recovery.
- You do not yet know the real build duration or peak concurrency.
Plan for multiple nodes when:
- Several Xcode versions must remain available.
- Release signing competes with pull-request validation.
- A single failed update would block a business-critical release.
- Different projects require different dependency or security baselines.
- Queue time is already unacceptable during normal development.
Do not estimate node count from developer headcount. A small team can generate heavy CI traffic during a release, while a larger team may create little demand if builds are infrequent.
Record these values from the first pipeline:
queue_wait: time from job creation to runner pickup.build_duration: execution time after pickup.success_rate: successful jobs divided by total jobs.peak_parallel_jobs: highest simultaneous job demand.disk_growth: workspace, cache, dependency, and artifact growth.recovery_time: time from failure or reboot to the next successful job.
Use this as a planning model:
Required runner slots
= peak parallel jobs
+ maintenance reserve
+ recovery reserve
This is not a performance guarantee. Your Xcode version, test suite, dependency installation, simulator usage, and artifact upload pattern determine the actual result.
Second step: prepare the macOS host and runner account
Create a dedicated local macOS account for GitLab Runner. Do not use a developer’s personal account.
The preparation order should be:
- Create the dedicated runner account.
- Decide whether it needs administrator rights.
- Configure the approved remote administration path.
- Enable FileVault and document the recovery process.
- Restrict inbound network access.
- Install GitLab Runner for the host architecture.
- Install Xcode and the required command-line components.
- Record the baseline before registration.
GitLab’s macOS implementation runs as a user-mode LaunchAgent. The runner depends on the macOS user session and does not behave like a normal system daemon. Review the official macOS installation requirements before designing unattended recovery.
The dedicated account reduces accidental access to personal files. It does not provide complete isolation. The Shell executor still runs with that account’s permissions. Remove unnecessary SSH keys, cloud credentials, browser sessions, personal configuration files, and unrelated source code.
FileVault protects data at rest, but it also affects restart recovery. Apple provides specific guidance for managing FileVault recovery keys and encrypted Mac deployments in its FileVault security documentation.
Recovery warning: A runner shown as online in GitLab has not passed the recovery test. You must verify disk unlock, user session restoration, LaunchAgent startup, GitLab connectivity, and a successful build.
Remote administration must match your security policy. SSH can be useful for diagnostics, but starting or repairing a macOS user-mode service only through SSH may fail when the expected graphical user session is unavailable. Test the exact access path your operators will use during an incident.
Third step: establish the Apple Silicon and Xcode baseline
Treat Xcode as part of the runner image. It is not an optional developer application.
Record the following before registration:
- macOS version.
- Host architecture.
- Xcode version.
- Active developer directory.
- Swift and Ruby versions where relevant.
- CocoaPods, Swift Package Manager, and other dependency tooling.
- Installed Simulator runtimes.
- Signing identities and profile policy.
- Workspace, cache, and artifact locations.
- Available storage at the start of the pilot.
Apple’s Xcode command-line tools documentation explains the distinction between the standalone Command Line Tools package and the full Xcode installation. A normal iOS archive workflow needs the full Xcode toolchain, including xcodebuild.
Use a baseline check such as:
sw_vers
uname -m
xcodebuild -version
xcode-select -p
xcrun --find xcodebuild
gitlab-runner --version
These commands make the environment visible. They do not prove that your project will build.
Select the intended Xcode path explicitly when the host contains more than one version:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
GitLab includes this type of preparation in its macOS runner setup guide.
Should you use Apple Silicon for the GitLab iOS build machine?
Use Apple Silicon when your projects and dependencies support the required architecture and your validation scope matches the target devices and release process. Keep an Intel-compatible node only when a legacy dependency, binary, or compatibility requirement still needs Intel behavior.
Do not treat an architecture change as a free upgrade. Test native dependencies, Ruby gems, package managers, simulator runtimes, binary plugins, and any Rosetta-dependent tools during the pilot.
Fourth step: register the runner and route jobs
Create the runner in GitLab first. Then register it on the Mac with the authentication token.
Registration alone is not a security design. You also need tags, protected branches, protected tags, project permissions, and variable protection.
Use tags that describe the environment:
macos
ios
apple-silicon
xcode-release
trusted-signing
Do not give every job every tag. Tags determine which runners can accept a job. The runner registration documentation explains how to register and assign runners to the required scope.
Set the runner to reject untagged jobs. Protect the runner when it can access signing credentials. Route release jobs only from protected branches or protected tags. GitLab’s runner configuration guidance covers protected runners, tags, and job routing controls.
Which executor should a GitLab iOS build machine use?
Use the Shell executor when the job needs direct access to Xcode, the iOS Simulator, local keychains, or native macOS tools. That direct access is its main benefit. Limited isolation is its main weakness.
For a trusted enterprise repository, Shell is usually the practical choice for native iOS builds. For untrusted code, do not rely on Shell executor tags as if they were a container boundary. Use a separate environment or redesign the job so sensitive host resources are not reachable.
Fifth step: validate the first GitLab CI/CD pipeline
Keep the first pipeline small. Do not begin with the full release workflow.
Validate these stages in sequence:
- Checkout: confirm repository access.
- Toolchain: print macOS, architecture, Xcode, and dependency versions.
- Dependency installation: verify lockfiles and package access.
- Build: run
xcodebuildagainst the intended workspace and scheme. - Test: run unit tests and a controlled Simulator test if required.
- Signing: use the correct identity and provisioning profile.
- Archive: create the expected archive or package.
- Artifact upload: verify that GitLab stores and retrieves the result.
- Cleanup: remove temporary keychains, profiles, credentials, and workspaces.
A minimal pipeline structure can look like this:
stages:
- verify
- build
- test
- archive
variables:
LANG: "en_US.UTF-8"
verify:
stage: verify
tags:
- macos
script:
- sw_vers
- uname -m
- xcodebuild -version
- xcode-select -p
build:
stage: build
tags:
- macos
script:
- xcodebuild -workspace App.xcworkspace -scheme App build
test:
stage: test
tags:
- macos
script:
- xcodebuild test -workspace App.xcworkspace -scheme App -destination 'platform=iOS Simulator,name=YOUR_SIMULATOR'
archive:
stage: archive
tags:
- xcode-release
- trusted-signing
script:
- xcodebuild archive -workspace App.xcworkspace -scheme App -archivePath build/App.xcarchive
artifacts:
paths:
- build/App.xcarchive
Replace the workspace, scheme, simulator destination, and signing settings with values from your project. A simulator name copied from another Mac may not exist on the new host.
How should GitLab Runner manage iOS signing certificates?
Keep certificates, private keys, and provisioning profiles out of the repository. Inject them only into jobs that require signing. Use a temporary keychain, import the minimum required identity, apply controlled access settings, and delete the temporary keychain after the job.
Do not print certificate passwords, private keys, provisioning profile contents, or protected variables in logs. Log only non-sensitive identity and profile metadata needed for troubleshooting.
Apple’s provisioning profile documentation explains how profiles are created, downloaded, and managed. Your pipeline should also verify expiration, bundle identifier, team identifier, and certificate matching before attempting an archive.
Separate the signing node from the ordinary test node when possible. If you cannot yet provide separate machines, use protected tags and a tightly limited runner scope. Document this as a temporary control, not as full isolation.
A decision tool for the first production layout
Use these conditions before expanding:
- If one Xcode version serves the pilot and release jobs are infrequent, choose one dedicated trusted node.
- If pull-request tests and release signing compete for the same machine, choose separate test and signing tags before adding more projects.
- If two Xcode versions must coexist, choose separate nodes by toolchain baseline.
- If repositories have different trust levels, choose separate project or group runners.
- If queue time repeatedly exceeds the release tolerance, choose another node or scheduled remote Mac capacity.
- If you lack reliable on-site recovery, choose a remote Mac pilot before purchasing a large fleet.
- If a project needs physical devices, private appliances, or specialized peripherals, fall back to owned hardware or a hybrid design.
The rule is simple: add capacity after measuring a constraint, not because the team has reached an arbitrary size.
Sixth step: harden the runner during the first week
After the first successful build, assume the deployment is not ready for unrestricted production use.
Protect runner identity and tokens
The runner authentication token is stored locally in the runner configuration. Anyone who obtains the token may be able to clone or impersonate the runner. Restrict access to the configuration file and rotate or revoke the token when ownership changes.
Review GitLab’s token security guidance during the handover process.
Segment the network
Allow only the outbound destinations needed for GitLab, dependency retrieval, Apple services, artifact storage, monitoring, and approved administration.
Do not expose SSH directly to the public Internet unless the access design requires it and includes strong authentication, logging, and network controls.
Control workspaces and caches
Persistent Shell runners may preserve data between jobs. Remove temporary keychains and profiles. Clean workspaces after sensitive jobs. Review whether a cache can cross project boundaries.
A cache that shortens builds but exposes another project’s dependencies is not an acceptable optimization.
Protect pipeline variables
Mark signing variables as protected. Do not allow merge-request pipelines from untrusted branches to reach a release runner.
Remember that .gitlab-ci.yml is executable code on a Shell executor. Anyone who can modify the pipeline may influence what the host runs.
Test unattended recovery
Test the full sequence:
- Normal restart.
- FileVault unlock.
- Approved user session restoration.
- LaunchAgent startup.
- Runner visibility in GitLab.
- Git checkout.
- Xcode availability.
- Keychain access.
- Signed test build.
- Remote rescue access.
How can GitLab Runner keep running on macOS?
It runs within the authenticated user session as a user-mode LaunchAgent. A Mac that stops at the login screen after a reboot has not passed unattended recovery. Document who unlocks the disk, how the user session starts, and how the runner returns online.
Seventh step: expand from capacity evidence
Review at least one representative release cycle and normal development traffic before making a procurement decision.
| Signal | What to record | Expansion response |
|---|---|---|
| Queue wait | Median and peak time before pickup | Add a node or use scheduled remote capacity |
| Build duration | By project, job type, and Xcode version | Separate slow release jobs from fast validation |
| Failure rate | Toolchain, signing, network, and infrastructure causes | Fix environment drift before buying capacity |
| Disk growth | Workspace, cache, dependencies, and artifacts | Improve cleanup or revise storage planning |
| Version conflict | Xcode and macOS combinations | Split the node pool by toolchain |
| Recovery time | Incident or reboot to first successful job | Improve remote access or add a standby node |
How many Mac build nodes does an enterprise need?
Start with the simultaneous jobs that must complete within the release window. Add maintenance and recovery reserve. Do not infer the answer from headcount.
Your node count should reflect:
- Peak parallel jobs.
- Queue tolerance.
- Average and peak build duration.
- Xcode version overlap.
- Signing workload.
- Acceptable single-node outage time.
- Recovery capability.
- Storage and artifact retention.
- Security separation between projects.
Compare three operating models:
Owned Mac nodes fit steady utilization, strict physical-control requirements, and teams able to manage replacement, patching, storage, power, and recovery.
Remote Mac rental fits seasonal demand, short validation cycles, limited on-site support, or a need to test the real pipeline before committing to hardware.
Hybrid capacity keeps a stable base for daily validation and adds remote capacity during releases, migrations, or temporary Xcode overlap.
Use a variable-based TCO model:
Annual owned TCO =
hardware purchase
+ networking and accessories
+ support and replacement reserve
+ administration time
+ facility and power cost
+ security and recovery cost
Annual remote Mac TCO =
rental commitment
+ storage or transfer charges
+ administration time
+ integration and monitoring cost
Do not insert a price or saving percentage until you have a written quote or a verified internal cost. Compare the same term, region, access method, support scope, storage policy, and recovery responsibility.
For a pilot, you can review KVMNODE’s remote Mac options and compare the available term against your internal TCO worksheet. The goal is not to assume that rental always costs less. The goal is to reduce premature hardware commitment while you measure the workload.
Production go/no-go review
Approve production access only when the evidence supports each answer:
- Does a trusted project complete checkout, build, test, archive, and artifact upload?
- Are release tags and protected branches the only routes to signing jobs?
- Can ordinary test jobs run without distribution credentials?
- Does the runner return after the documented reboot and unlock procedure?
- Can an administrator reach the host through the approved rescue path?
- Are runner tokens, SSH keys, CI/CD variables, and local workspaces protected?
- Is the Xcode baseline recorded and change-controlled?
- Is capacity sufficient for measured peak demand and maintenance?
- Is the failure of one node acceptable?
- Is rollback documented for Xcode, macOS, certificates, and profiles?
If any answer is “not yet,” keep the node in pilot status.
A green runner indicator only proves that GitLab can see the runner. It does not prove that the signing chain, recovery path, security boundary, or capacity plan works.
When remote Mac capacity is the better next step
After the single-node pipeline works, collect peak concurrency, Xcode versions, release frequency, signing requirements, and recovery records. Then run the same workflow on a remote Mac before ordering a larger fleet.
Buying hardware immediately can create procurement delay, depreciation, replacement responsibility, and a recovery process that depends on someone reaching the office or data room. Remote capacity can shorten the pilot, absorb release spikes, and expose toolchain conflicts before you make a long-term purchase.
The correct choice is not automatically “buy” or “rent.” It should match your measured queue, signing risk, recovery capability, and utilization pattern.
If you need temporary capacity or a controlled validation environment, a remote Mac from KVMNODE may be a better next step than purchasing several machines before the workload is known. Review the Mac mini remote environment options alongside your TCO worksheet, then decide the long-term node count from real pipeline evidence.