Pourquoi utiliser un runner Mac auto-hébergé ?
Les runners macOS hébergés par GitHub sont pratiques, mais facturés à la minute : le coût augmente avec votre volume de builds. À 0,062 $ par minute (tarif du runner macOS standard de GitHub en octobre 2026), une équipe effectuant 75 heures de builds par mois paie environ 279 $. GitHub ne facture pas les minutes des runners auto-hébergés : un Mac mini M4 dédié de My Remote Mac coûte un forfait de 85 $/mois (USD, HT), sans facturation à la minute, et vous offre le même matériel (voire meilleur) pour une fraction du coût.
| Caractéristique | My Remote Mac auto-hébergé | Runner hébergé par GitHub |
|---|---|---|
| Coût | 85 $/mois (sans facturation à la minute) | 0,062 $/min (~279 $/mois pour 75h) |
| Architecture | Apple M4, Mac physique attribué 1:1 | VM Apple M1, 3 cœurs, 7 Go de RAM (runner standard) |
| Vitesse de build | ~4 min (même projet) | ~12 min (projet moyen) |
| Cache persistant | Oui (disque persistant) | Non (éphémère) |
| Logiciels personnalisés | Accès root complet | Limité |
| Jobs simultanés | 1 job par runner ; ajoutez des runners ou des Mac pour monter en charge | 5 (Free, Pro, Team) / 50 (Enterprise) |
Prix My Remote Mac en USD, hors taxes. Chiffres GitHub : runner macOS standard à 0,062 $ par minute, d'après la documentation tarifaire de GitHub, en octobre 2026.
Avantage clé : Comme le runner est persistant, DerivedData, les caches SPM et CocoaPods sont préservés entre les builds. Cela seul peut réduire les temps de build de 50 à 70 % par rapport aux runners GitHub hébergés éphémères qui repartent de zéro à chaque fois.
Prérequis
Avant de commencer, assurez-vous de disposer des éléments suivants :
- Un serveur de build Mac mini M4 dédié chez My Remote Mac (à partir de 85 $/mois, USD, HT)
- Un compte GitHub avec un accès admin à votre dépôt ou organisation
- Un accès SSH à votre Mac Mini (fourni avec votre abonnement My Remote Mac)
- Un compte Apple Developer (pour la signature de code et les profils de provisionnement)
- Une connaissance de base de YAML et des commandes terminal
Étape 1 : Se connecter en SSH à votre Mac Mini et installer Xcode
Commencez par vous connecter à votre Mac Mini M4 via SSH. Vous aurez reçu vos identifiants lors de la mise en place de votre serveur My Remote Mac.
Connexion via SSH
# Connect to your Mac Mini M4
ssh admin@your-server-ip
# Verify you're on Apple Silicon
uname -m
# Expected output: arm64
# Check the macOS version
sw_vers
Installer les outils en ligne de commande Xcode
# Install Command Line Tools
xcode-select --install
# Accept the license agreement
sudo xcodebuild -license accept
# Verify installation
xcode-select -p
# /Library/Developer/CommandLineTools
Installer Xcode (version complète)
Pour les builds iOS, vous avez besoin de l'application Xcode complète. Le moyen le plus rapide de l'installer sur un serveur sans écran est d'utiliser xcodes :
# Install Homebrew (if not already installed)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add Homebrew to PATH
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
# Install xcodes CLI tool
brew install xcodes
# List available Xcode versions
xcodes list
# Install the latest release of Xcode (asks for your Apple Account)
xcodes install --latest
# Show the installed versions and their paths
xcodes installed
# e.g. 27.0 (27A266a) /Applications/Xcode-27.0.0.app
# Set it as the active Xcode (interactive SSH session: sudo asks for your password)
sudo xcode-select -s /Applications/Xcode-27.0.0.app/Contents/Developer
# Verify
xcodebuild -version
# Xcode 27.0
# Build version 27A266a
Installer les simulateurs iOS
# Install the iOS simulator runtime that matches your Xcode
xcodebuild -downloadPlatform iOS
# Verify simulator availability
xcrun simctl list runtimes
xcrun simctl list devices available
Étape 2 : Installer le runner GitHub Actions
Téléchargeons et configurons maintenant l'agent runner GitHub Actions. Accédez à votre dépôt sur GitHub, allez dans Settings > Actions > Runners > New self-hosted runner, et sélectionnez macOS + ARM64.
Télécharger et configurer
# Create a directory for the runner
mkdir -p ~/actions-runner && cd ~/actions-runner
# Download the latest runner package (macOS ARM64). The same commands, with the
# current version and its checksum, are shown on Settings > Actions > Runners >
# New self-hosted runner
RUNNER_VERSION=$(curl -fsSL https://api.github.com/repos/actions/runner/releases/latest \
| sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
curl -o "actions-runner-osx-arm64-${RUNNER_VERSION}.tar.gz" -L \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-osx-arm64-${RUNNER_VERSION}.tar.gz"
# Extract the package
tar xzf "actions-runner-osx-arm64-${RUNNER_VERSION}.tar.gz"
# Configure the runner
# Replace YOUR_TOKEN with the token from GitHub Settings
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \
--token YOUR_TOKEN \
--name "mac-mini-m4-runner" \
--labels "self-hosted,macOS,ARM64,M4" \
--work "_work"
# Test the runner interactively first
./run.sh
Installer en tant que service launchd persistant
Exécuter l'agent de manière interactive convient pour les tests, mais pour la production, il faut qu'il démarre automatiquement et redémarre en cas de plantage. GitHub fournit un script d'installation de service intégré pour macOS :
# Install as a launchd service (a LaunchAgent; no sudo on macOS)
cd ~/actions-runner
./svc.sh install
# Start the service
./svc.sh start
# Check the service status
./svc.sh status
# View the launchd plist (for reference)
cat ~/Library/LaunchAgents/actions.runner.*.plist
Le service démarre désormais à chaque ouverture de session de l'utilisateur du runner et relance le runner si le processus s'arrête. Un LaunchAgent ne démarre pas tout seul au démarrage du Mac : sur un Mac sans écran, activez l'ouverture de session automatique pour cet utilisateur (Réglages Système > Utilisateurs et groupes ; indisponible tant que FileVault est activé) afin que le runner revienne en ligne après un redémarrage. Vous pouvez vérifier que le runner apparaît comme "Idle" dans la page Settings > Actions > Runners de votre dépôt GitHub.
Configurer le runner pour plusieurs dépôts (niveau organisation)
# For an organization-level runner, use the organization URL:
./config.sh --url https://github.com/YOUR_ORG \
--token YOUR_ORG_TOKEN \
--name "mac-mini-m4-org-runner" \
--labels "self-hosted,macOS,ARM64,M4" \
--runnergroup "Default" \
--work "_work"
# This allows ALL repositories in your organization to use this runner
Étape 3 : Créer votre workflow de build iOS
Créez un fichier workflow dans votre dépôt à l'emplacement .github/workflows/ios-build.yml. Ce workflow s'exécutera sur votre runner Mac Mini M4 auto-hébergé.
name: iOS Build & Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: [self-hosted, macOS, ARM64, M4]
# bash -eo pipefail: a failing xcodebuild fails the step even when piped to xcbeautify
defaults:
run:
shell: bash
env:
SCHEME: "MyApp"
# Any device listed by: xcrun simctl list devices available
DESTINATION: "platform=iOS Simulator,name=iPhone 17,OS=latest"
# Pick the Xcode for this job without sudo (path from: xcodes installed)
DEVELOPER_DIR: /Applications/Xcode-27.0.0.app/Contents/Developer
# Caches sit next to the checkout: actions/checkout cleans the checkout
# (git clean -ffdx) on every run, but not its parent folder
DERIVED_DATA: ${{ github.workspace }}/../DerivedData
SPM_CACHE: ${{ github.workspace }}/../SourcePackages
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Show the selected Xcode
run: xcodebuild -version
- name: Resolve Swift Package Dependencies
run: |
xcodebuild -resolvePackageDependencies \
-scheme "$SCHEME" \
-clonedSourcePackagesDirPath "$SPM_CACHE"
- name: Install xcbeautify
run: command -v xcbeautify >/dev/null || brew install xcbeautify
- name: Build the app
run: |
xcodebuild build \
-scheme "$SCHEME" \
-destination "$DESTINATION" \
-clonedSourcePackagesDirPath "$SPM_CACHE" \
-derivedDataPath "$DERIVED_DATA" \
| xcbeautify
- name: Run unit tests
run: |
xcodebuild test \
-scheme "$SCHEME" \
-destination "$DESTINATION" \
-clonedSourcePackagesDirPath "$SPM_CACHE" \
-derivedDataPath "$DERIVED_DATA" \
-resultBundlePath TestResults.xcresult \
| xcbeautify
- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
with:
name: test-results
path: TestResults.xcresult
Ajouter la signature de code pour les builds de release
Pour les déploiements TestFlight ou App Store, ajoutez des étapes de signature de code. Stockez vos certificats et profils de provisionnement en tant que GitHub Secrets :
deploy:
needs: build
runs-on: [self-hosted, macOS, ARM64, M4]
if: github.ref == 'refs/heads/main'
env:
# Same Xcode as the build job (App Store Connect requires Xcode 26 or later)
DEVELOPER_DIR: /Applications/Xcode-27.0.0.app/Contents/Developer
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Install certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# Create a temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import certificate
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# Install provisioning profile (the folder Xcode 16 and later read)
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
mkdir -p ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles
cp $PP_PATH ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles
- name: Build for distribution
run: |
xcodebuild archive \
-scheme "MyApp" \
-archivePath DerivedData/MyApp.xcarchive \
-destination "generic/platform=iOS" \
CODE_SIGN_STYLE=Manual
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath DerivedData/MyApp.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath DerivedData/Export
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
run: |
# altool reads the key from ~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8
xcrun altool --upload-app \
-f DerivedData/Export/MyApp.ipa \
-t ios \
--apiKey $APP_STORE_CONNECT_API_KEY \
--apiIssuer $APP_STORE_CONNECT_ISSUER_ID
# The runner is persistent: remove the signing files at the end of every job
- name: Clean up keychain and provisioning profile
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
rm -f ~/Library/Developer/Xcode/UserData/Provisioning\ Profiles/build_pp.mobileprovision
Étape 4 : Optimiser les performances
L'un des plus grands avantages d'un runner auto-hébergé est le cache persistant. Voici les optimisations clés pour tirer le meilleur parti de votre Mac Mini M4.
Activer le cache DerivedData
Puisque le runner est persistant, DerivedData est préservé entre les builds. Utilisez un chemin DerivedData cohérent :
# In your workflow, keep DerivedData next to the checkout (job env, as above):
# DERIVED_DATA: ${{ github.workspace }}/../DerivedData
# ... -derivedDataPath "$DERIVED_DATA"
# Each repository then gets ~/actions-runner/_work/<repo>/DerivedData
# On the runner, remove the DerivedData of repositories not built for 7 days
# (cron job every Sunday at 03:00, keeps your existing crontab entries)
(crontab -l 2>/dev/null; echo '0 3 * * 0 find "$HOME"/actions-runner/_work/*/DerivedData -maxdepth 0 -type d -mtime +7 -exec rm -rf {} +') \
| crontab -
Mettre en cache les paquets SPM
# Use clonedSourcePackagesDirPath to keep SPM packages on disk
# (SPM_CACHE points outside the checkout, as in the workflow above)
xcodebuild build \
-scheme "MyApp" \
-clonedSourcePackagesDirPath "$SPM_CACHE" \
-derivedDataPath "$DERIVED_DATA"
# This avoids re-downloading packages on every build
Exécution parallèle des tests
# Run tests in parallel across multiple simulators
# (devices from: xcrun simctl list devices available)
set -o pipefail
xcodebuild test \
-scheme "MyApp" \
-destination "platform=iOS Simulator,name=iPhone 17,OS=latest" \
-destination "platform=iOS Simulator,name=iPhone 17 Pro,OS=latest" \
-parallel-testing-enabled YES \
-maximum-parallel-testing-workers 4 \
-derivedDataPath "$DERIVED_DATA" \
| xcbeautify
Installer xcbeautify pour de meilleurs logs
# xcbeautify formats Xcode output for CI environments
brew install xcbeautify
# Use it by piping xcodebuild output (pipefail keeps xcodebuild's exit status):
set -o pipefail && xcodebuild build -scheme "MyApp" | xcbeautify
Résolution des problèmes courants
Le runner apparaît "Hors ligne" dans GitHub
Cela signifie généralement que le service launchd n'est pas en cours d'exécution. Vérifiez l'état du service et les journaux :
# Check service status
./svc.sh status
# View logs
cat ~/actions-runner/_diag/Runner_*.log | tail -50
# Restart the service
./svc.sh stop
./svc.sh start
La signature de code échoue avec "No signing certificate"
Le service launchd s'exécute sous un contexte utilisateur différent. Assurez-vous que le trousseau est accessible :
# Ensure the login keychain is unlocked for the runner user
security unlock-keychain -p "YOUR_PASSWORD" ~/Library/Keychains/login.keychain-db
# Or use a dedicated keychain in your workflow (recommended)
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
Le simulateur ne démarre pas
Les simulateurs restent parfois bloqués. Réinitialisez-les entre les builds :
# Shutdown all running simulators
xcrun simctl shutdown all
# Erase all simulator data (nuclear option)
xcrun simctl erase all
# Boot a specific simulator
xcrun simctl boot "iPhone 17"
Espace disque insuffisant
Les builds Xcode génèrent beaucoup de données. Configurez un nettoyage automatique :
# Clean old DerivedData
rm -rf ~/Library/Developer/Xcode/DerivedData/*
# Remove simulators whose runtime is no longer installed
xcrun simctl delete unavailable
# Clean Homebrew cache
brew cleanup --prune=7
# Remove old Xcode archives
rm -rf ~/Library/Developer/Xcode/Archives/*
Analyse des coûts
Voici une comparaison détaillée des coûts pour différentes tailles d'équipe et volumes de builds :
| Taille de l'équipe | Coût My Remote Mac | Builds/mois | Coût runner GitHub hébergé | Économies mensuelles |
|---|---|---|---|---|
| Développeur solo | 85 $/mois | 100 builds (10 min moy.) | 62 $/mois | Aucune (coûte 23 $/mois de plus) |
| Petite équipe (5) | 85 $/mois | 500 builds (10 min moy.) | 310 $/mois | 225 $/mois |
| Équipe moyenne (15) | 229 $/mois (M4 Pro) | 1500 builds (10 min moy.) | 930 $/mois | 701 $/mois |
| Entreprise (50+) | 458 $/mois (2x M4 Pro) | 5000+ builds | 3 100+ $/mois | 2 642+ $/mois |
Prix My Remote Mac en USD, hors taxes. Chiffres GitHub : runner macOS standard à 0,062 $ par minute, d'après la documentation tarifaire de GitHub, en octobre 2026.
En résumé : Pour les équipes effectuant plus de ~140 builds par mois, un Mac Mini M4 auto-hébergé est immédiatement rentable. Et comme les builds sont plus rapides sur du matériel persistant avec des caches préchauffés, votre équipe gagne aussi du temps de développement. À 100 builds par mois, comme dans la ligne Développeur solo, les runners hébergés par GitHub coûtent 23 $/mois de moins ; un développeur solo peut tout de même préférer un Mac dédié pour ses caches persistants, ses builds plus rapides et l'absence de facturation à la minute. Si vous ne publiez qu'occasionnellement, l'offre à la journée vous donne un Mac pour le jour de la publication dès 14 $ par 24 heures (USD, hors taxes) : il s'arrête au bout de 24 heures sauf si vous activez le renouvellement automatique, et vous pouvez passer au mensuel sur le même Mac.