本文へスキップ

Mac Mini M4でのGitLab CI/CDランナー:完全ガイド

専用のMac Mini M4にGitLab CI/CDランナーをインストールして構成します。Apple SiliconでiOSアプリをネイティブにビルドし、実際のシミュレーターでテストを実行し、TestFlightへデプロイする、すべてをGitLabパイプラインから実行できます。

CI/CD 読了30分 2026年10月更新

なぜMacでセルフホスト型のGitLabランナーを使うのか?

GitLabはLinux上の共有ランナーを提供していますが、iOSアプリのビルドにはApple製ハードウェア上で動作するmacOSが必要です。GitLab公式のmacOS共有ランナーは制約が多く、コストも高くなります。セルフホスト型のMac Mini M4ランナーなら、次のメリットが得られます。

  • 無制限のCI/CD実行時間

    GitLabの無料プランでは、共有ランナーで400分のCI/CD実行時間が含まれます。セルフホストなら制限はありません。

  • ネイティブなApple Silicon

    ユーザーのデバイスと同じM4チップ上でビルドできます。Rosettaによる変換のオーバーヘッドはありません。

  • 環境の完全なコントロール

    必要なXcodeのバージョン、シミュレーター、ツール、依存関係を自由にインストールできます。

  • 永続的なキャッシュ

    DerivedData、SPMパッケージ、CocoaPodsのキャッシュがパイプライン実行間で保持されます。

GitLab Runnerをインストールする

専用 Mac mini M4 ビルドサーバーにSSH接続し、Homebrewを使用してGitLab Runnerと、ビルドログを読みやすくするxcbeautifyをインストールします。

# Connect to your Mac Mini M4
ssh admin@your-server-ip

# Install Homebrew (if not already installed)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

# Install GitLab Runner, plus xcbeautify for the pipeline's build logs
brew install gitlab-runner xcbeautify

# Verify installation (prints the version and OS/Arch: darwin/arm64)
gitlab-runner --version

macOSサービスとしてインストールする

# Run the runner as a launchd service. It is a LaunchAgent: it starts when
# your user session starts, so on a headless Mac enable automatic login for
# this user (System Settings > Users & Groups) to bring it back after a reboot
brew services start gitlab-runner

# Verify the service is running
brew services list | grep gitlab-runner
# gitlab-runner started admin ~/Library/LaunchAgents/homebrew.mxcl.gitlab-runner.plist

ランナーを登録する

GitLabのプロジェクト(またはグループ)を開き、Settings > CI/CD > Runners > New project runner に移動します。Tags に macos と m4 を入力し(後述のパイプラインがこのタグを指定します)、ランナーを作成して、表示された認証トークンをコピーします。認証トークンを使う場合、タグなどのランナー設定は register コマンドではなく、この GitLab の画面で行います。

新しいランナー登録フローで登録する(GitLab 16以降)

# Register the runner using the authentication token from GitLab UI
# (GitLab 16+ uses authentication tokens instead of registration tokens).
# Tags, untagged jobs and locking come from the runner you created in GitLab:
# do not pass --tag-list or --run-untagged with an authentication token
gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --token "YOUR_RUNNER_AUTHENTICATION_TOKEN" \
  --executor "shell" \
  --description "mac-mini-m4-runner"

# Confirm the runner can reach GitLab
gitlab-runner verify

レガシーの登録トークンで登録する(GitLab 15以前)

# For older GitLab instances using registration tokens
gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --registration-token "YOUR_REGISTRATION_TOKEN" \
  --executor "shell" \
  --description "mac-mini-m4-runner" \
  --tag-list "macos,apple-silicon,m4,ios,xcode" \
  --run-untagged="false"

ランナーの構成を確認する

# View the runner config file
cat ~/.gitlab-runner/config.toml

# Expected output:
# concurrent = 2
# check_interval = 0
#
# [session_server]
#   session_timeout = 1800
#
# [[runners]]
#   name = "mac-mini-m4-runner"
#   url = "https://gitlab.com/"
#   token = "..."
#   executor = "shell"
#   [runners.cache]
#     MaxUploadedArchiveSize = 0

# Adjust concurrency based on your hardware:
# Mac Mini M4 (16GB): concurrent = 2
# Mac Mini M4 Pro (24GB): concurrent = 3
# Mac Mini M4 Pro (48GB): concurrent = 4

~/.gitlab-runner/config.toml を編集して、並行実行数(concurrency)を調整します。

# Edit the config
nano ~/.gitlab-runner/config.toml

# Set concurrent to match your hardware capacity
concurrent = 2

# Restart the service to apply changes
brew services restart gitlab-runner

これでランナーは、GitLabプロジェクトの Settings > CI/CD > Runners に オンライン として表示されるはずです。

iOS向けに.gitlab-ci.ymlを作成する

リポジトリのルートに .gitlab-ci.yml ファイルを作成します。この完全なパイプラインは、iOSアプリのビルド、テスト、デプロイを行います。

# .gitlab-ci.yml

stages:
  - build
  - test
  - deploy

