你的起点

从 KVMFLUX 租用一台专属云端 Mac mini M4,你会拿到真实 Apple 硬件上等同 root 的管理权限:16GB 统一内存、256GB SSD、预装 macOS,可通过 SSH 与 VNC 访问。没有任何共享,也没有虚拟化,所以 xcodebuild 看到的是完整的 M4,Secure Enclave 的行为也和你桌上那台一样。

选一个离你的产物存储最近的区域,而不是离团队最近。一个 runner 与 Git 托管和 CDN 通信的频率,远高于任何人类真正操作它的频率——覆盖新加坡、日本、韩国、香港、美国东部与美国西部

第一步 — 先把 SSH 锁死,再做别的

机器出厂时开启了密码登录,方便你第一次进去。第一次会话就该结束这种状态:先推密钥,再关密码。

本地终端
ssh-keygen -t ed25519 -f ~/.ssh/kvmflux_ci -C "ci-runner"
ssh-copy-id -i ~/.ssh/kvmflux_ci.pub admin@<YOUR_MAC_HOST>
在 Mac 上
sudo tee -a /etc/ssh/sshd_config <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
EOF
sudo launchctl kickstart -k system/com.openssh.sshd

对 CI 节点来说还有两项设置很关键:阻止机器休眠,以及在没有接显示器时不要让后台守护进程暂停。

在 Mac 上
sudo pmset -a sleep 0 displaysleep 0 disksleep 0
sudo systemsetup -setrestartfreeze on

保留 VNC 作为救命通道。有些 Xcode 弹窗——许可协议提示、模拟器首次运行权限——只会在图形界面里出现,你需要一条在 SSH 配置出问题时也能进去的路。

第二步 — 安装 Xcode 工具链

跳过 App Store。xcodes 可以非交互式地安装指定的 Xcode 版本,正是无头节点需要的。先装 Homebrew,再装工具链。

在 Mac 上
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install xcodesorg/made/xcodes aria2
xcodes install 16.2 --experimental-unxip
sudo xcodes select 16.2
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch

确认你实际要构建的目标平台。如果流水线要跑模拟器测试,现在就下载运行时,别等第一个任务时再下。

在 Mac 上
xcodebuild -downloadPlatform iOS
xcrun simctl list runtimes
xcodebuild -version

加上移动端流水线常用的工具:fastlane 用于 lane 与签名,如果仓库带二进制资产就装 git-lfs

在 Mac 上
brew install fastlane git-lfs jq
git lfs install --system

第三步 — 把机器注册为 runner

GitHub Actions、GitLab 和 Buildkite 的模式都一样:下载一个代理,给它一个限定范围的 token,把它当作服务运行。这是 Apple Silicon 上 GitHub Actions 版本的做法。

在 Mac 上
mkdir ~/actions-runner && cd ~/actions-runner
curl -o runner.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gz
tar xzf runner.tar.gz
./config.sh --url https://github.com/your-org/your-app \
  --token <REGISTRATION_TOKEN> \
  --labels macos,arm64,m4 --unattended
./svc.sh install && ./svc.sh start

svc.sh 安装会把 runner 接入 launchd,这样重启后即使没有登录的桌面会话也能继续存活。用标签来指定目标:

.github/workflows/ios.yml
jobs:
  build:
    runs-on: [self-hosted, macos, m4]
    steps:
      - uses: actions/checkout@v4
      - run: xcodebuild -scheme App -destination \
          'platform=iOS Simulator,name=iPhone 16' test

注册 token 一小时后失效,且只能用一次——这部分没问题。runner 的长期凭证保存在 runner 目录里的 .credentials 文件中,所以让这个用户账户保持"无趣":不放个人 SSH 密钥,不留浏览器会话。

第四步 — 给 CI 一个它能自己解锁的密钥串

签名是大多数自托管 Mac 配置最容易卡住的地方。解决方案是一个专用密钥串,由你的 lane 创建、解锁、用完即扔——这样登录密钥串永远不会弹窗,也不会在任务之间泄漏。

签名 lane
KEYCHAIN=ci.keychain-db
security create-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN
security set-keychain-settings -lut 3600 $KEYCHAIN
security unlock-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN
security import dist.p12 -k $KEYCHAIN -P "$P12_PASS" \
  -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: \
  -s -k "$KEYCHAIN_PASS" $KEYCHAIN
security list-keychains -d user -s $KEYCHAIN login.keychain-db

set-key-partition-list 这一行是所有人都容易忘掉的——没有它,codesign 会一直挂着等一个永远不会出现的图形密码弹窗。如果你用 fastlane,配合一个私有证书仓库的 match 能把这整段全部自动化。

缓存:把成果留在磁盘上

专属机器胜过临时 runner 的核心原因正是状态可以持久保留。善用这一点,别每个任务都重新下载全世界:

  • DerivedData — 用 -derivedDataPath ~/ci-cache/dd 指向一个固定路径,增量构建时间通常能从几分钟降到几秒。
  • Swift 包 — 设置 -clonedSourcePackagesDirPath ~/ci-cache/spm,让依赖解析在任务之间复用已有的检出内容。
  • CocoaPods 与 Homebrew — 两者默认都缓存在 runner 用户的主目录下;只是别在构建之间清空工作区。

诚实估算磁盘用量。256GB SSD 上,Xcode 加运行时大约要占用 40GB,一个活跃项目的缓存会逐渐涨到 60–80GB。按计划清理,而不是等出问题才清理:

每周定时任务
find ~/ci-cache/dd -maxdepth 1 -mtime +14 -exec rm -rf {} +
xcrun simctl delete unavailable
df -h /

如果项目自带大体量资产,或需要并存多个 Xcode 版本,额外 SSD +1TB 选配只要 $11.7/月,比到发布时排查磁盘写满的问题划算得多。

并发:一台 M4 能扛几个任务?

一台 16GB 内存的 M4 处理一个重任务表现很好:一次完整的干净构建加模拟器测试套件能用满所有核心。同时跑两个基于模拟器的任务对小应用还行,但内存压力上来后两个都会掉速。

我们经过几个月流水线值守后总结的经验规则:

  • 对构建加测试类工作负载,一个 runner 进程一次跑一个任务,是最稳妥的默认设置。
  • 只有在加了第二台机器之后,才把 lint、单元测试和 UI 测试拆成独立任务——放在同一台机器上排队只会带来额外开销。
  • 夜间任务是白得的算力:把依赖审计和截图跑批安排在该区域时区的非工作时段。

当排队时间成为瓶颈时,加一台租用节点能线性扩容——用同样的标签注册即可,调度器会自动做负载均衡。按日还是按月节点的算账是另一个话题,我们在租期成本拆解里算过;相关工作流也可以在应用场景概览里看到。

整套配置,浓缩版

  1. 仅密钥登录 SSH,关闭睡眠,保留 VNC 作为后备。
  2. xcodes 安装 Xcode,接受许可,预先下载运行时。
  3. 把 runner 注册为带有意义标签的 launchd 服务。
  4. 专用 CI 密钥串,设置好分区列表。
  5. 持久化缓存并配合定时清理。

整套操作动手时间不到一个小时,结果是一个团队不再需要操心的构建节点——这正是重点所在。

在真机上试一下

上面每一条命令都是在我们租出去的这台机器上编写并验证过的。租一台,跟着做,如果不合适就取消——无论哪种结果都不涉及硬件账单。

Mac Mini M4 · 16GB / 256GB
按日$19.3 /天
按周$52.2 /周
按月$96.7 /月
按季$263 /季

专属硬件,美元计费,随时可取消。