This article is about one thing: Flutter iOS CI/CD (primary anchor: Flutter iOS build on Mac mini) — how to run flutter build ipa reliably on a Mac mini M4, split CocoaPods caching, a self-hosted macOS Runner, and Linux-side Android/test jobs. Flutter’s real bottleneck is rarely Dart; it’s centralized dependence on the iOS build chain: pod install → xcodebuild archive → signing → TestFlight upload — all Mac-only. Our team’s default split: Android on Windows/Linux CI, Flutter iOS CI on one dedicated rack Mac mini. Mixing native Xcode? See offloading Xcode builds; runner setup and TCO: self-hosted macOS runners.
1) Why Flutter iOS CI needs its own Mac
“One codebase” reuses business logic, not build planes. iOS artifacts still ride the full Xcode pipeline — you cannot run Flutter iOS CI on ubuntu-latest. Hosted macOS minutes are expensive, queues drift, and without persistent cache every flutter build ipa snaps back to the 15–25 minute band. Flutter iOS CI/CD on an always-on Mac mini buys: pinned Flutter/Xcode, three reusable cache layers, 24/7 without lid-close sleep — so Android and iOS release cadences actually decouple.
flutter test and build apk on Linux. The Flutter iOS CI host only does iOS integration builds and signing.2) Flutter iOS CI cloud architecture
Production Flutter iOS CI/CD topology we run today — developers don’t all need a local Mac; the IPA comes from a self-hosted runner.
Flutter iOS CI in the cloud
local: Dart · Android · git push
ubuntu-latestflutter test · build apk
labels: macos, flutter-ios
fvm flutter build ipapod install · xcodebuild archive
Persistent cache on the Mac mini (what makes Flutter iOS CI fast)
~/.pub-cache- CocoaPods cache
- DerivedData
3) Three ways to run Flutter iOS CI
| Mode | Local / other CI | Cloud M4 Mac | Best for |
|---|---|---|---|
| A. iOS build only in cloud | daily flutter run, Android packages | build ipa, store upload | Win/Linux + device |
| B. Fully remote Flutter (rare) | editor + git only | simulator, green flutter doctor | no local Mac, OK with VNC lag |
| C. Cloud Mac = iOS runner | PR triggers Linux job | self-hosted runner, label macos | frequent releases, strong cache |
We run A + C: merge → flutter test on Linux; Flutter iOS CI on tag/main via the Mac mini runner. B works for short stints without a Mac; long term, runner-ify it.
4) Flutter iOS CI environment checklist (one-time on Mac mini)
- Xcode + CLT: Match
ios/Podfileand Flutter channel minimums; thensudo xcodebuild -license accept. - Flutter SDK:
fvmor fixed~/flutter; commit.fvmrcso devs and CI share a version. - CocoaPods:
gem install cocoapodsor Homebrew; large repos: commitPodfile.lock. - Signing: Fastlane Match or Xcode automatic signing; never commit p12/profiles — encrypted store or CI secrets.
- Network: Region near Git remote and pub.dev/CDN; corporate Git may need VPN — see help center egress notes.
On first boot run flutter doctor -v and paste into the team wiki — that “gold snapshot” is your first triage reference.
5) CocoaPods cache: the Flutter iOS CI speed lever (Before / After)
Same medium Flutter repo (~40+ plugins, release IPA), three runs per environment, median times — Flutter iOS CI variance is almost entirely “does cache stay on the Mac mini?”.
| Environment | First build (cold) | Second build (cache hit) |
|---|---|---|
| Local MacBook Air M2 (8GB, lid closed / throttle) | 18–25 min | 12–15 min |
GitHub macos-latest (no custom cache) | 16–22 min | 14–18 min |
| Mac mini M4 (16GB) · Flutter iOS CI + 3 cache layers | 15–20 min | 4–8 min |
Second builds drop from 12+ minutes to single digits because of the three persistent layers in the diagram — that’s when Flutter iOS build on Mac mini graduates from “it works” to “it’s CI”.
PUB_CACHE/~/.pub-cache: fixed path in runner env, reuse Dart deps across jobs.- CocoaPods cache +
ios/Pods: unchangedPodfile.lock→ skippod install; else incremental. - DerivedData: pin
~/DerivedData; nocleanunless Xcode major bump.
6) Flutter iOS CI command flow (SSH or runner script)
From repo root; swap Flutter version for your lockfile:
cd ~/work/my_flutter_app
export PUB_CACHE=$HOME/.pub-cache
fvm flutter pub get
cd ios && pod install --repo-update && cd ..
fvm flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist
In GitHub Actions, put steps in release-ios.yml with runs-on: [self-hosted, macos, flutter-ios]. TestFlight uploads often ride a more stable path from the Mac mini than home Wi‑Fi.
7) GitHub Actions: split Flutter iOS CI/CD workflows
Two workflows (or matrix + if):
test-android.yml:ubuntu-latest—flutter test,build apk.release-ios.yml:runs-on: [self-hosted, macos, flutter]— tag ormainmerge only:build ipa+ upload.
If VNC debug and runners share one cloud Mac, use a separate build user so manual edits don’t fight checkout. Concurrency/disk: runner TCO. Windows-first teams: Xcode on Windows + cloud Mac — same idea for Flutter without local Xcode.
8) When Flutter iOS CI stalls (how / fix)
Matches typical why / how / fix / error searches — our wiki entry points.
pod install takes 10+ minutes — is that normal?
Why: runner checks out to a temp dir every job, CocoaPods download cache not mounted, or pod install --repo-update in the default script.
Fix: fixed WORK path; hash Podfile.lock and skip when unchanged; move --repo-update to a weekly maintenance job.
xcodebuild archive fails / exit code 65
Why: Flutter/Xcode mismatch, bad DerivedData, native plugin incompatible with SDK.
Fix: log fvm flutter doctor -v at job start; wipe DerivedData and retry; check Xcode release notes for your channel.
signing failed / provisioning mismatch
Why: p12 not in keychain, bundle id ≠ profile, expired Match repo.
Fix: dedicated keychain on Mac mini runner; renew Fastlane Match; VNC “always trust” if needed — the top non-code failure in Flutter iOS CI.
DerivedData fills the disk
Why: parallel branch jobs, old archives, multiple Flutter versions.
Fix: cron ~/Library/Developer/Xcode/Archives; runner concurrency 1–2; disk ≥512GB or expand on schedule.
Flutter version drift → plugin build errors
Why: devs skip fvm, CI uses another channel.
Fix: commit .fvmrc; CI runs fvm install && fvm flutter pub get; PR template checkbox for SDK bumps.
9) When a dedicated Mac mini for Flutter iOS CI pays off
| Your situation | Borrow a Mac / buy used | Cloud M4 Mac mini |
|---|---|---|
| ≥4 iOS releases/month | coordination tax | recommended fixed builder + cache |
| no Mac, Flutter only | hard to sustain | day rental on real build ipa |
| office Mac mini 24/7 | self-host is enough | cloud Mac for DR/peak |
| heavy native plugins | local Mac still handy | release in cloud, debug local |
10) FAQ (Flutter iOS CI/CD)
Flutter iOS CI vs native iOS CI?
Extra flutter pub get and plugin codegen; version must match .fvmrc; Xcode side is still Pods + archive.
vs Codemagic / hosted macOS?
Hosted is fine for a quick proof; when second builds must sit under 10 minutes with strong cache, Flutter iOS build on Mac mini + self-hosted runner wins on control.
Time to first green pipeline?
~1 working day for experienced teams: Xcode/Flutter, runner, signing, one build ipa → TestFlight.
Engineering conclusion: minimum viable Flutter iOS CI (MVF)
If you ship iOS from Flutter at least weekly and daily drivers are Windows/Linux, the smallest setup that holds:
- One 16GB Mac mini M4 (on-prem or dedicated cloud) for Flutter iOS CI only;
- self-hosted macOS runner, separate workflows for Linux
flutter test/build apk; - persistent
.pub-cache, CocoaPods cache, DerivedData; - goal: first
flutter build ipa→ TestFlight on a real repo within 48 hours, then decide on a second machine for concurrency/DR.
Same MVF puts repeat builds in the 4–8 minute band (section 5 table). For dedicated Mac mini nodes over SSH, weigh plans and help center against release frequency — validate the pipeline before you scale.