variables:
  SCHEME: "MyApp"
  WORKSPACE: "MyApp.xcworkspace"
  # Any device listed by: xcrun simctl list devices available
  DESTINATION: "platform=iOS Simulator,name=iPhone 17,OS=latest"
  # Pick the Xcode for every job without sudo (path from: xcodes installed)
  DEVELOPER_DIR: "/Applications/Xcode-27.0.0.app/Contents/Developer"

# Only run on our Mac runner
default:
  tags:
    - macos
    - m4
  before_script:
    # The runner cleans the checkout (git clean -ffdx) before each job, so keep
    # DerivedData outside it: one warm folder per project and concurrent job slot
    - export DERIVED_DATA="${CI_BUILDS_DIR}/DerivedData/${CI_PROJECT_PATH_SLUG}-${CI_CONCURRENT_PROJECT_ID}"
    - xcodebuild -version
    # Pods/ is removed with the rest of the checkout's untracked files: reinstall it
    - |
      if [ -f "Podfile" ]; then
        pod install
      fi

# ---- BUILD ----

build:
  stage: build
  script:
    - |
      xcodebuild build \
        -workspace "${WORKSPACE}" \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}" \
        CODE_SIGNING_ALLOWED=NO \
        | xcbeautify

# ---- TEST ----

unit_tests:
  stage: test
  needs: ["build"]
  script:
    - |
      xcodebuild test \
        -workspace "${WORKSPACE}" \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}" \
        -resultBundlePath "TestResults.xcresult" \
        -parallel-testing-enabled YES \
        | xcbeautify --report junit --report-path build/reports
  artifacts:
    when: always
    paths:
      - TestResults.xcresult/
    reports:
      junit: build/reports/junit.xml
    expire_in: 7 days
  after_script:
    - xcrun simctl shutdown all 2>/dev/null || true

# ---- DEPLOY ----

deploy_testflight:
  stage: deploy
  needs: ["unit_tests"]
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  script:
    - |
      # Install or update Fastlane
      which fastlane || brew install fastlane

      # Run Fastlane beta lane
      fastlane beta
  environment:
    name: testflight
  variables:
    MATCH_PASSWORD: ${MATCH_PASSWORD}
    # Names read by fastlane's app_store_connect_api_key action (call it in your lane)
    APP_STORE_CONNECT_API_KEY_KEY_ID: ${APP_STORE_KEY_ID}
    APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${APP_STORE_ISSUER_ID}
    APP_STORE_CONNECT_API_KEY_KEY: ${APP_STORE_KEY_CONTENT}

GitLabにCI/CD変数を追加する

GitLabプロジェクトで Settings > CI/CD > Variables に移動し、これらの変数を「Masked」および「Protected」として追加します。

  • MATCH_PASSWORD:Fastlane Matchの暗号化用パスワード
  • APP_STORE_KEY_ID:App Store Connect APIキーのID
  • APP_STORE_ISSUER_ID:App Store ConnectのIssuer ID
  • APP_STORE_KEY_CONTENT:.p8キーファイルの内容

パフォーマンスを最適化する

GitLabのキャッシュ構成

永続的な Mac で最も速いキャッシュは、GitLab が移動させる必要のないキャッシュです。上記のパイプラインは DerivedData をチェックアウトの外に置くため、ジョブ間でウォームな状態が保たれます。shell エグゼキューターでは GitLab のキャッシュはジョブごとに圧縮・展開されるため、Pods/ のような小さなフォルダーに使いましょう。

# DerivedData already stays on disk outside the checkout (DERIVED_DATA above).
# Use GitLab's cache for small folders, keyed on the lockfile:
build:
  cache:
    key:
      files:
        - Podfile.lock
    paths:
      - Pods/
    policy: pull-push

# For test jobs that don't modify the cache, use pull-only:
unit_tests:
  cache:
    key:
      files:
        - Podfile.lock
    paths:
      - Pods/
    policy: pull

ステージ間のデータにアーティファクトを使用する

# Useful when the build and test jobs can run on different Macs.
# build-for-testing compiles the app and its test bundles once; artifacts must
# sit inside the project folder, so this job builds there
build:
  script:
    - |
      xcodebuild build-for-testing \
        -workspace "${WORKSPACE}" \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath build/DerivedData \
        | xcbeautify
  artifacts:
    paths:
      - build/DerivedData/Build/Products/
    expire_in: 2 hours

