How I Turned a Mac Studio Into a Frontend CI Workhorse

I wired a Mac Studio into my CI pipeline and stopped waiting forever on frontend builds. Here is the exact setup, what broke, and what actually got faster.
How I Turned a Mac Studio Into a Frontend CI Workhorse
Photo by Jaime Marrero / Unsplash

Why I put a Mac Studio in my CI pipeline

I got tired of my CI lying to me.

Cloud runners pretended to be fast. They were fine for tiny libraries, but my real projects are not tiny. Big Next.js apps. Tailwind. Storybook. Playwright. A pile of Node tools that barely tolerate each other. Every pull request felt like a mini release cycle.

Local builds on my M2 machine were quick. CI builds on generic x86 runners were two to three times slower. That mismatch kept biting me. Locally green. CI red. Or worse, everything green until a Mac-specific bug popped up on production devices.

So I stopped outsourcing performance and bought a Mac Studio. I decided it should behave like a very opinionated build server that just happens to sit under my desk in the Netherlands.

Apple Silicon is ridiculously good at frontend workloads. Bundling. TypeScript. Image optimization. Headless browsers. It eats all of that for breakfast. The trick is wiring that power into a continuous integration pipeline without turning your team into SSH admins.

What I actually wanted from Apple Silicon CI

I wrote down a simple list before touching anything:

  • CI builds should be as fast as my local M2 build, or close.
  • No manual SSH dance for each run.
  • Mac Studio must survive reboots, power cuts, and updates without babysitting.
  • Same Node and toolchain versions on developer machines and CI.
  • Easy to swap out the hardware later.

If a solution did not tick those boxes, I dropped it. That killed a lot of overcomplicated ideas quickly.

The hardware and basic setup

I went with a Mac Studio M2 Max, 64 GB RAM, 1 TB SSD. Not cheap, but I treat it as a shared power tool. Everyone uses it, nobody owns it.

Physical setup specifics:

  • Wired Ethernet to the router. WiFi is for humans, not for CI.
  • Connected to a cheap UPS. Builds should not die because the kettle trips the fuse.
  • Screenless most of the time, but I keep a spare HDMI monitor nearby for when macOS gets picky.

Software baseline:

  • Fresh macOS install with FileVault on.
  • Disabled anything that pops up UI dialogs during boot.
  • Created a dedicated ci user account.
  • Installed Xcode Command Line Tools, Homebrew, Node via nvm.

This is boring infrastructure work. Do it once. Document it like you are going to throw the Mac in a canal and need to rebuild everything on a new machine tomorrow.

Choosing the integration style: runner vs remote shell

There are really two ways to integrate a Mac Studio into CI:

  • Run a native CI agent on it. GitHub Actions self-hosted runner, GitLab Runner, whatever.
  • Treat it as a generic SSH target. Use your existing CI just to orchestrate and push commands over SSH.

I tried both. For frontend work, I prefer a native runner. It is less fragile, logs end up where you expect, and you get native caching behavior.

So I will describe the GitHub Actions runner path, then mention how to adapt if you prefer SSH.

Installing a GitHub Actions runner on the Mac Studio

On the Mac Studio, logged in as the ci user, I installed a self-hosted runner for our org. GitHub has a wizard, but the core steps are:

  • Create a directory, for example /Users/ci/actions-runner.
  • Download the runner tarball for macOS ARM from GitHub.
  • Run ./config.sh and register it with the repo or org.
  • Label it something explicit like mac-studio and apple-silicon.

I then turned the runner into a service using the provided ./svc.sh script. That makes it start automatically on boot. No manual launchpad clicking. No forgotten terminal window.

From that point on, GitHub Actions sees the Mac Studio as just another runner. Which is the whole point.

Declaring jobs that must run on Apple Silicon

In the workflow files, I split jobs by what they actually need.

Example:

jobs:
  lint-and-unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run lint
      - run: npm test

  mac-build-and-e2e:
    runs-on: self-hosted
    labels: [mac-studio, apple-silicon]
    needs: lint-and-unit-tests
    steps:
      - uses: actions/checkout@v4
      - name: Use Node 20
        run: |
          source ~/.nvm/nvm.sh
          nvm use 20
      - run: npm ci
      - run: npm run build
      - run: npm run test:e2e

The fast, embarrassingly parallel stuff stays on cheap cloud runners. Lint and unit tests do not care about CPU architecture. They just want cores.

The expensive, architecture-sensitive work lands on the Mac Studio. That usually means the production build and any Playwright or Cypress suites that are supposed to mimic real user environments more closely.

Matching local and CI toolchains

If your local setup looks nothing like your CI, you will hate your life. I keep them almost identical.

  • Same macOS major version on dev machines and CI, whenever possible.
  • Same Node major version, managed by nvm.
  • Same package manager. I use pnpm now, previously npm. Pick one and commit.
  • Same global tools installed via package.json scripts rather than random global installs.

I also added a simple ci:doctor script to each main repo.

