С чего вы начинаете

Арендовав выделенный облачный Mac mini M4 у KVMFLUX, вы получаете фактически root-доступ на настоящем железе Apple: 16ГБ единой памяти, 256ГБ SSD, предустановленный macOS с доступом по SSH и VNC. Никакого совместного использования, никакой виртуализации — поэтому xcodebuild видит полноценный M4, а Secure Enclave ведёт себя так же, как на вашей настольной машине.

Выбирайте регион ближе к хранилищу артефактов, а не к команде. Раннер обращается к репозиторию и CDN намного чаще, чем к нему подключается человек — доступны регионы Сингапур, Япония, Южная Корея, Гонконг, Восточное побережье США и Западное побережье США.

Шаг 1 — сначала заблокируйте 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 сломается.

Шаг 2 — установите тулчейн 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

Определитесь с целевыми платформами. Если конвейер запускает тесты в симуляторе, загрузите нужные runtime заранее, а не при первой же задаче.

На 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

Шаг 3 — зарегистрируйте машину как раннер

GitHub Actions, GitLab и Buildkite работают по одной схеме: скачиваете агент, даёте ему токен с ограниченной областью действия и запускаете как сервис. Вот как это делается для GitHub Actions на Apple Silicon.

На 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 подключает раннер к 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

Токен регистрации истекает через час и одноразовый — это нормально. Долгосрочные учётные данные раннера хранятся в файле .credentials внутри его каталога, так что держите эту учётную запись «пустой»: без личных SSH-ключей, без сохранённых сессий в браузере.

Шаг 4 — дайте 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 с приватным репозиторием сертификатов автоматизирует всё это целиком.

Кэш: оставляйте результаты на диске

Главное преимущество выделенной машины перед эфемерным раннером — сохраняемое состояние. Используйте это, не скачивая всё заново на каждой задаче:

  • DerivedData — укажите фиксированный путь через -derivedDataPath ~/ci-cache/dd, и время инкрементальной сборки обычно падает с минут до секунд.
  • Пакеты Swift — задайте -clonedSourcePackagesDirPath ~/ci-cache/spm, чтобы разрешение зависимостей повторно использовало уже выполненные checkout между задачами.
  • CocoaPods и Homebrew — оба по умолчанию кэшируются в домашней папке пользователя раннера; просто не очищайте рабочую область между сборками.

Честно оцените использование диска. На SSD 256ГБ сам Xcode с runtime занимает около 40ГБ, а кэш активного проекта постепенно вырастает до 60–80ГБ. Чистите по расписанию, а не когда уже возникла проблема:

Еженедельная задача по расписанию
find ~/ci-cache/dd -maxdepth 1 -mtime +14 -exec rm -rf {} +
xcrun simctl delete unavailable
df -h /

Если в проекте крупные ресурсы или нужно держать несколько версий Xcode одновременно, опция доп. SSD +1ТБ стоит всего $11.7/мес. — это дешевле, чем разбираться с забитым диском перед релизом.

Параллелизм: сколько задач выдержит один M4?

Один M4 с 16ГБ памяти отлично справляется с одной тяжёлой задачей: полная чистая сборка плюс набор тестов в симуляторе загружает все ядра. Одновременный запуск двух задач с симулятором подойдёт для небольших приложений, но при росте нагрузки на память обе замедлятся.

Вот эмпирические правила, к которым мы пришли после месяцев обслуживания конвейеров:

  • Для нагрузок «сборка плюс тесты» один процесс раннера, обрабатывающий одну задачу за раз, — самая надёжная настройка по умолчанию.
  • Разделяйте lint, юнит-тесты и UI-тесты на отдельные задачи только после того, как добавите вторую машину — на одной машине это лишь создаёт очередь и накладные расходы.
  • Ночные задачи — бесплатные вычисления: планируйте аудит зависимостей и прогон скриншотов на нерабочие часы часового пояса региона.

Когда время ожидания в очереди становится проблемой, добавление ещё одного арендованного узла масштабируется линейно — просто зарегистрируйте его с теми же метками, и планировщик сам распределит нагрузку. Расчёт выгодности суточного и постоянного узла — отдельная тема, которую мы разобрали в статье о расходах на периоды аренды; соответствующие рабочие процессы также показаны в обзоре сценариев использования.

Вся настройка в сжатом виде

  1. Вход по SSH только по ключу, сон отключён, VNC оставлен как резервный вариант.
  2. Установите Xcode через xcodes, примите лицензию, заранее скачайте runtime.
  3. Зарегистрируйте раннер как сервис launchd с осмысленными метками.
  4. Отдельный брелок для CI с настроенным списком партиций.
  5. Постоянный кэш с регулярной очисткой по расписанию.

Вся процедура занимает меньше часа практической работы, а результат — узел сборки, о котором команде больше не нужно думать. Это и есть главная цель.

Проверьте это на настоящем железе

Каждая команда выше написана и проверена на той же машине, которую мы сдаём в аренду. Возьмите одну, повторите шаги, и если что-то не подойдёт — отмените аренду. В любом случае никаких обязательств по железу не остаётся.

Mac Mini M4 · 16GB / 256GB
Сутки$19.3 /сутки
Неделя$52.2 /нед.
Месяц$96.7 /мес.
Квартал$263 /кварт.

Выделенное железо, оплата в USD, отмена в любой момент.