Guida - CI/CD

GitLab CI/CD Runner su Mac Mini M4: guida completa

Installa e configura un GitLab CI/CD runner su un Mac Mini M4 dedicato. Compila app iOS nativamente su Apple Silicon, esegui test su simulatori reali e distribuisci su TestFlight -- tutto dalla tua pipeline GitLab.

30 min di lettura Aggiornato a marzo 2026

1. Perché GitLab Runner self-hosted su Mac?

GitLab offre runner condivisi su Linux, ma compilare app iOS richiede macOS in esecuzione su hardware Apple. I runner macOS condivisi di GitLab sono limitati e costosi. Un Mac Mini M4 runner self-hosted ti offre:

Minuti CI/CD illimitati

Il piano gratuito di GitLab include 400 minuti CI/CD sui runner condivisi. Su self-hosted non c'è alcun limite.

Apple Silicon nativo

Compila sullo stesso chip M4 su cui girano i dispositivi dei tuoi utenti. Nessun overhead di traduzione Rosetta.

Controllo completo dell'ambiente

Installa qualsiasi versione di Xcode, simulatori, strumenti e dipendenze di cui hai bisogno.

Cache persistenti

Le cache di DerivedData, pacchetti SPM e CocoaPods sopravvivono tra un'esecuzione della pipeline e l'altra.

2. Installa GitLab Runner

Connettiti al tuo Mac Mini M4 via SSH e installa GitLab Runner usando 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
brew install gitlab-runner

# Verify installation
gitlab-runner --version
# Version:      17.7.0
# Git revision:  ...
# Git branch:    17-7-stable
# GO version:    go1.22.10
# Built:         ...
# OS/Arch:       darwin/arm64

Installa come servizio macOS

# Install the runner as a launchd service
# This ensures it starts automatically on boot
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

# Check runner status
gitlab-runner status
# gitlab-runner: Service is running

3. Registra il Runner

Vai al tuo progetto GitLab (o gruppo) e apri Settings > CI/CD > Runners > New project runner. Copia il token di registrazione.

Registrazione con il nuovo flusso di registrazione del Runner (GitLab 16+)

# Register the runner using the authentication token from GitLab UI
# (GitLab 16+ uses authentication tokens instead of registration tokens)
gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --token "YOUR_RUNNER_AUTHENTICATION_TOKEN" \
  --executor "shell" \
  --description "mac-mini-m4-runner" \
  --tag-list "macos,apple-silicon,m4,ios,xcode"

Registrazione con token di registrazione legacy (GitLab 15 e precedenti)

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

Verifica la configurazione del 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

Modifica ~/.gitlab-runner/config.toml per regolare la concorrenza:

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

# Set concurrent to match your hardware capacity
concurrent = 2

# Restart the runner to apply changes
gitlab-runner restart

Il runner ora dovrebbe apparire come Online nel tuo progetto GitLab sotto Settings > CI/CD > Runners.

4. Crea .gitlab-ci.yml per iOS

Crea un file .gitlab-ci.yml nella radice del tuo repository. Questa pipeline completa compila, testa e distribuisce la tua app iOS:

# .gitlab-ci.yml

stages:
  - setup
  - build
  - test
  - deploy

variables:
  SCHEME: "MyApp"
  WORKSPACE: "MyApp.xcworkspace"
  DESTINATION: "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2"
  DERIVED_DATA: "${CI_PROJECT_DIR}/DerivedData"

# Only run on our Mac runner
default:
  tags:
    - macos
    - m4

# ---- SETUP ----

setup:
  stage: setup
  script:
    - sudo xcode-select -s /Applications/Xcode-16.2.app/Contents/Developer
    - xcodebuild -version
    - swift --version
    # Install CocoaPods if using Podfile
    - |
      if [ -f "Podfile" ]; then
        pod install --repo-update
      fi
  cache:
    key: pods-${CI_COMMIT_REF_SLUG}
    paths:
      - Pods/
      - .spm-cache/

# ---- BUILD ----

build:
  stage: build
  needs: ["setup"]
  script:
    - |
      xcodebuild build \
        -workspace "${WORKSPACE}" \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}" \
        -clonedSourcePackagesDirPath ".spm-cache" \
        CODE_SIGNING_ALLOWED=NO \
        | xcbeautify
  cache:
    key: derived-data-${CI_COMMIT_REF_SLUG}
    paths:
      - DerivedData/
      - .spm-cache/
  artifacts:
    paths:
      - DerivedData/
    expire_in: 1 hour

# ---- 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
  artifacts:
    when: always
    paths:
      - TestResults.xcresult/
    reports:
      junit: TestResults.xcresult/report.junit
    expire_in: 7 days
  after_script:
    - xcrun simctl shutdown all 2>/dev/null || true

# ---- DEPLOY ----

deploy_testflight:
  stage: deploy
  needs: ["unit_tests"]
  only:
    - 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}
    APP_STORE_CONNECT_API_KEY_ID: ${APP_STORE_KEY_ID}
    APP_STORE_CONNECT_API_ISSUER_ID: ${APP_STORE_ISSUER_ID}
    APP_STORE_CONNECT_API_KEY_CONTENT: ${APP_STORE_KEY_CONTENT}