"scripts": {
  "ci:doctor": "node scripts/ci-doctor.mjs"
}

That script checks Node version, presence of required binaries like ffmpeg for video capture in tests, and even free disk space. It fails loudly before we start a 12 minute build. I think this kind of preflight check is underrated.

Leveraging Apple Silicon performance where it matters

So what actually speeds up on Apple Silicon for frontend CI?

Concrete numbers from one Next.js + Storybook + Playwright project:

  • Production build time went from ~7 minutes on the standard GitHub hosted runner to ~3 minutes on the Mac Studio.
  • Storybook static build dropped from ~5 minutes to ~2 minutes.
  • Full Playwright suite (visual + interaction tests) shrank from ~11 minutes to ~4.5 minutes.

Not magic. Just cores, fast SSD, and an architecture that loves Node and V8.

Those numbers turned PR builds from "go get coffee" into "scan Slack, come back, results are ready". That changes how you work. You ship smaller changes. You are less afraid to run the full suite often.

Making cache work on a long-lived Mac

Cloud runners are stateless. Your Mac Studio is not. That is both helpful and dangerous.

Helpful because you can keep heavy caches between runs:

  • node_modules for some projects.
  • .next/cache or similar build cache directories.
  • Playwright browser binaries.

Dangerous because stale caches can cause mysterious bugs that only show up on CI.

My rule: cache aggressively, but version everything.

  • Use lockfile hashes in cache keys.
  • Mix in Node version into keys.
  • Have a ci:clean command that wipes everything. Use it regularly.

On the Mac Studio, I also wrote a weekly cron job that purges temporary directories older than 14 days in /Users/ci/tmp and clears some Library/Caches folders. macOS loves to hoard.

Dealing with macOS quirks in a CI context

Running CI on macOS is not the same as running it on Ubuntu in a container. macOS has opinions.

Some real issues I hit:

  • Login window after reboot. If the machine reboots and does not auto login the ci user, the runner is dead. I set auto login for the CI user and locked down everything else.
  • Sleep settings. Turn off every sleep and display power saving option for the CI user. I want it awake 24/7.
  • Dialog popups. First-time security prompts for things like screen recording or accessibility can block headless browser tests. I ran the test suite once interactively, granted all permissions, then it was smooth.
  • Updates. I turned off automatic macOS updates. Instead, I schedule a manual update window once a month. Yes, that requires discipline.

Each of those can silently kill your builds and waste hours. If your Mac Studio is in an office, tape a big "CI NODE" label on it and keep people from treating it like a spare workstation.

Securing a CI Mac that lives on your network

A self-hosted runner is basically a door into your network that GitHub can knock on. So you should secure it properly.

My non-negotiables:

  • Dedicated CI user account. No personal accounts on that machine. Ever.
  • SSH keys only. No password login. Keys with passphrases where possible.
  • Firewall on. Only allow SSH from the local network and whatever is absolutely required.
  • No secrets in repos. Use GitHub Actions secrets or a proper secret manager. The Mac is treated as compromised by default.
  • Audit scripts. I kept a tiny log of when I changed runner configuration or installed global tools.

It is not a bank, but it is also not a toy. Treat it like production infra because, functionally, it is.

When a Mac Studio runner is not worth it

I like this setup, but I do not think everyone should copy it.

For example, if your frontend builds are already under 3 minutes and you rarely run E2E tests in CI, the complexity is not worth it. You will just create a new pet to maintain.

Also, if your team is not comfortable owning hardware, patching OS updates, and watching disk space, you are going to resent this box. Managed macOS CI from services like Buildkite, Circle, or dedicated Mac cloud providers might be better.

I picked the Mac Studio path because I already live in Apple Silicon land and I like owning the full stack. When something is slow, there is no black box support ticket to open. I can literally walk over and see what the CPU is doing.

What changed in my actual workflow

The biggest shift was psychological. The pipeline stopped feeling like a separate universe.

The CI logs now look like my local terminal output. Same Node version, same OS, same architectures, same weird warnings. When something fails in CI, I can usually reproduce it locally without fighting random environment differences.

Feedback loops tightened. On feature branches, we now run the full build plus E2E suite by default. Before, we only did that on main because it was too slow and expensive on cloud macOS runners.

Also, I simplified my local dev. I lean on the CI to run heavy visual regression suites while I just focus on unit tests and fast stories locally. The Mac Studio became the muscle that I rent for bigger checks.

Would I do it again?

Yes. For large frontend projects, pairing a cloud CI control plane with a beefy Apple Silicon node hits a sweet spot.

You get the speed and realism of running on the same class of machine your users hold in their hands. You keep the convenience of GitHub Actions or whatever orchestration you already use. And you can scale sideways later by adding another Mac if this one reaches capacity.

If your pipeline feels slow, and your local M-series machine feels fast, do not shrug and accept that gap. Put an Apple Silicon box in the pipeline and make CI feel like your desk again.

Subscribe to my newsletter

Subscribe to my newsletter to get the latest updates and news

Member discussion