1. Why Fastlane on a Dedicated Mac?
Fastlane is the standard automation tool for iOS and Android development. It handles everything from code signing to TestFlight deployment. Running Fastlane on a dedicated Mac Mini M4 gives you:
Code signing certificates persist between runs. No need to import/export on every build.
DerivedData persists, so fastlane build takes 2-4 minutes instead of 15+.
Run fastlane snapshot with real simulators for all device sizes.
A stable, dedicated environment means fewer flaky deployments and code signing issues.
2. Install Fastlane
SSH into your Mac Mini M4 and install Fastlane. We recommend Homebrew for the simplest setup:
Option A: Install via Homebrew (Recommended)
# 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 Fastlane
brew install fastlane
# Verify installation
fastlane --version
# fastlane 2.225.0
Option B: Install via RubyGems
# Use the system Ruby or install rbenv for version management
gem install fastlane -NV
# Or with Bundler (recommended for team consistency):
# Create a Gemfile in your project root
cat > Gemfile <<'EOF'
source "https://rubygems.org"
gem "fastlane"
gem "cocoapods" # if using CocoaPods
EOF
bundle install
Initialize Fastlane in Your Project
# Navigate to your project directory
cd /path/to/your/ios-project
# Initialize Fastlane
fastlane init
# Choose option 4: "Manual setup"
# This creates the fastlane/ directory with Appfile and Fastfile
3. Configure Match for Code Signing
Fastlane Match stores your code signing certificates and provisioning profiles in a private Git repository or cloud storage. This ensures all machines (and team members) use the same signing identity.
Initialize Match
# Initialize match (choose "git" for storage)
fastlane match init
# This creates fastlane/Matchfile
Configure Matchfile
# fastlane/Matchfile
git_url("https://github.com/your-org/ios-certificates.git")
storage_mode("git")
type("appstore") # default type, can be overridden per lane
app_identifier(["com.yourcompany.myapp"])
username("your-apple-id@example.com")
# For CI environments, use App Store Connect API key instead of username/password
# api_key_path("fastlane/AuthKey.json")
Generate Certificates
# Generate development certificates and profiles
fastlane match development
# Generate App Store distribution certificates and profiles
fastlane match appstore
# For ad-hoc distribution
fastlane match adhoc
# On CI, use readonly mode to avoid accidentally creating new certs
fastlane match appstore --readonly
4. Create Fastfile (Build, Test, Deploy Lanes)
Here is a complete Fastfile with lanes for building, testing, and deploying your iOS app:
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
# ---- SHARED ----
before_all do
setup_ci if ENV['CI'] # Configures keychain for CI environments
end
# ---- BUILD ----
desc "Build the app for testing"
lane :build do
match(type: "development", readonly: true)
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
configuration: "Debug",
destination: "generic/platform=iOS Simulator",
derived_data_path: "DerivedData",
skip_archive: true,
skip_codesigning: true
)
end
# ---- TEST ----
desc "Run all unit and UI tests"
lane :test do
run_tests(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
devices: ["iPhone 16 Pro"],
derived_data_path: "DerivedData",
result_bundle: true,
output_directory: "fastlane/test_results",
parallel_testing: true,
concurrent_workers: 4
)
end
# ---- BETA ----
desc "Build and push a new beta to TestFlight"
lane :beta do
# Ensure we are on a clean git state
ensure_git_status_clean
# Fetch App Store certificates
match(type: "appstore", readonly: true)
# Increment build number
increment_build_number(
build_number: latest_testflight_build_number + 1
)
# Build the app
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
export_method: "app-store",
derived_data_path: "DerivedData",
output_directory: "fastlane/builds"
)
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
api_key_path: "fastlane/AuthKey.json"
)
# Commit the version bump
commit_version_bump(
message: "chore: bump build number [skip ci]",
force: true
)
# Tag the release
add_git_tag(
tag: "beta/#{lane_context[SharedValues::BUILD_NUMBER]}"
)
push_to_git_remote
end
# ---- RELEASE ----
desc "Build and submit to App Store Review"
lane :release do
match(type: "appstore", readonly: true)
# Increment version number (patch)
increment_version_number(bump_type: "patch")
increment_build_number(
build_number: latest_testflight_build_number + 1
)
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
export_method: "app-store",
derived_data_path: "DerivedData"
)
upload_to_app_store(
submit_for_review: true,
automatic_release: false,
api_key_path: "fastlane/AuthKey.json",
precheck_include_in_app_purchases: false
)
commit_version_bump(message: "chore: release #{lane_context[SharedValues::VERSION_NUMBER]}")
add_git_tag
push_to_git_remote
end
# ---- ERROR HANDLING ----
error do |lane, exception|
# Send notification on failure (Slack, email, etc.)
# slack(
# message: "Lane #{lane} failed: #{exception.message}",
# success: false
# )
end
end
5. Set Up Automated Screenshots
Fastlane Snapshot captures App Store screenshots across multiple devices and languages automatically. On a dedicated Mac, this runs reliably without competing for resources.
# Initialize snapshot
fastlane snapshot init
# This creates:
# - fastlane/Snapfile
# - fastlane/SnapshotHelper.swift (add to UI test target)
Configure Snapfile
# fastlane/Snapfile
devices([
"iPhone 16 Pro Max",
"iPhone 16 Pro",
"iPhone SE (3rd generation)",
"iPad Pro 13-inch (M4)"
])
languages([
"en-US",
"fr-FR",
"de-DE",
"ja"
])
scheme("MyAppUITests")
output_directory("./fastlane/screenshots")
clear_previous_screenshots(true)
# Speed up by running in parallel
concurrent_simulators(true)
Add Snapshot to Your UI Tests
// In your XCUITest file:
import XCTest
class ScreenshotTests: XCTestCase {
override func setUp() {
continueAfterFailure = false
let app = XCUIApplication()
setupSnapshot(app)
app.launch()
}
func testHomeScreen() {
snapshot("01_HomeScreen")
}
func testDetailScreen() {
let app = XCUIApplication()
app.cells.firstMatch.tap()
snapshot("02_DetailScreen")
}
func testSettings() {
let app = XCUIApplication()
app.tabBars.buttons["Settings"].tap()
snapshot("03_Settings")
}
}
Add a Screenshot Lane
# Add to your Fastfile:
desc "Capture App Store screenshots"
lane :screenshots do
capture_screenshots
frame_screenshots(white: true) # Add device frames
upload_to_app_store(
skip_binary_upload: true,
skip_metadata: true,
api_key_path: "fastlane/AuthKey.json"
)
end
6. Deploy to TestFlight
For CI environments, use the App Store Connect API key instead of your Apple ID credentials. This avoids 2FA prompts on headless servers.
Create an API Key
- Go to App Store Connect > Users and Access > Keys
- Click the + button to generate a new API key
- Select App Manager role
- Download the
.p8file - Note the Key ID and Issuer ID
Create the API Key JSON
# fastlane/AuthKey.json
{
"key_id": "YOUR_KEY_ID",
"issuer_id": "YOUR_ISSUER_ID",
"key": "-----BEGIN PRIVATE KEY-----\nYOUR_P8_KEY_CONTENT\n-----END PRIVATE KEY-----",
"in_house": false
}
# IMPORTANT: Add this to .gitignore!
echo "fastlane/AuthKey.json" >> .gitignore
Run the Deployment
# Deploy to TestFlight
fastlane beta
# Or run the full release pipeline
fastlane release
7. Integrate with CI/CD
Fastlane integrates seamlessly with any CI/CD platform. Here is a GitHub Actions example using your self-hosted Mac Mini M4 runner:
# .github/workflows/deploy.yml
name: Deploy to TestFlight
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: [self-hosted, macOS, ARM64, M4]
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up App Store Connect API Key
run: |
mkdir -p fastlane
echo '${{ secrets.APP_STORE_CONNECT_API_KEY }}' > fastlane/AuthKey.json
- name: Install dependencies
run: |
bundle install
pod install # if using CocoaPods
- name: Deploy to TestFlight
run: bundle exec fastlane beta
- name: Clean up API key
if: always()
run: rm -f fastlane/AuthKey.json
8. Best Practices
Add a Gemfile with a pinned Fastlane version. Run bundle exec fastlane to ensure consistent versions across all environments.
API keys avoid 2FA prompts and are more secure for CI environments.
match --readonly in CI
Prevents accidental creation of new certificates. Only generate certs manually when needed.
setup_ci in CI environments
This creates a temporary keychain to avoid polluting the system keychain and prevents keychain permission dialogs.
On a dedicated Mac, specify derived_data_path to reuse build artifacts between runs.