Aggiungi variabili CI/CD in GitLab

Vai su Settings > CI/CD > Variables nel tuo progetto GitLab e aggiungi queste variabili come "Masked" e "Protected":

  • MATCH_PASSWORD - Password per la crittografia di Fastlane Match
  • APP_STORE_KEY_ID - App Store Connect API Key ID
  • APP_STORE_ISSUER_ID - App Store Connect Issuer ID
  • APP_STORE_KEY_CONTENT - Il contenuto del file di chiave .p8

5. Ottimizza le prestazioni

Configurazione della cache di GitLab

GitLab Runner supporta il caching locale per gli executor shell. Poiché il tuo runner è persistente, le cache locali sono estremamente efficienti:

# In .gitlab-ci.yml, configure cache per branch:
cache:
  key: "${CI_COMMIT_REF_SLUG}"
  paths:
    - DerivedData/
    - .spm-cache/
    - Pods/
  policy: pull-push

# For test jobs that don't modify cache, use pull-only:
unit_tests:
  cache:
    key: "${CI_COMMIT_REF_SLUG}"
    paths:
      - DerivedData/
    policy: pull

Usa gli artefatti per i dati tra fasi

# Pass build artifacts between stages efficiently
build:
  artifacts:
    paths:
      - DerivedData/Build/Products/
    expire_in: 2 hours

# The test stage receives the built products without rebuilding
test:
  needs: ["build"]  # only download artifacts from the build job
  script:
    - xcodebuild test-without-building \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}"

Esecuzione dei test in parallelo

# Split tests across parallel jobs using GitLab's parallel keyword
unit_tests:
  stage: test
  parallel: 2
  script:
    - |
      # Use test plan partitioning or custom splitting
      xcodebuild test \
        -workspace "${WORKSPACE}" \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}" \
        -parallel-testing-enabled YES \
        -maximum-parallel-testing-workers 4

Pulizia programmata della cache

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

# Add these lines:
# Clean DerivedData older than 7 days every Sunday at 3 AM
0 3 * * 0 find ~/builds/*/DerivedData -maxdepth 0 -mtime +7 -exec rm -rf {} + 2>/dev/null

# Clean old GitLab Runner builds older than 14 days
0 4 * * 0 find ~/builds -maxdepth 2 -mtime +14 -type d -exec rm -rf {} + 2>/dev/null

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

6. Risoluzione dei problemi

Il runner appare "offline" in GitLab

Controlla lo stato e i log del servizio runner:

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

# View logs
cat /usr/local/var/log/gitlab-runner.log

# Restart the service
brew services restart gitlab-runner

# Verify connectivity to GitLab
gitlab-runner verify

Errori "Permission denied" durante le build

Il runner potrebbe aver bisogno dell'accesso alla directory dello sviluppatore Xcode:

# Ensure the runner user has Xcode access
sudo xcode-select -s /Applications/Xcode-16.2.app/Contents/Developer
sudo xcodebuild -license accept

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

La cache non viene ripristinata

Assicurati che le chiavi della cache siano coerenti e che i percorsi esistano:

# 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

La build di Xcode si blocca o va in timeout

Questo è spesso causato da richieste di accesso al portachiavi o da problemi con i simulatori:

# 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

7. FAQ

Posso usare un executor Docker su macOS?

Docker su macOS esegue container Linux in una VM, che non può accedere alle API di macOS, a Xcode o ai simulatori iOS. Per le build iOS devi usare l'executor shell. Docker va bene per Swift lato server o altre attività basate su Linux eseguite insieme al tuo runner Mac.

Come registro il runner per un gruppo GitLab?

Vai su Settings > CI/CD > Runners > New group runner del tuo gruppo GitLab. Usa il token del runner di gruppo invece di un token di progetto. In questo modo il runner sarà disponibile per tutti i progetti del gruppo.

Dovrei usare l'executor shell o l'executor SSH?

Usa l'executor shell. Esegue i comandi direttamente sul Mac, garantendo accesso completo a Xcode, ai simulatori e al portachiavi. L'executor SSH è pensato per macchine remote, cosa non necessaria quando il runner è già sul Mac.

Posso eseguire runner GitLab e GitHub Actions sullo stesso Mac?

Sì. Entrambi i runner sono leggeri e possono coesistere sullo stesso Mac Mini M4. Assicurati solo di considerare l'utilizzo combinato delle risorse quando imposti i livelli di concorrenza per ciascun runner.

Come aggiorno GitLab Runner?

# Update via Homebrew
brew upgrade gitlab-runner

# Restart the service
brew services restart gitlab-runner

# Verify the new version
gitlab-runner --version

Cerchi un GitLab Runner completamente gestito? Prova Cloud-Runner

Salta del tutto la configurazione. Cloud-Runner fornisce GitLab Runner dedicati e preconfigurati su hardware Mac — nessuna installazione o manutenzione richiesta.

Guide correlate

Pronto a potenziare le tue pipeline GitLab?

Ottieni un Mac Mini M4 dedicato per il tuo GitLab CI/CD runner. Build illimitate a partire da 75 $/mese.

Cerchi maggiori dettagli?

Consulta la documentazione completa per guide passo passo, riferimenti di configurazione e risoluzione dei problemi.

Apri la documentazione →