VPSSpark Blog
← Back to Dev Diary

Can Two Cloud Macs Run Flutter Team CI?

Server Notes · Cloud Mac CI #9 · 2026.06.08 · ~12 min read

People search: Flutter CI macOS · Flutter iOS pipeline · Cloud Mac self-hosted runner · Flutter team CI architecture · GitHub Actions Flutter iOS

Flutter developer debugging a mobile app on Mac with CI pipeline context
Dual-platform Flutter CI usually bottlenecks on iOS/macOS—two Cloud Macs map cleanly to fast and archive pools.

VPSSpark Cloud Mac CI series #9. This post answers a question we hear constantly: can an 8–15 person Flutter team run stable CI on just two Cloud Macs? Yes—with one caveat: treat Android and iOS separately. Keep Android builds on GitHub-hosted ubuntu-latest; on macOS, split two self-hosted runners into macos-fast and macos-archive pools, and enforce the hard rule that PRs never trigger Archive. Below: load model, job tiers, workflow snippets, and a rollout checklist. For queue diagnosis see series #2; for runner registration and onboarding see the onboarding FAQ.

1 · Counterintuitive truth: Flutter CI bottlenecks are not Dart

Most Flutter teams see CI go red not because flutter test is slow, but because iOS work fills every macOS runner slot—often after someone slipped flutter build ipa into a PR.

Flutter’s cross-platform story tempts teams into one workflow for everything. In practice, flutter analyze and unit tests run fine on Linux; macOS is only mandatory for CocoaPods resolution, Xcode compilation, code signing, and IPA export. Let an Archive job into the PR path and a single Cloud Mac sits locked for 25–40 minutes while every other PR shows Queued—the same trap native iOS teams hit. The diagnostic still holds: wait time >> run time (see macOS runner queue diagnosis).

So “two Cloud Macs” does not mean two general-purpose machines. It means a deliberate dual-pool topology: one Mac for fast feedback, one for release-heavy work. Android APK/AAB builds stay on hosted Linux—do not burn scarce macOS slots on Gradle.

2 · Load model: how much macOS concurrency does an 8–15 person team need?

We benchmark a moderately active Flutter team (1–2 PRs per developer per day, nightly on main, weekly releases):

12
Typical team size
~18
macOS jobs / weekday
2
Cloud Mac slots

On the fast pool (macos-fast), target analyze + unit tests + iOS Simulator build in 8–14 minutes wall time (with pub/DerivedData cache). On the Archive pool (macos-archive), Release IPA plus notarization runs 20–35 minutes but fires rarely—main merges, tags, scheduled nightly only. Mix both pools on two machines and Archive chokes fast; split them and PR P95 wait time usually stays under 10 minutes, which is enough for code review cadence.

Teams above 15 people, or monorepos with matrix builds across many flavors, may need a third Archive node or elastic scaling—that is the capacity and elastic pool decision from series #1, not the “start with two” problem this article solves.

Platform Runner Typical job On PR?
Android ubuntu-latest (hosted) flutter build apk/appbundle, Android unit tests Yes
iOS fast feedback Cloud Mac #1 · macos-fast analyze, unit tests, build ios --simulator Yes
iOS release Cloud Mac #2 · macos-archive build ipa, notarization, TestFlight upload No

3 · Three Flutter job tiers: align with CI hard rules

Map your Flutter pipeline to the series L0/L1/L2 tiers so on-call runbooks stay consistent:

Tier Flutter work Pool PR trigger?
L0 dart format --set-exit-if-changed, flutter analyze, light unit tests Linux or macos-fast Yes
L1 Integration tests, flutter build ios --simulator, widget tests macos-fast Yes · no Archive
L2 flutter build ipa, App Store Connect upload, notarization macos-archive No · main/tag/schedule only

Three hard rules, same as native iOS: Rule 1 PRs must not run L2; Rule 2 L2 lives in an isolated pool; Rule 3 the fast pool must never block behind Archive. Flutter-specific wrinkle: much of L0 can run on Linux—binding it all to macos-fast wastes macOS concurrency. Run Linux and macOS jobs in parallel on PRs; macOS only runs steps that truly require a Mac.

4 · Dual-machine topology: how Cloud Mac #1 and #2 split work

Figure 1 · Typical topology: two Cloud Macs supporting Flutter team CI

GitHub Actions · PR / main splitAndroid → ubuntu-latest
Cloud Mac #1 · macos-fastL0/L1 · 8–14 min/job
Cloud Mac #2 · macos-archiveL2 · main/tag only

Cloud Mac #1 (fast pool): register with labels [self-hosted, macOS, macos-fast, flutter]. Preinstall a pinned Flutter SDK (fvm or image-level pin), Xcode, and CocoaPods. Run the runner as a always-on service with auto-start on boot. This machine can aggressively retain pub cache and DerivedData—jobs are short and turnover is high.

Cloud Mac #2 (Archive pool): labels [self-hosted, macOS, macos-archive, flutter-release]. Physically separate from the fast pool—do not hang runners for both pools on one Mac, or Rule 2 is meaningless. Store Distribution certificates and App Store Connect API keys here (Keychain + least-privilege tokens; registration steps in the 30–60 minute onboarding checklist). Before a release, run flutter doctor -v and a dry-run Archive on this machine so you do not pollute the fast pool.

Keep both machines in the same region on the same Xcode major version to avoid “fast pool green, Archive red” drift. Cloud Mac beats an office Mac for CI because image specs are fixed, GitHub connectivity is stable, and you skip UPS and physical security for a 7×24 node. Read GitHub’s guide on self-hosted runner security boundaries and scope Archive machine permissions to a single repo or org.

5 · Workflow example: PR vs main split

