macOS CI · AWS EC2 Mac · GitHub Actions

A fleet of Mac runners you can run from the command line.

I design and set up self-hosted GitHub Actions runners for iOS and macOS teams, on AWS EC2 Mac dedicated hosts. Building images, deploying, scaling and recovering the fleet are all GitHub workflows you trigger with gh. You end up owning the infrastructure, the code, and the knowledge to run it.

Building macOS CI infrastructure since 2020 Presented at SwiftLeeds and Swift Heroes
fleet · iOS · production · eu-west-1
machine types 2busy 0/6
$ gh workflow run deploy.yml -f team=iOS -f env=production
✓ Created workflow_dispatch event for deploy.yml
# plan → approval → apply. Fleet now matches fleets/ios.yml
Who it's for

Teams shipping to Apple platforms who have outgrown their CI.

macOS is the one CI platform that can't run on commodity Linux machines. Apple licensing requires Apple hardware, and AWS only rents it by the whole machine. Getting that right is where most teams lose time and money.

Hosted macOS minutes keep adding up

Per-minute billing grows with every pull request, and the bill is hard to predict once UI tests and archives are in the pipeline.

Flat hourly cost per machine, scaled to your working week.

You need control of the toolchain

Your release can't wait for a hosted image to ship the Xcode you need, or to keep the one you still depend on.

Your own images with the exact macOS, Xcode and Ruby versions you pick.

The Mac mini shelf doesn't scale

Office machines go offline, nobody owns them, and there's no alerting when a disk fills up at 2am before a release.

Cloud machines with monitoring, alarms and automatic recovery.

How it works

Infrastructure as code, operated through GitHub.

Packer builds the macOS image, Terraform deploys it to dedicated hosts in your AWS account, and GitHub Actions workflows run both. Every machine registers itself with your runner group on boot.

GitHub github.com or GHE

Operational workflows, fleet definitions in YAML, approval gates per environment, and the runner groups your app pipelines target.

orchestrates

Packer images

Builds a macOS AMI from Amazon's base image with Xcode, simulators, Ruby, Homebrew tooling, the Actions runner and the CloudWatch agent.

builds AMI · deploys with

Terraform provisioning

EC2 Mac instances, IAM roles, security groups, alarms and dashboards. Remote state in S3 with DynamoDB locking.

Your AWS account · private subnet dedicated hosts
prod
mac-m4.metalM4 · runner + watchdog
prod
mac-m4.metalM4 · runner + watchdog
prod
mac2.metalM1 · runner + watchdog
prod
mac2.metalM1 · runner + watchdog
qa
mac2.metalM1 · new AMI canary
qa
mac2.metalM1 · new AMI canary
Secrets ManagerRunner registration token, read at boot over IMDSv2
CloudWatch + SNSPer-team dashboards, disk alarms, watchdog metrics
IAM per teamLeast-privilege roles, isolated security groups
S3 + DynamoDBVersioned, encrypted, locked Terraform state
Day-to-day operations

No AWS console. No SSH. Run a workflow.

Each operation is a GitHub workflow, so anyone on the team with the right permissions can run it from the gh CLI or the Actions tab. Changes go through code review and approval, and every run leaves an audit trail.

~/platform-ci
Capabilities

What you get on day one.

A proven foundation, customised for your organisation: your teams, your AWS account, your toolchain, your release calendar.

Fleet as code

Fleet sizes live in a reviewed YAML file

Each environment lists its machine types, how many runners of each, and the image they run. Adding capacity, or trying a new Xcode on QA before production, is a pull request.

# fleets/ios.yml
production:
  - machine: mac-m4.metal
    runners: 6
    image: macos-26.1-xcode-27.1
  - machine: mac2.metal
    runners: 4
    image: macos-26.1-xcode-27.1
qa:
  - machine: mac2.metal
    runners: 1
    image: macos-26.2-xcode-27.2  # canary
Scheduling

Scales with your working week

Scheduled workflows shrink the fleet when nobody is building, such as weekends, and bring it back before your team starts work.

Deployments

Zero-downtime Xcode upgrades

Separate production and QA environments per team. Try a new image on QA first, then roll it out to production.

Self-healing

Runners that recover on their own

A watchdog reboots a runner that stops listening, with a circuit breaker after three attempts. Every job starts with fresh simulators and no stale DerivedData.

Observability

Dashboards and alarms per team

The CloudWatch agent is baked into every image and reports CPU, memory, disk and disk I/O. Each team gets a dashboard, and alarms email you before a disk fills up.

Read how it works
Multi-tenant

One platform, many teams

Each team gets its own state, IAM role, security group, runner group, alarms and approval gates, on shared networking.

Security

Private by default

No public IPs. IMDSv2 only. Tokens in Secrets Manager, never in code. Standard tags on every resource for cost allocation.

Cost control

Idle hosts get released

An hourly workflow finds dedicated hosts with no instance and releases them once AWS's 24-hour minimum has passed.

Quality gates

Tested like application code

Pull requests run actionlint, packer validate, terraform test and Bats tests for the shell scripts.

Working together

You hire me, not a licence.