# The test stage receives the built products and runs them without rebuilding
unit_tests:
  needs: ["build"]  # downloads the build job's artifacts
  script:
    - |
      xcodebuild test-without-building \
        -xctestrun build/DerivedData/Build/Products/*.xctestrun \
        -destination "${DESTINATION}" \
        | xcbeautify

テストの並列実行

# On one Mac, let xcodebuild split the test classes across simulator clones.
# (GitLab's parallel: keyword only helps if each copy runs a different part of
# the suite, using CI_NODE_INDEX and CI_NODE_TOTAL; otherwise it repeats it.)
unit_tests:
  stage: test
  script:
    - |
      xcodebuild test \
        -workspace "${WORKSPACE}" \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}" \
        -parallel-testing-enabled YES \
        -maximum-parallel-testing-workers 4 \
        | xcbeautify

キャッシュの定期的なクリーンアップ

# On the Mac Mini, set up a weekly cleanup cron job
crontab -e

# Add these lines:
# Every Sunday at 3 AM, remove DerivedData folders not used for 7 days
# (the pipeline keeps them in ~/builds/DerivedData, one per project and slot)
0 3 * * 0 find ~/builds/DerivedData -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null

# Checkouts in ~/builds are reused by the next job (git fetch): no need to delete them

# Clean Homebrew cache monthly
0 5 1 * * /opt/homebrew/bin/brew cleanup --prune=30 2>/dev/null

トラブルシューティング

GitLabでランナーが「offline」と表示される

ランナーは LaunchAgent として動作するため、ユーザーのログイン時に起動します。再起動後は、そのユーザーがログインするまでオフラインのままです。ヘッドレスの Mac では、このユーザーの自動ログインを有効にしてください(システム設定 > ユーザとグループ。FileVault がオンの間は利用できません)。そのうえで、ランナーサービスのステータスとログを確認します。

# Check service status
brew services list | grep gitlab-runner

# View logs
cat /opt/homebrew/var/log/gitlab-runner.log

# Restart the service
brew services restart gitlab-runner

# Verify connectivity to GitLab
gitlab-runner verify

ビルド中の「Permission denied」エラー

ランナーがXcodeのデベロッパーディレクトリへのアクセスを必要としている可能性があります。

# Ensure the runner user has Xcode access (run once, in your SSH session)
sudo xcode-select -s /Applications/Xcode-27.0.0.app/Contents/Developer
sudo xcodebuild -license accept

# If using simulators, ensure the user can access them
xcrun simctl list devices

キャッシュが復元されない

キャッシュキーが一貫しており、パスが存在することを確認します。

# Check cache directory permissions
ls -la ~/builds/

# The shell executor stores caches locally by default
# Verify the cache directory in config.toml:
cat ~/.gitlab-runner/config.toml

# Ensure [runners.cache] section has the right settings
# For local caching (most efficient for persistent runners):
# [runners.cache]
#   Type = ""  # empty = local cache

Xcodeのビルドがハングまたはタイムアウトする

これは多くの場合、キーチェーンのアクセス許可ダイアログやシミュレーターの問題が原因です。

# Unlock the keychain before builds
security unlock-keychain -p "YOUR_PASSWORD" ~/Library/Keychains/login.keychain-db

# Kill stuck simulators
xcrun simctl shutdown all
pkill -f "Simulator.app" 2>/dev/null || true

# Set a build timeout in .gitlab-ci.yml
build:
  timeout: 30 minutes

FAQ

macOSでDockerエグゼキューターを使用できますか?
macOS上のDockerは、Linuxコンテナを仮想マシン内で実行するため、macOSのAPI、Xcode、iOSシミュレーターにはアクセスできません。iOSのビルドには、shellエグゼキューターを使用する必要があります。Dockerは、Macランナーと並行して実行するサーバーサイドのSwiftやその他のLinuxベースのタスクには適しています。
GitLabグループ向けにランナーを登録するにはどうすればよいですか?
GitLabグループの Settings > CI/CD > Runners > New group runner に移動します。プロジェクトトークンの代わりにグループランナートークンを使用します。これにより、グループ内のすべてのプロジェクトでランナーが利用可能になります。
shellエグゼキューターとSSHエグゼキューターのどちらを使うべきですか?
shellエグゼキューターを使用してください。コマンドをMac上で直接実行するため、Xcode、シミュレーター、キーチェーンにフルアクセスできます。SSHエグゼキューターはリモートマシン向けであり、ランナーがすでにMac上にある場合は不要です。
同じMacでGitLabとGitHub Actionsの両方のランナーを実行できますか?
はい。どちらのランナーも軽量で、同じMac Mini M4上で共存できます。各ランナーの並行実行数を設定する際は、リソース使用量の合計を考慮するようにしてください。
GitLab Runnerを更新するにはどうすればよいですか?
# Update via Homebrew
brew upgrade gitlab-runner

# Restart the service
brew services restart gitlab-runner

# Verify the new version
gitlab-runner --version
フルマネージドのGitLab Runnerをお探しですか?Cloud-Runnerをお試しください セットアップは一切不要です。Cloud-Runnerは、Macハードウェア上に事前構成済みの専用GitLab Runnerを提供します、インストールもメンテナンスも必要ありません。

GitLabパイプラインを強化する準備はできましたか?

GitLab CI/CDランナー向けに専用のMac Mini M4を導入しましょう。無制限のビルドが月額$85から利用できます。

My Remote Mac の料金は USD・税抜です。

さらに詳しい情報をお探しですか?

ステップバイステップのセットアップ手順、設定リファレンス、トラブルシューティングについては、完全なドキュメントをご覧ください。

ドキュメントを開く →