Software Development
One Idle MacBook Is All Your Mobile CI/CD Needs
October 9, 2026
A build pipeline sat in pending for over an hour on a Tuesday afternoon before anyone actually walked over and looked at the runner. When I did, there was a macOS keychain dialog sitting on screen — an actual GUI prompt, waiting for someone to click “Always Allow” — because Fastlane’s match had gone looking for a keychain that, as far as the job was concerned, didn’t exist. Except there was no one around to see it. The Mac running our iOS builds lives tucked under another laptop, lid closed.
That’s roughly where this started. Not a strategy doc, not a cost-cutting initiative — a genuinely dumb failure mode that made it obvious our mobile release process had never really been designed, just assembled over time.
Every mobile team has some version of this story. Ours involved a keychain, but it doesn’t much matter what breaks first — the point is that most teams either pay a managed Mac-cloud vendor to make the whole category of problem someone else’s job, or keep doing releases by hand and hope nobody’s on vacation the week a release is due. We’d already tried the first option. Twice.
We already paid two vendors to solve this
Before any self-hosting happened, we were on a managed mobile CI vendor, on an annual subscription. For a mobile team of three to four people, that fixed yearly cost got harder to justify every renewal — we simply weren’t shipping often enough to make a flat annual license make sense. We moved to a second vendor mainly to fix that, specifically to get onto pay-as-you-go pricing instead of a lump sum up front. That helped the invoice. It didn’t fix the actual problem — we were still renting someone else’s understanding of our release process, just at a different billing model.
The real gap isn’t technical — it’s organizational
None of this was ever really about build speed. A release build on a developer’s own laptop was never slow. What actually breaks is consistency — getting three or four mobile engineers to run the same multi-step release checklist, in the same order, with the same signing setup, every single time, without someone skipping a step under deadline pressure. That’s a process problem, and it’s the one SaaS vendors are quietly getting paid to paper over rather than fix.
Part of why it never gets fixed properly: DevOps at most companies doesn’t speak mobile — keystores, provisioning profiles, why an iOS build needs a real Mac at all — none of that is in their mental model. And mobile engineers mostly only know how to ship from their own machine, which is a real skill, just one that lives entirely outside CI. Nobody owns the space between the two, so it stays a manual, person-dependent ritual until something breaks loudly enough — like a keychain prompt nobody’s around to click.

The unglamorous fix
The fix, when we actually tried it, was almost embarrassingly modest: an M1 MacBook that was sitting completely unused at the company — not anyone’s assigned dev machine, just idle hardware — registered as a self-hosted GitLab runner, tagged so release jobs route to it specifically. No new hardware budget, no cluster, no vendor contract to renew.
We had a working proof of concept in about a week, running through GitLab’s cloud CI but hitting our own self-hosted runner for anything that needs a Mac. The GitLab side is still cloud for now — moving the GitLab instance itself on-premise is the next step, once this has more mileage behind it.

Back to that keychain prompt
The actual fix wasn’t glamorous. That dialog is macOS trying to ask a human for permission on a machine that’s supposed to run completely unattended. The fix is to stop letting a job depend on whatever ambiguous keychain state the machine happens to be in: create a fresh keychain, unlock it non-interactively, set it not to auto-lock mid-build, add it to the search list explicitly — and delete it in after_script regardless of whether the job passed or failed. Once that’s scripted instead of assumed, there’s nothing left for the GUI to ask permission for.
The same discipline applies to everything else with a secret in it: Android keystore, key passwords, the Play Console service-account JSON, the iOS certificates and provisioning profiles (kept in their own separate encrypted repo via Fastlane match, decrypted with a password that’s just another masked CI variable). None of it lives on the runner longer than the job needs it.
It doesn’t have to be GitLab
The stack behind this is Fastlane and Expo, running on GitLab CI hitting a self-hosted M1 runner — but nothing about the shape of it is GitLab-specific. Jenkins with a macOS agent, GitHub Actions with a self-hosted macOS runner, Azure Pipelines with a self-hosted agent pool — same architecture, different YAML dialect. What has to carry over is the pattern, not the tool: manual gate before a release build, secrets injected at run time and wiped immediately after, certificates kept separate from general CI variables, and a deploy step that’s decoupled from build so publishing to a store track is its own reviewable, re-runnable action.
What actually changed
We stopped paying that second vendor’s per-minute meter. Release builds stopped depending on whichever engineer happened to have the right certificates installed that week — the pipeline is the source of truth now, not someone’s laptop. And, somewhat counterintuitively, the signing material is more locked down than it was under the old manual process, because the ephemeral, wipe-after-use pattern is stricter than anything an individual developer was enforcing by hand.
The honest catch
One Mac doesn’t parallelize. A team of three or four release builds a week is fine on a single runner; five teams trying to ship at once would need more than one machine. And keeping secrets off the runner is now our discipline to maintain, not a vendor’s SLA to point at if something goes wrong.
For a team our size, that trade is a good one.
This is running on one project right now. The plan is to roll the same setup across our other mobile projects internally, and — since none of this is actually specific to us — package it as something we can offer to clients who are stuck doing manual local builds, or paying a similar-sized subscription bill to a mobile CI vendor, for a problem that fits on hardware already sitting around unused.
Author: Özgün Bal