Symptom: SSH works again after the Mac restarts, but GitLab still shows the Runner as offline.
Fastest fix: Check the host, logged-in user session, LaunchAgent, Runner process, registration, network, and job tags in that order. Do not reinstall first.
This guide is for you if you maintain one remote Mac CI node and repeatedly restore its Runner by hand, operate shared GitLab Runner infrastructure, or release iOS and macOS software that depends on signing, Keychain access, or Simulator.
The key decision is simple: keep the user-level macOS LaunchAgent and repair the layer that failed. Do not replace it with a system-level LaunchDaemon merely because the machine is reachable through SSH. GitLab documents the LaunchAgent model for macOS and does not support a LaunchDaemon as the substitute service model. Read GitLab’s macOS installation and service-mode guidance.
Start with the failure layer, not the installation command
A reboot-related incident often looks like one problem in the GitLab interface. It is usually one of several separate states:
- The Mac itself is unreachable.
- The Mac is reachable, but no required user session is active.
- The LaunchAgent did not load.
- The GitLab Runner process loaded but cannot connect.
- The Runner is online, but the job cannot match its tags.
- The job starts but fails because signing, Keychain, or Simulator access is unavailable.
A common failure case is misleading: SSH returns normally, a shell command works, and the Mac appears healthy. GitLab still reports the Runner as offline. That evidence proves only that the operating system and SSH path are available. It does not prove that the GUI user domain was bootstrapped, the LaunchAgent was loaded, or the Runner authenticated successfully.
Use this outside-in evidence order:
| Layer | What to verify | Stop condition | Next action |
|---|---|---|---|
| Host | SSH or console access, hostname, system uptime | You cannot reach the Mac | Restore the host or access path |
| User session | Correct account is logged into the graphical session | No GUI session exists | Use VNC or the web console; review security constraints |
| LaunchAgent | Service is loaded in the correct user domain | No service entry or load error | Repair the GUI-domain installation |
| Runner process | Process and local logs | Process exits or is killed | Inspect plist, binary, permissions, and logs |
| Registration | Runner appears online and has valid credentials | Authentication or registration error | Repair registration only with preserved configuration |
| Network | Endpoint, proxy, certificate, and outbound access | Connection errors continue | Fix network or trust configuration |
| Scheduling | Tags and protected-branch rules | Jobs remain pending | Correct job tags or Runner assignment |
| Execution | Build, signing, Keychain, and Simulator behavior | Job starts but fails | Repair the user-session or toolchain dependency |
Stop at the first failed layer. If the host is healthy and the user session is absent, reinstalling the Runner adds risk without addressing the cause.
First step: prove whether the user session survived the reboot
On macOS, GitLab Runner’s supported service mode is a user-level LaunchAgent. It runs in the context of the logged-in user, not as an independent system daemon. That distinction explains why a Mac can accept SSH connections while its Runner remains unavailable.
Check these facts from an interactive graphical session:
whoami
id
launchctl print "gui/$(id -u)"
Use the account that owns the Runner configuration. Replace the placeholder account and paths in every command with your actual values. Do not paste a real registration token, hostname, certificate, or project name into a ticket or public issue.
If the command shows no usable GUI domain, inspect the Mac through VNC or the provider’s web console. A terminal opened inside that graphical login is materially different from a shell created only through SSH. The latter may authenticate you successfully while lacking the user bootstrap domain required by the LaunchAgent.
Why does a Mac restart leave GitLab Runner offline?
The usual reason is that the Mac restarted into a state where the Runner user is not logged into the graphical session. The LaunchAgent therefore has no valid user domain in which to run. This is especially important for jobs that use the login Keychain, code-signing identities, or iOS Simulator.
For a basic shell build that needs only files, command-line tools, and network access, the session requirement may appear less obvious. For Xcode signing or Simulator work, treating the session as optional produces a false recovery: the Runner may become online, but the job still fails later.
Automatic login can change this behavior, but it is a security decision, not a universal fix. Apple documents restrictions involving FileVault and organization-managed policies. Review Apple’s automatic-login support guidance. Do not disable FileVault or weaken a company policy simply to make a CI node appear online.
Operational warning: If the node handles signing identities, production credentials, or customer source code, document who can access the graphical session before enabling any automatic-login workflow. A successful reboot is not sufficient evidence that the resulting access model is acceptable.
Second step: repair a LaunchAgent installed from the wrong session
A frequent clue is an error such as launchctl failed: Could not find domain for. This points toward a service-management context problem, not automatically toward a corrupt Runner installation.
The important distinction is:
- LaunchAgent: user-scoped and tied to the logged-in graphical user.
- LaunchDaemon: system-scoped and not the supported replacement for the macOS GitLab Runner service model.
- Runner process: the executable that must remain alive and connect to GitLab.
- GitLab Runner status: the platform’s view of registration and connectivity.
If the Runner was installed or started from a pure SSH session, move to the Mac’s graphical session before repeating the service operation. Open Terminal through VNC or the web console, confirm the account, and follow the current GitLab macOS service instructions. Use GitLab’s official Runner commands reference for the supported command set.
Do not delete the plist or configuration file just because launchctl reports a missing domain. First capture:
launchctl print "gui/$(id -u)" > "$HOME/launchctl-gui-domain.txt"
ps aux | grep -i '[g]itlab-runner'
Then inspect the service definition and its ownership. The exact filename can differ by installation method, so locate the existing user-level service rather than inventing a replacement:
find "$HOME/Library/LaunchAgents" -maxdepth 1 -type f -print
ls -l "$HOME/Library/LaunchAgents"
If the service exists under another user, or if the current account does not own the plist, stop and correct the account decision before loading anything. A service loaded into the wrong user domain can look installed while remaining invisible to the account that owns the Runner configuration.
Third step: collect plist, binary, and log evidence
When a LaunchAgent is present but the service still fails, inspect the launch definition before reinstalling. You need to establish four facts:
- The
ProgramArgumentspath points to a real GitLab Runner binary. - The configuration path points to the intended Runner configuration.
- The log paths exist.
- The owning account can read the configuration and write the log directory.
Typical checks include:
test -x "/path/to/gitlab-runner" && echo "binary executable"
test -r "/path/to/config.toml" && echo "config readable"
ls -ld "/path/to/log-directory"
ls -l "/path/to/LaunchAgent.plist"
Use placeholders until you have confirmed the real paths. GitLab’s command and configuration documentation explains how command options and configuration locations relate; use that reference rather than guessing a path from another installation. Check GitLab’s Runner configuration command documentation.
Three launch errors require different evidence:
killed: 9: inspect whether the process was terminated by the operating system or a policy, then capture the surrounding system and Runner logs. Do not assume a bad registration.exit status 134: capture the full process output and launch context. This is a process termination signal, not proof that the service file should be deleted.Load failed: 5: verify the user domain, plist syntax, ownership, executable path, and writable output locations before trying another load operation.
Keep the original configuration and registration information in a protected backup location. Reinstall only when the service file or binary is demonstrably damaged. A reinstall that discards the configuration turns a startup fault into a registration and credential recovery project.
Separate an online Runner from a schedulable Runner
A Runner can appear online while jobs remain pending. This is not necessarily a startup failure.
GitLab schedules a job only when its requirements match an available Runner. Tags are one of the decisive filters. A job tagged macos, xcode, or another project-specific label will not run on a Runner that lacks the required matching tag. Review the job definition and the Runner assignment together. See GitLab’s official tag-matching rules for Runner selection.
Why can a Runner show online while a macOS CI job stays queued?
The process may have reconnected correctly, but the job may target different tags, a protected branch, or a Runner that is not eligible for that project. Check the pending job’s requirements before restarting the Mac again.
Use this comparison:
- If the GitLab page says offline, inspect the process, registration, network, and logs.
- If it says online but the job is pending, inspect tags, project scope, protected status, and concurrency conditions.
- If the job is running but fails during signing, inspect the user session, Keychain unlock state, certificates, provisioning assets, and Xcode environment.
- If the job reaches the Simulator and fails, treat graphical-session availability as a functional dependency.
The Runner’s advanced settings can also affect how it behaves under load and during reconnects. Review them only after establishing that the service is actually running. Use GitLab’s advanced Runner configuration reference.
Fourth step: test network and registration independently
Once the process is present, read its logs for connection attempts and timestamps. Compare the last local connection event with the status timestamp in GitLab. This helps distinguish three conditions that look similar in the web interface:
- The Runner never started.
- The Runner started but cannot reach GitLab because of a proxy, certificate, or outbound network issue.
- The Runner reached GitLab but no longer has valid registration state.
Check the host’s normal network path, DNS resolution, proxy variables, certificate trust, and any outbound firewall rule required by your GitLab deployment. Avoid changing several variables at once. For example, a new proxy setting can hide a registration problem by changing the destination path before you have captured baseline logs.
Registration repair should be deliberate. Preserve the existing configuration, record the Runner identity, and confirm which project or group owns it. Do not generate a new registration identity for every reboot. That creates abandoned Runners and makes incident history harder to interpret.
The Shell executor deserves additional scrutiny. It runs commands with the permissions of the Runner user. GitLab warns that this executor is appropriate only for trusted projects because jobs can access the host user’s files and environment. Read GitLab’s Shell executor security guidance.
For a shared Mac, check whether one project can see another project’s checkout, cached credentials, SSH keys, signing material, or temporary build artifacts. A Runner that survives reboot but exposes credentials across projects is not a successful recovery.
Fifth step: use a reboot acceptance test
A repair is incomplete until the node passes a real restart test. Do not validate only with a status page or a successful ssh connection.
Before restarting, record:
- The Runner’s visible status in GitLab.
- The active account and GUI session state.
- The LaunchAgent state.
- The Runner process ID and recent log timestamp.
- The tags assigned to the Runner.
- The result of one ordinary command-line build.
- The result of one signing or Simulator task, if production requires it.
Then perform the test:
- Allow the current CI job to finish.
- Confirm that no deployment or signing operation is still using the node.
- Restart the Mac through the approved administrative path.
- Reconnect through SSH to prove host availability.
- Open VNC or the web console and confirm the intended user session.
- Check the user LaunchAgent from that graphical session.
- Confirm the Runner process and log timestamps.
- Confirm that GitLab shows the Runner online.
- Submit a harmless job with the expected macOS tags.
- Run the required Xcode, signing, Keychain, or Simulator validation.
- Record whether recovery was automatic, required a user login, or required manual service action.
Use this acceptance checklist for every node:
- [ ] SSH reaches the expected Mac after reboot.
- [ ] The intended CI account is active in the graphical session.
- [ ] The user LaunchAgent loads in the correct GUI domain.
- [ ] The Runner process remains alive after startup.
- [ ] Logs show a fresh connection attempt.
- [ ] GitLab reports the expected Runner as online.
- [ ] A job with the production tag set is picked up.
- [ ] A shell-only build completes.
- [ ] A signing job can access the required Keychain items.
- [ ] A Simulator-dependent job completes, if applicable.
- [ ] No unrelated project can read another project’s credentials or checkout.
- [ ] The recovery result is recorded with the node identifier and test date.
This separates “the service came back” from “the delivery workflow is usable.” If only shell builds pass, keep signing and Simulator workloads out of production until the user-session path is fixed.
Decide whether to keep, redesign, or replace the node
After the test, choose one of three outcomes.
Keep the design when the graphical session is restored under the approved security policy, the LaunchAgent loads consistently, the Runner reconnects, tags match, and required signing or Simulator jobs pass.
Redesign the recovery path when ordinary builds work but signing tasks require a person to log in. You may retain the node for non-interactive work and provide a controlled graphical recovery procedure for release jobs. Document the limitation instead of labeling the node fully automatic.
Replace or isolate the node when the account model conflicts with FileVault or organization policy, the network path is unreliable, credentials cannot be isolated, or repeated restarts produce inconsistent service state. A second access path is valuable here: manage the host through both SSH and a graphical console so that a missing GUI session does not become a blind recovery operation.
If your current machine is difficult to reproduce, you can evaluate a remote Mac environment from MACCOME with both SSH and graphical-console access before moving production CI. The purpose is not to assume that every rented Mac solves the problem. It is to reproduce the exact reboot, session, Runner, signing, and Simulator conditions on an isolated node first.
A real remote Mac can also expose the same trade-offs as a local machine: user-session dependencies remain, network latency affects interactive troubleshooting, and access policy still matters. Validate those constraints before migration.
Why the current setup may be worse than a managed Mac test node
If your current CI host is a repurposed local Mac, a self-managed virtual machine, or a Linux server attempting to replace macOS tooling, the recurring weaknesses are usually concrete:
- Reboots depend on someone being physically present or on a fragile automation path.
- SSH availability creates false confidence because it does not prove the GUI LaunchAgent is active.
- Signing identities, Keychain state, and Simulator availability are hard to reproduce on a generic server.
- Shared Shell executor access can expose source code and credentials when isolation is weak.
For short-term recovery work, release preparation, or a controlled migration test, renting a Mac through MACCOME’s Mac mini options can give you a separate node with a graphical console and SSH path to test the full restart procedure. Keep long-running production workloads on a self-owned machine when you need fixed hardware custody, dedicated physical interfaces, or a stable workload over an extended period. The right choice is the one that passes the reboot acceptance checklist, not the one that merely shows “online” in GitLab.