The snippet below shows core routing logic (a full pipeline still needs cache, secrets, and artifact steps). Flutter’s official continuous delivery docs cover iOS signing in depth; here we assume certificates are already in the Archive machine Keychain.

flutter-ci.yml (core routing)
jobs:
                  android-pr:
                    if: github.event_name == 'pull_request'
                    runs-on: ubuntu-latest
                    steps:
                      - run: flutter analyze && flutter test
                      - run: flutter build appbundle --release

                  ios-pr-fast:
                    if: github.event_name == 'pull_request'
                    runs-on: [self-hosted, macOS, macos-fast, flutter]
                    steps:
                      - run: flutter analyze
                      - run: flutter test
                      - run: flutter build ios --simulator --no-codesign

                  ios-release:
                    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
                    runs-on: [self-hosted, macOS, macos-archive, flutter-release]
                    steps:
                      - run: flutter build ipa --export-options-plist=ExportOptions.plist
                      # TestFlight upload / notarization — L2 only

Note that ios-pr-fast has no build ipa. If product needs a device build on a PR, trigger it manually via workflow_dispatch and still route through the Archive pool—never as a default PR event. In GitHub Actions workflow syntax, a concurrency group that cancels stale runs for the same PR cuts useless queue time by roughly 20%.

6 · Cache checklist: four directories Flutter teams should persist

Two Cloud Macs stay stable when cache beats CPU tier. Persist these under the runner user home (or nightly snapshots):

  • PUB_CACHE / ~/.pub-cache — Dart dependencies; incremental refresh when pubspec.lock changes
  • Flutter SDK directory — pin with fvm; avoid flutter upgrade drift in CI
  • ios/Pods + CocoaPods download cachepod install is a common iOS tail latency
  • Xcode DerivedData — high payoff when ios/Podfile.lock is stable

Fast and Archive pools must not share one DerivedData directory—Debug/Simulator and Release/Archive artifacts mixed together cause bizarre link errors. Keep Android Gradle cache on ubuntu-latest via Actions Cache; no need to occupy Cloud Mac disk.

Image baseline tip
Monthly “clean image” upgrade: Flutter minor bump + Xcode patch + pod repo update. Validate on the Archive machine first, then sync the fast pool. Day-to-day PRs reuse cache—do not put flutter pub upgrade in default CI steps.

7 · Rollout checklist: zero to dual-machine CI

Follow this order; a PoC fits in 1–2 business days:

  • Provision two same-spec Cloud Macs (M4 + 16GB minimum—iOS compile peaks often exceed 12GB RAM)
  • #1: Flutter/Xcode/CocoaPods, register macos-fast runner; #2: import signing assets, register macos-archive
  • Split workflows: Android on ubuntu-latest; PR macOS L1 only; main/tag for L2
  • Persist pub/DerivedData/Pods; watch 3 days—P95 wait time should stay < 10 min
  • If wait >> run still holds, check for L2 in PR workflows before adding hardware

During PoC you can buy one Cloud Mac for the fast pool and temporarily use GitHub-hosted macos-latest for Archive (accept the queue)—prove pool separation, then add the second machine. That does not contradict “start with two”: the second Mac is release SLA insurance, not a PR feedback requirement.

8 · FAQ

Can all Flutter unit tests run on Linux— is one Cloud Mac enough?

If PRs skip macOS compile checks, theoretically yes—but most teams run build ios --simulator on PRs to catch native plugin issues. One Mac for L1 works; Archive must stay isolated from fast feedback, or releases queue every PR. Two machines is the safe default.

How do two Cloud Macs compare to two Mac minis in the office?

Same topology; different ops. Cloud Mac skips hardware procurement, supports image snapshots, and usually has steadier GitHub bandwidth. Office Macs suit always-on 7×24 with proper rack space; distributed or short-cycle teams often pick Cloud Mac. See capacity planning and elastic pools in series #1.

Should we keep Codemagic or GitHub-hosted macOS?

As Archive backup or PR overflow, yes. Once self-hosted is the primary path, hosted macOS fits teams shipping fewer than four times per month; active Flutter teams usually win on cost with self-hosted long term.

How do monorepos with multiple apps count jobs?

Multiply by flavor/matrix size. If a PR matrix spawns more than four macOS jobs, two fast-pool machines may not suffice—narrow the matrix or add a third machine instead of pushing Archive into PRs.

Series navigation (#9 Flutter dual-machine CI)
#1 Capacity · #2 Queues · #3 Self-hosted economics · #8 Build speed · #9 this article

Two Cloud Macs, Flutter dual-platform CI in the right lanes

In a Flutter team’s dual-platform pipeline, Android can live on Linux—but iOS compile, signing, and IPA export require macOS. Two Cloud Mac mini M4 nodes split macos-fast and macos-archive so PR feedback and release-heavy work never fight for the same slot—far better than stuffing Archive into PR workflows and watching everyone sit in Queued. Apple Silicon unified memory keeps flutter build ios and Xcode link stages smooth; ~4W idle draw suits a 7×24 CI node.

Compared to building an office Mac rack, Cloud Mac provisions by plan, images clone cleanly, and GitHub connectivity is more predictable—good for distributed Flutter teams standing up dual-pool topology fast. Native macOS Unix runs Flutter, CocoaPods, and Fastlane without an extra virtualization layer.

If you are planning iOS CI migration for a Flutter team, two VPSSpark Cloud Mac mini M4 machines are the minimum viable topologyexplore plans now, prove the fast pool in a PoC, then add the Archive pool for releases.

Limited offer

Flutter CI stuck on iOS? Split pools on two Cloud Macs

macos-fast + macos-archive · PR vs release isolation

Back to home
Limited offer See plans now