GitLab’s current macOS installation flow supports separate runner binaries for Intel and Apple Silicon Macs. That architecture choice is important, but it is not the deployment finish line. A runner that appears online is not automatically ready for production CI. A reliable macOS GitLab Runner deployment needs a dedicated standard user, the Shell executor, a persistent macOS user session, a controlled Xcode toolchain, isolated signing credentials, reboot recovery, and a real project pipeline that produces verifiable evidence. GitLab’s macOS installation guide documents the official installation and service model.
This week’s recommended action: deploy the runner without signing first, pass a real build and test pipeline, then add certificates only after reboot recovery and workspace cleanup are verified.
You should read this if you are moving iOS packaging from manual work to GitLab CI, maintaining cross-platform CI infrastructure, or evaluating whether a remote Mac can operate as a long-term build node. If your team has no Mac that can remain available for toolchain configuration and maintenance, this guide also helps you define the host, access, and delivery requirements before choosing a hosted option.
01Define what the Mac Runner is allowed to do
A remote Mac belongs in your GitLab CI architecture when the job genuinely depends on macOS-only components. Typical examples include Xcode, Apple SDKs, iOS simulators, macOS frameworks, or Apple code signing. A normal Linux build, static analysis job, or backend test should remain on a Linux runner unless the project has a clear macOS dependency.
This boundary prevents an expensive mistake: placing every pipeline stage on one privileged Mac. The Mac should handle jobs that need it. It should not become a general-purpose execution host for arbitrary repositories.
Decide the runner scope before registration:
- Project runner: suitable when one repository owns the Mac workload and its CI scripts are controlled.
- Group runner: useful when several trusted projects share the same toolchain and security policy.
- Shared or broadly available runner: a poor fit for a Mac that contains signing identities, simulator state, private dependencies, or production credentials.
- Protected runner: the correct direction for release jobs that can access signing material.
A Mac-based Shell executor provides direct access to Xcode and the host filesystem, but it offers limited isolation. GitLab’s Shell executor security guidance explains that jobs run with the permissions of the runner user. A malicious or careless script may access files, processes, credentials, and other project data available to that account.
Before installing anything, write down the evidence required for approval:
- A real repository can be checked out.
- Xcode can build and test without manual clicks.
- The runner returns after a user-session restart or host reboot.
- Signing assets never enter the repository or job artifacts.
- A failed job leaves the workspace in a known state.
- A release job cannot be selected by untrusted branches or fork pipelines.
If you cannot define these checks, registration is premature.
02Warning: Do not use a personal developer account as the CI identity. It often carries unrelated SSH keys, cached Git credentials, default keychains, and access to projects that the runner does not need.
First hour: create the execution context
First step: use a dedicated standard macOS user
Create a standard user for CI work. Avoid administrator privileges unless a specific installation task requires temporary elevation. The runner user should own its home directory, GitLab Runner configuration, build workspaces, caches, and CI-specific keychains.
This account should not be used for daily browsing, personal development, or unrelated administration. Shell executor isolation is limited, so this is not a complete security boundary. It is a way to make the execution context predictable and reduce accidental exposure.
Connect through SSH or the remote console, then confirm the identity used for installation:
whoami
echo "$HOME"
echo "$SHELL"
GitLab’s macOS runner setup guide documents the expected shell configuration and related setup details. If your scripts use Bash-specific syntax, verify that Bash is available to the runner user instead of relying on an interactive terminal profile.
Second step: install the architecture-matched runner
Use GitLab’s official binary for the host architecture. On Apple Silicon, use the ARM target. On Intel, use the AMD64 target. Keep the command aligned with GitLab’s current documentation rather than copying an old release path from a community tutorial.
A generic installation pattern is:
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
Replace the architecture suffix when required. Treat the downloaded binary as part of your software supply chain. Restrict who can replace it, record the installed version, and review it during maintenance.
Third step: initialize the Xcode toolchain
Install the Xcode version required by the project. Do not choose a version only because it is the newest available. Check deployment targets, Swift package compatibility, simulator needs, SDK behavior, and signing requirements against the project’s support policy and current Apple documentation.
Complete the initial setup under the CI user:
sudo xcodebuild -runFirstLaunch
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcodebuild -version
xcode-select -p
The active developer directory affects xcodebuild, SDK lookup, simulator tools, and signing commands. Apple’s continuous integration build documentation explains the role of xcodebuild, dependency pinning, and external CI workflows.
Commit Package.resolved when the project uses Swift packages. For private dependencies, configure SSH keys and known_hosts for the runner user instead of assuming that your personal SSH configuration exists in the service session.
Fourth step: register with a narrow tag
Create the runner in the intended project or group, then register it from the dedicated macOS user:
gitlab-runner register
Use the GitLab URL, the current runner authentication token, the runner description, the macos tag, and the shell executor when prompted. GitLab’s runner registration documentation describes the current registration workflow and token handling.
Do not publish the token in a repository, ticket, screenshot, or job log. Keep the tag specific enough that ordinary Linux jobs cannot select the Mac accidentally.
A minimal .gitlab-ci.yml should begin with an unsigned build and test:
stages:
- build
- test
variables:
LANG: "en_US.UTF-8"
build_ios:
stage: build
tags:
- macos
script:
- xcodebuild -version
- xcodebuild -workspace "App.xcworkspace" \
-scheme "App" \
-sdk iphonesimulator \
-configuration Debug \
build \
CODE_SIGNING_ALLOWED=NO
test_ios:
stage: test
tags:
- macos
script:
- xcodebuild -workspace "App.xcworkspace" \
-scheme "App" \
-sdk iphonesimulator \
-destination "platform=iOS Simulator,name=YOUR_SIMULATOR" \
test \
CODE_SIGNING_ALLOWED=NO
Use placeholders for the workspace, scheme, and simulator. The destination must match an installed simulator runtime. Do not add archive or export steps until these jobs complete consistently.
03Make the macOS service survive normal recovery
This is where many deployments fail.
GitLab’s documented macOS service runs in user mode. The service configuration is expected under ~/Library/LaunchAgents/gitlab-runner.plist, and the runner service depends on the user session. The same model matters when simulator access is required because GUI-related services and user keychains are tied to the logged-in account. The official macOS service documentation should take precedence over community scripts.
Install the service as the CI user:
su - ci-runner
cd ~
gitlab-runner install
gitlab-runner start
Then verify the service file and status:
ls -l ~/Library/LaunchAgents/gitlab-runner.plist
gitlab-runner status
Do not convert the service into a custom LaunchDaemon and call that an official production configuration. A community workaround may appear to solve an offline state while breaking simulator access, keychain behavior, or future upgrades.
Test the complete sequence before adding secrets:
- Submit a job while the user is logged in.
- Disconnect SSH without logging out.
- Interrupt the network briefly.
- Reconnect and inspect runner status.
- Reboot the Mac.
- Confirm the CI user session and service state.
- Run the same build again.
The objective is not to make the Mac “always on” at any security cost. Automatic login may simplify unattended recovery, but it also changes the physical and account security boundary. Decide based on the host location, console access, disk encryption policy, and whether a human recovery path exists.
If automatic login is unacceptable, document who restores the user session and how release jobs are blocked while the runner is unavailable. A manual recovery procedure is better than silently weakening host security.
04Operational rule: If the runner disappears after reboot, inspect the logged-in user, LaunchAgent, service owner, network reachability, and Xcode access before changing permissions or creating a system daemon.
Build GitLab CI in layers
A reliable GitLab CI pipeline should grow in stages. Each layer should have a pass condition before the next one is added.
Layer one: verify checkout and environment
Confirm that the repository is available to the CI user. Print only non-sensitive environment details:
pwd
xcode-select -p
xcodebuild -version
git --version
Do not print all environment variables. CI variables can contain tokens and credentials.
Layer two: resolve dependencies
For Swift packages, keep Package.resolved under version control. For CocoaPods, Ruby, or other dependency managers, pin versions in the repository and make the installation command reproducible.
If private packages use SSH, configure the CI user’s key and known_hosts. Avoid disabling host-key verification as a shortcut. Apple’s CI guidance treats SSH configuration as part of private dependency access, not as an optional convenience.
Layer three: run an unsigned build and test
Use CODE_SIGNING_ALLOWED=NO where appropriate for simulator or compile validation. This separates toolchain failures from signing failures.
Save the complete build log and preserve test results as GitLab artifacts. A green job without retained evidence is difficult to investigate later.
Layer four: add an archive
Only after the unsigned path works should you introduce an archive step:
xcodebuild archive \
-workspace "App.xcworkspace" \
-scheme "App" \
-archivePath "build/App.xcarchive"
The actual command depends on the workspace, scheme, export method, and project signing configuration. Keep these decisions in version-controlled scripts rather than hiding them in a manually configured Mac.
Layer five: export and publish
Publishing requires a separate trust decision. App Store Connect credentials, API keys, certificates, and provisioning profiles should be available only to protected release jobs.
The release job should also have a cleanup phase that removes temporary files, exported packages, and imported credentials. Do not treat a successful archive as proof that publishing is safe.
05Add certificates and keychains only after validation
Signing problems usually come from execution context, not from the certificate file itself. A job may run under a different user, a locked keychain, a different keychain search list, or a service session without the expected security context.
Apple’s keychain documentation and code-signing certificate guidance explain how certificates and private keys are managed and accessed.
A safer release design has these properties:
- A dedicated signing keychain is used for CI.
- The keychain password is stored in protected GitLab variables.
- Certificates and private keys are imported only during the release job when possible.
- Access is granted to the required signing tools, not every process.
- The keychain is unlocked only for the required job window.
- Temporary profiles and exported credentials are deleted afterward.
- Release jobs run only on protected branches and protected runners.
- Fork pipelines cannot access the runner or protected variables.
Do not place .p12 files, provisioning profiles, API keys, or exported archives in the repository. Apple’s provisioning profile documentation explains the profile lifecycle and supports treating these files as managed deployment assets rather than permanent source files.
Test the exact signing command under the same user and service context as GitLab Runner. A command that succeeds in an interactive administrator shell may fail in the LaunchAgent session because the keychain is locked or the search list differs.
06Validate real jobs and failure recovery
A production node is judged by what happens when a job fails.
GitLab documents process handling for Shell executor jobs, including cancellation behavior and cleanup expectations. This matters for simulators, background processes, temporary keychains, and large build directories. Do not assume that canceling a pipeline automatically removes every process or temporary file.
Add cleanup logic for:
- Derived data.
- Temporary archives.
- Export directories.
- Imported signing identities.
- Temporary keychains.
- Simulator processes.
- Package-manager caches that exceed your storage policy.
Do not delete every cache after every job. That can make builds slower and hide capacity problems. Instead, define what may persist, what must be removed, and who owns the cleanup schedule.
During the first week, observe real project jobs rather than an empty demonstration repository. Check:
- Queue time and runner assignment.
- Build and test success rate.
- Workspace and DerivedData growth.
- Cache behavior.
- Concurrent-job conflicts.
- Reboot recovery.
- Network interruption recovery.
- Signing failures.
- Artifact upload and retention.
- Cleanup after canceled jobs.
Use this decision guide:
Continue with one runner when real jobs are stable, the queue remains acceptable, reboot recovery is documented, and signing access is limited.
Clean and optimize when builds pass but disk growth, cache duplication, or orphaned simulator processes increase.
Add another Mac runner when jobs queue behind one another, projects require incompatible Xcode environments, or release work must be isolated from validation.
Return to manual release work temporarily when signing is unreliable, the runner returns online but cannot access Xcode or the keychain, or untrusted code can reach protected credentials.
The difference between an online runner and a usable CI node is the evidence collected here.
07FAQ
The most common long-tail problems are connected to session state, not registration. A runner can appear registered in GitLab while its macOS service is stopped, its user is logged out, or its keychain is unavailable.
Keep these checks in your runbook:
- Who owns the runner process?
- Is the expected LaunchAgent loaded?
- Is the CI user logged in?
- Does
xcode-select -ppoint to the intended developer directory? - Can an unsigned build run without interaction?
- Can the release job unlock its controlled keychain?
- Does the host recover after reboot?
- Can a canceled job clean its temporary files?
Choose the host before you choose the lease
Your current setup may be a personal Mac, an overloaded office Mac, or a Linux server that cannot provide Xcode and Apple signing tools. Those options have real weaknesses: personal Macs carry unrelated credentials, shared office hardware is difficult to keep online, and Linux cannot replace the native macOS toolchain for iOS builds.
Buying a dedicated Mac mini can solve control, but it also leaves you responsible for hardware procurement, maintenance, storage, remote recovery, and idle capacity between release cycles. It may be the right choice for a permanent heavy workload or a team that needs physical device access. It is less attractive when the build demand is periodic or the hardware would sit unused between release windows.
If you need a real Mac that can accept a dedicated CI user, keep the Xcode toolchain available, and remain online without tying up a developer workstation, a hosted option is easier to evaluate against the acceptance criteria above. Review the VpsMesh Mac rental pricing, then compare the available remote Mac ordering options with your build frequency, signing policy, and recovery requirements.
VpsMesh is a better fit when you need temporary or periodic macOS CI capacity without purchasing hardware that may sit idle. It is not automatically the best choice for permanent heavy workloads, strict physical-device access, or teams that must own every layer of the machine. The right decision is the host that passes your real pipeline, security, and reboot tests—not the one that merely shows “online” in GitLab.