Photo by Daniil Komov on Unsplash
I accidentally built a self-hosted iOS CI pipeline
A few days ago, I discovered Lima VM.
At first, it was just curiosity. I wanted a cleaner way to isolate my development environments without constantly fighting my local machine.
I didn’t expect it to turn into this:
I now have a self-hosted CI pipeline that builds iOS apps using macOS VMs running inside my Mac.
It Started With a Simple Problem
Like most developers, I was juggling environments:
- development vs staging
- different
.envsetups - Docker containers stepping on each other
Everything worked… but it never felt clean.
So I tried something different.
One Project, Two Machines
Instead of one machine handling everything, I split a single project into two Lima VMs:
- Dev VM → hot reload, fast iteration
- Staging VM → pull changes, build production images, test
Each VM had its own hostname:
lima-project-dev.local
lima-project.local
No conflicts. No weird container overlaps. No “wait, which environment am I in?”
Photo by Heliberto Arias on Unsplash
The setup wasn’t smooth.
I had to:
- figure out Lima YAML configs
- mount projects correctly
- reconfigure git inside the VM
It took a few retries. A lot of trial and error.
But once it worked?
It felt different.
Then I Got Curious (Again)
I’ve been running Gitea in my homelab for months.
I knew it supported Actions, similar to GitHub Actions—but I never fully committed to it because:
“Where do I even run the runners?”
That question basically killed my motivation before.
Until Lima.
Running My Own CI Runners
Instead of overthinking it, I spun up another VM.
Installed the Gitea Act Runner.
Connected it to my instance.
And… it worked.
I was now running CI jobs for my private repositories—fully self-hosted.
Photo by Jake Walker on Unsplash
That alone already felt like a win.
But I wasn’t satisfied.
The Real Goal: iOS Builds
As an iOS developer, there’s always one limitation:
You need macOS to build iOS apps.
Cloud CI solves this. GitHub Actions solves this.
But I wanted to see:
Can I do this myself?
macOS Inside macOS (Yes, Really)
Lima recently introduced experimental macOS VM support.
So I tried it.
I set up a macOS VM… inside my Mac.
Then I tried to turn it into a CI runner.
This Part Was Painful
Setting it up was not easy.
I had to manually install and configure:
- Xcode
- Node.js
- Cocoapods
- React Native Expo dependencies
Things broke.
A lot.
At some point, I stopped “trying random fixes” and started documenting everything.
Eventually, I built my own repeatable setup guide.
Then I wiped the VM.
Recreated it from scratch.
Followed my own steps.
And finally…
It Worked
I triggered a Gitea Action.
The runner picked it up.
The macOS VM executed it.
And I got:
An iOS build artifact generated using
xcodebuild.
From my own self-hosted pipeline.
No GitHub Actions. No external CI.
Just:
- Gitea
- Lima VM
- My own machines
That moment honestly felt incredible.

Photo by Baltasar Henderson on Unsplash
What I Have Now
Right now, my setup looks something like this:
- Multiple Lima VMs for dev/staging
- Ubuntu runners for general CI
- macOS VM runner for iOS builds
- Gitea Actions orchestrating everything
I can now:
- Build iOS apps automatically
- Generate unsigned IPAs
- Deploy them to my own distribution system (work in progress)
Small Limitations (For Now)
It’s not perfect.
For example:
- Gitea Actions currently supports
upload-artifactup to v3 - Newer versions (v6) aren’t fully supported yet
But honestly?
That’s a small tradeoff.
If needed, I can just push artifacts to another server.
What I Learned: Provisioning Lima & Creating Templates
One of the most valuable things I gained from this experience wasn’t just getting everything to work—it was learning how to provision Lima VMs properly and create reusable templates.
Setting up a macOS VM from scratch was not easy. It involved a lot of trial and error, debugging, and repeated deployments. But that process forced me to understand how Lima’s configuration works at a deeper level, especially its YAML-based provisioning system.
And that changed everything.
Instead of treating each VM as a one-off setup, I can now think in terms of templates.
From here on out, I can create custom Lima templates for my own workflows, such as:
- Spinning up a fresh VM for new project development
- Creating pre-configured Ubuntu runners for Gitea Actions
- Preparing environments with Docker and commonly used services already set up
- Bootstrapping CI/CD tools without installing anything directly on my host machine
This turns Lima into more than just a VM tool—it becomes a foundation for repeatable, disposable infrastructure.
The initial setup may be painful, but once you’ve built your own provisioning templates, every new environment becomes significantly faster and more consistent.
And for me, that’s probably one of the biggest wins from this entire experiment.
Why I’m Doing This
Partly?
Because it’s fun.
But also:
- I want to understand how CI/CD actually works under the hood
- I want control over my infrastructure
- I want to build systems—not just use them
This isn’t about replacing GitHub Actions.
It’s about learning by building the system yourself.
Is Lima VM “Life Changing”?
Too early to say.
But right now?
It already changed how I think about:
- development environments
- isolation
- automation
- self-hosted infrastructure
Even if I stop using it someday…
This experiment was already worth it.
What’s Next
- Explore ephemeral runners
- Improve VM provisioning (less manual setup)
- Integrate into a real product workflow
- Write a step-by-step guide for macOS runners



Top comments (0)