There's no product to subscribe to. I work with your team as a freelance consultant, set up the system in your accounts, tailor it to how you ship, and hand it over so you can run it yourselves.

Step 1

Talk

A conversation about how your team builds and ships.

  • Your pipelines and toolchain
  • Teams, fleets and environments
  • Release calendar and peaks
Step 2

Build

Set up the platform in your AWS account and GitHub organisation.

  • Custom macOS images
  • Fleets, IAM, monitoring
  • Workflows and schedules
Step 3

Roll out

Move your app pipelines over, starting with QA.

  • Runner labels in your workflows
  • Side-by-side with existing CI
  • Tuning from real build data
Step 4

Hand over

Your team owns and runs it from here.

  • Runbooks and architecture docs
  • Training session for your engineers
  • Full ownership of code and infrastructure
Pricing

What the machines cost.

You pay for two things: my consultancy to deliver the solution, and the machines it runs on. AWS bills the machines directly to your account. One dedicated host runs one runner, priced per hour with a 24-hour minimum each time a host is allocated.

On-demand, USD · May 2026 · monthly = hourly × 730 h
Instance typeChipPer hourPer dayPer month

Savings Plans lower these rates. For machines that run all the time, a 1- or 3-year AWS Savings Plan cuts the hourly price. A common setup covers the always-on baseline with a Savings Plan and adds on-demand hosts for busy periods.

Estimate your fleet

Schedule
Estimated AWS cost
$0 / month
Per runner, per month$0
Billed hours / runner / month0
Per year$0

On-demand rates. Weekdays only assumes hosts are released from Friday night to Monday morning, about 60 hours a week. Excludes storage and data transfer, which are usually small next to host costs.

The engagement

Consultancy and a delivered solution

One engagement covers everything. I design the platform with you, deliver it into your AWS account and GitHub organisation, and get your pipelines running on it.

  • Custom macOS images with your Xcode versions
  • Fleets on dedicated hosts in your chosen region
  • Scaling schedules for your working week and releases
  • Rolling deployments for zero-downtime upgrades
  • Dashboards, alarms and self-healing runners
  • Operational workflows you run with gh
  • Migration of your app pipelines
  • Runbooks, architecture docs and team training
Get in touch
What you pay
To me · one-off Consultancy + delivery

Quoted per project after a first conversation, based on your teams, fleets and pipelines.

To AWS · monthly Infrastructure

Billed directly to your AWS account through standard AWS billing, at the rates above.

Once the system is operational, your team owns and operates it from then on. No licence, no subscription, no lock-in.

Talks

The thinking behind it, on stage.

I've presented the architecture, the trade-offs and the lessons from running it at two Swift conferences.

About

Alberto De Bortoli

I've been building CI/CD infrastructure on macOS hardware since 2020. Along the way I learned what it takes to make Apple build machines scale, stay observable, and recover on their own.

Now I help teams build their own in-house solution on top of that knowledge, tailored to their AWS account, GitHub organisation, toolchain and release calendar.

Connect on LinkedIn
FAQ

Common questions.

Something else on your mind? Ask me on LinkedIn.

Why not use GitHub-hosted macOS runners?

They're a good start. Once you run many builds a day, per-minute pricing usually costs more than renting the machines, and you can't pick your exact Xcode versions or keep machines tuned to your project. The estimator above gives a first idea, and we'll work out your break-even point together.

Why dedicated hosts?

Apple's macOS licence requires Apple hardware, so AWS rents each Mac as a whole physical machine on a dedicated host. That also means no noisy neighbours and consistent build times.

What does the 24-hour minimum mean for us?

AWS charges each Mac host for at least 24 hours once it's allocated. The scaling schedules and the host-release workflow are designed around this, so you don't pay for hosts you briefly needed.

Do we need a specific GitHub plan?

No. Self-hosted runners work with every GitHub plan, on github.com and on GitHub Enterprise Server. On GitHub Team and Enterprise, runner groups can also keep each team's runners separate. On other plans, jobs pick their runners by label.

Can we lower the AWS bill?

Yes, in two ways. Scheduling releases hosts when nobody is building, such as weekends. For the part of the fleet that runs all the time, a 1- or 3-year AWS Savings Plan lowers the hourly rate. We'll pick the mix that fits your usage.

Which regions can we use?

Any AWS region that offers EC2 Mac instances. Availability of each instance type differs by region, as the pricing table shows. I'll recommend a region based on your team's location and the instance types you need.

What happens after the engagement ends?

Your team owns and operates the system from then on. You keep the repository, the images and the infrastructure, along with runbooks and training, and you pay AWS directly through your usual AWS billing.

Get in touch

Let's size your fleet.

Send me a message on LinkedIn. A short conversation is enough to tell whether self-hosted macOS CI makes sense for you, and roughly what it would cost.

linkedin.com/in/albertodebortoli
  • How many iOS / macOS engineers you have
  • Roughly how many CI builds you run per day
  • What you use for CI today
Message me on LinkedIn
© 2026 Alberto De Bortoli AWS, GitHub, Apple, macOS and Xcode are trademarks of their respective owners.