Langsung ke konten

GitLab CI/CD Runner di Mac Mini M4: Panduan Lengkap

Instal dan konfigurasi runner GitLab CI/CD pada Mac Mini M4 khusus. Bangun aplikasi iOS secara native di Apple Silicon, jalankan pengujian pada simulator sungguhan, dan deploy ke TestFlight, semuanya dari pipeline GitLab Anda.

CI/CD Baca 30 menit Diperbarui Oktober 2026

Mengapa GitLab Runner Self-Hosted di Mac?

GitLab menyediakan shared runner di Linux, tetapi membangun aplikasi iOS memerlukan macOS yang berjalan di perangkat keras Apple. Shared runner macOS milik GitLab sendiri terbatas dan mahal. Runner Mac Mini M4 self-hosted memberi Anda:

  • Menit CI/CD Tanpa Batas

    Paket gratis GitLab mencakup 400 menit CI/CD pada shared runner. Pada self-hosted, tidak ada batas.

  • Apple Silicon Native

    Bangun pada chip M4 yang sama dengan yang dijalankan perangkat pengguna Anda. Tanpa overhead terjemahan Rosetta.

  • Kontrol Penuh atas Lingkungan

    Instal versi Xcode, simulator, alat, dan dependensi apa pun yang Anda butuhkan.

  • Cache Persisten

    Cache DerivedData, paket SPM, dan CocoaPods tetap ada di antara setiap eksekusi pipeline.

Instal GitLab Runner

SSH ke server build Mac mini M4 khusus Anda dan instal GitLab Runner, beserta xcbeautify untuk log build yang mudah dibaca, menggunakan Homebrew:

# 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

Instal sebagai Layanan 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

Daftarkan Runner

Buka proyek (atau grup) GitLab Anda dan navigasikan ke Settings > CI/CD > Runners > New project runner. Di Tags, masukkan macos dan m4 (pipeline di bawah menargetkan tag ini), buat runner, lalu salin token autentikasinya. Dengan token autentikasi, tag dan opsi runner lainnya diatur di sini, di GitLab, bukan di perintah register.

Daftarkan dengan Alur Registrasi Runner Baru (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

Daftarkan dengan Token Registrasi Lama (GitLab 15 dan sebelumnya)

# 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"

Verifikasi Konfigurasi Runner

# 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

Edit ~/.gitlab-runner/config.toml untuk menyesuaikan konkurensi:

# 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

Runner sekarang seharusnya muncul sebagai Online di proyek GitLab Anda pada Settings > CI/CD > Runners.

Buat .gitlab-ci.yml untuk iOS

Buat file .gitlab-ci.yml di root repositori Anda. Pipeline lengkap ini membangun, menguji, dan men-deploy aplikasi iOS Anda:

# .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}

Tambahkan Variabel CI/CD di GitLab

Navigasikan ke Settings > CI/CD > Variables di proyek GitLab Anda dan tambahkan variabel-variabel ini sebagai "Masked" dan "Protected":

  • MATCH_PASSWORD: Kata sandi untuk enkripsi Fastlane Match
  • APP_STORE_KEY_ID: ID Kunci API App Store Connect
  • APP_STORE_ISSUER_ID: Issuer ID App Store Connect
  • APP_STORE_KEY_CONTENT: Isi file kunci .p8

Optimalkan Performa

Konfigurasi Cache GitLab

Di Mac yang persisten, cache tercepat adalah yang tidak perlu dipindahkan GitLab: pipeline di atas menyimpan DerivedData di luar checkout, sehingga tetap hangat di antara job. Dengan shell executor, cache GitLab sendiri dikompres dan diekstrak di setiap job, jadi gunakan untuk folder kecil seperti 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

Gunakan Artifact untuk Data Antar-Stage

# 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

Eksekusi Pengujian Paralel

# 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

Pembersihan Cache Terjadwal

# 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

Pemecahan Masalah

Runner menampilkan "offline" di GitLab

Runner berjalan sebagai LaunchAgent, jadi mulai saat sesi pengguna Anda dimulai: setelah reboot, runner tetap offline sampai pengguna itu login. Di Mac headless, aktifkan Automatic login untuk pengguna ini (System Settings > Users & Groups; tidak tersedia selama FileVault aktif). Lalu periksa status layanan runner dan log-nya:

# 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

Error permission denied saat build

Runner mungkin memerlukan akses ke direktori developer 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

Cache tidak dipulihkan

Pastikan cache key konsisten dan path-nya ada:

# 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

Build Xcode macet atau timeout

Ini sering disebabkan oleh prompt akses keychain atau masalah simulator:

# 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

Bisakah saya menggunakan Docker executor di macOS?
Docker di macOS menjalankan container Linux dalam VM, yang tidak dapat mengakses API macOS, Xcode, atau simulator iOS. Untuk build iOS, Anda harus menggunakan shell executor. Docker cocok untuk Swift sisi server atau tugas berbasis Linux lainnya yang berjalan bersama runner Mac Anda.
Bagaimana cara mendaftarkan runner untuk grup GitLab?
Buka Settings > CI/CD > Runners > New group runner pada grup GitLab Anda. Gunakan token group runner alih-alih token proyek. Ini membuat runner tersedia untuk semua proyek dalam grup tersebut.
Haruskah saya menggunakan shell executor atau SSH executor?
Gunakan shell executor. Ia menjalankan perintah langsung di Mac, yang memberikan akses penuh ke Xcode, simulator, dan keychain. SSH executor ditujukan untuk mesin jarak jauh, yang tidak diperlukan ketika runner sudah berada di Mac.
Bisakah saya menjalankan runner GitLab dan GitHub Actions di Mac yang sama?
Ya. Kedua runner ringan dan dapat berdampingan di Mac Mini M4 yang sama. Cukup pastikan untuk memperhitungkan penggunaan sumber daya gabungan saat menetapkan tingkat konkurensi untuk masing-masing runner.
Bagaimana cara memperbarui GitLab Runner?
# Update via Homebrew
brew upgrade gitlab-runner

# Restart the service
brew services restart gitlab-runner

# Verify the new version
gitlab-runner --version
Mencari GitLab Runner yang dikelola sepenuhnya? Coba Cloud-Runner Lewati proses pengaturan sepenuhnya. Cloud-Runner menyediakan GitLab Runner khusus yang telah dikonfigurasi sebelumnya pada perangkat keras Mac, tanpa instalasi atau pemeliharaan.

Siap Memberdayakan Pipeline GitLab Anda?

Dapatkan Mac Mini M4 khusus untuk runner GitLab CI/CD Anda. Build tanpa batas mulai dari $85/bulan.

Harga My Remote Mac dalam USD, belum termasuk pajak.

Butuh detail lebih lanjut?

Jelajahi dokumentasi lengkap untuk panduan langkah demi langkah, referensi konfigurasi, dan pemecahan masalah.

Buka dokumentasi →