From connection to your first build artifact

Your First Hour
with Your Mac

Use the first half hour to connect, check your toolchain and run an initial build. Spend the second half organizing code signing and CI. You have a dedicated physical Mac, not a virtual machine, so you can keep using familiar macOS GUI and command-line workflows.

Already placed an order? Open the console to find your actual host address, port, username and connection details. Don’t use the example variables as login credentials.

00–10 min

Verify the host fingerprint and connect over SSH.

10–20 min

Check Xcode, project dependencies and disk space.

20–30 min

Run unsigned checks first, then verify an archive.

Step 1 · Verify your connection

Use SSH for commands and Screen Sharing for the GUI

Generate a key locally and upload only the public key. Keep the private key on your own device and protect it with a passphrase. Use the actual host, port and username provided in the console—not the example values below.

Run in a local macOS terminal
read -r -p "Host address: " MAC_HOST
read -r -p "SSH port: " MAC_PORT
read -r -p "Username: " MAC_USER
ssh-keygen -t ed25519 -f "$HOME/.ssh/osxhost_build"
cat "$HOME/.ssh/osxhost_build.pub"
ssh -p "$MAC_PORT" "$MAC_USER@$MAC_HOST"

Public key files end in .pub . After connecting with the credentials provided for your first login, append the public key to the remote ~/.ssh/authorized_keysThen open another local terminal and verify that key-based login works.

Add your public key, then change your password

On the remote host, create ~/.sshand set the directory permissions to 700and the public key file permissions to 600. Append the public key in an editor, keeping any existing authorized keys. Then run passwd to change the current user’s password.

Open another terminal and run ssh -i ~/.ssh/osxhost_build -p "$MAC_PORT" "$MAC_USER@$MAC_HOST" Once you’ve confirmed the new connection works, close the original session. Don’t close your only working connection first.

Check: You can sign in with your key in a new session

Use Screen Sharing through an encrypted tunnel

Run the tunnel command below locally and leave the terminal open. On macOS, open Screen Sharing and connect to vnc://127.0.0.1:5901On Windows or Linux, use a client that supports the relevant VNC authentication method and connect to local port 5901.

Screen Sharing must be enabled on the remote Mac, and your user must be authorized. Your GUI login credentials and SSH private key are separate authentication methods. Don’t expose the VNC port to all sources.

Check: You can see and control the macOS desktop
Local SSH tunnel · Target: remote loopback address
ssh -N -L 5901:127.0.0.1:5900 \
  -i "$HOME/.ssh/osxhost_build" \
  -p "$MAC_PORT" "$MAC_USER@$MAC_HOST"

Step 2 · Local Mac → Cloud Mac

Migrate in three stages—don’t clone your entire old setup

Verify your data and toolchain before registering a CI runner. Check “can log in” separately from “can build unattended” so you don’t mistake an interactive local setup for a CI-ready configuration.

Path 01 · Data

Move your project, not plaintext secrets

  • Export the required signing certificate and private key from Keychain as an encrypted .p12file. Share its password through a separate secure channel.
  • Migrate only the provisioning profiles your current project needs, and check the team, app identifier and expiration date.
  • Don’t copy your entire login keychain. Create a dedicated build keychain, import only what you need and authorize the signing tools.
  • Sync your source code and required assets, excluding dependency directories, DerivedData and old build artifacts.

Pass when: Project versions match, certificates include private keys, and no sensitive files are in Git.

Path 02 · Toolchain

Pin versions before installing dependencies

  • Use xcodes list to check available versions, then install the required Xcode after completing any necessary authorization.
  • Use xcode-select to explicitly select the active toolchain; don’t rely on the last selection made in the GUI.
  • Check Homebrew and the arm64 toolchain. Don’t copy dependency directories directly from an Intel environment.
  • Use a Ruby version manager and Gemfile to pin Ruby, CocoaPods and fastlane.

Pass when: Xcode, Ruby and Bundler versions match in your terminal and CI.

Path 03 · CI setup

Allow runners to handle trusted jobs only

  • GitHub Actions: Register a self-hosted runner for the repository and select the macOS ARM64 package.
  • GitLab Runner: Register a project-level runner, choose shell execution if needed and restrict its project scope.
  • Jenkins: Create a macOS agent with an isolated working directory and controlled credential injection.
  • Start with one concurrent signing build, then increase concurrency based on peak memory usage.

Pass when: A trusted commit can be pushed and its logs and build artifacts can be retrieved.

Sync the project locally · Keep existing remote files
rsync -avz \
  --exclude='.git/' \
  --exclude='build/' \
  --exclude='DerivedData/' \
  --exclude='Pods/' \
  --exclude='node_modules/' \
  -e "ssh -p $MAC_PORT -i $HOME/.ssh/osxhost_build" \
  ./ "$MAC_USER@$MAC_HOST:~/build-workspace/"

This sync command doesn’t delete remote files. For production CI, check out a pinned commit from your repository. Cache keys should include at least the toolchain version, dependency lockfiles and target architecture. Never let untrusted external pull requests access signing credentials.

Step 3 · Verify signing and artifacts separately

Use one Fastfile to connect match, gym and pilot

From the project root, run bundle installand commit Gemfile.lock. The workflow below assumes your signing repository is initialized, your app already exists, and CI injects environment variables securely. It doesn’t create new signing assets.

Remote preflight · Check the toolchain first
xcode-select -p
xcodebuild -version
xcodebuild -showsdks
ruby --version
bundle --version
bundle exec fastlane --version
security find-identity -v -p codesigning
df -h "$HOME"

Set the signing repository URL in MATCH_GIT_URLand the decryption passphrase in MATCH_PASSWORD. For uploads, use a server-side API key. Set the key file path with ASC_KEY_PATH , and provide the key ID and issuer ID with ASC_KEY_IDandASC_ISSUER_ID . Store the key file as a CI secret and write it to disk at runtime; don’t commit it to the project repository.

Set IOS_APP_IDENTIFIER,IOS_TEAM_ID,IOS_WORKSPACE and IOS_SCHEME. Set the distribution type in the signing repository to match appstore . Verify the shared scheme before running the full pipeline.

fastlane/Fastfile · TestFlight pipeline for a single app
default_platform(:ios)

platform :ios do
  desc "Archive a signed build and upload it to TestFlight"
  lane :release do
    setup_ci

    api_key = app_store_connect_api_key(
      key_id: ENV.fetch("ASC_KEY_ID"),
      issuer_id: ENV.fetch("ASC_ISSUER_ID"),
      key_filepath: ENV.fetch("ASC_KEY_PATH"),
      in_house: false
    )

    match(
      type: "appstore",
      readonly: true,
      git_url: ENV.fetch("MATCH_GIT_URL"),
      app_identifier: ENV.fetch("IOS_APP_IDENTIFIER"),
      team_id: ENV.fetch("IOS_TEAM_ID"),
      api_key: api_key
    )

    gym(
      workspace: ENV.fetch("IOS_WORKSPACE"),
      scheme: ENV.fetch("IOS_SCHEME"),
      configuration: "Release",
      clean: true,
      export_method: "app-store",
      output_directory: "build/artifacts",
      output_name: "release.ipa"
    )

    pilot(
      api_key: api_key,
      ipa: "build/artifacts/release.ipa",
      skip_waiting_for_build_processing: true
    )
  end
end
Run and verify · CI injects the environment variables
bundle exec fastlane ios release
find build/artifacts -maxdepth 1 -type f -print

A successful upload doesn’t mean processing is complete.This example skips the post-upload processing wait. Check the build’s processing status in your distribution dashboard, and use a unique build number for every upload.readonly: true prevents CI from modifying the signing repository. If a certificate or provisioning profile expires, update it in a controlled environment and rerun the pipeline.

Cross-platform projects · Keep your lockfiles

Run iOS jobs independently and make caches safe to invalidate

The iOS stages of Flutter and React Native projects both depend on Xcode, CocoaPods and a signing setup. Run platform diagnostics before adding upload steps. A cache hit is no reason to skip dependency consistency checks.

Flutter: Get the simulator build working, then export an IPA

Pin the Flutter SDK version and keep pubspec.lock. Reuse the Pub download cache; key Pods by Podfile.lock and DerivedData by the Xcode version and project configuration. The first run takes longer because downloads and compilation start with an empty cache.

Run the command below from the project root. Once code signing is configured,flutter build ipa --release will export a build for a physical device.

Don’t reuse build caches across major Xcode versions

React Native: Pin Node, Ruby and Pods

Commit the lockfile for your JavaScript package manager and use the install command specified by your project. The example below uses npm. Manage Ruby dependencies with Bundler to avoid conflicts between system gems and project versions.

Use pod install --repo-updateonly when a new dependency requires an index update. For routine installs, run bundle exec pod install. Don’t delete Pods and lockfiles every time.

Include architecture and dependency lockfiles in cache keys
Flutter project root
flutter --version
flutter doctor -v
flutter pub get
flutter build ios --simulator
flutter build ipa --release
React Native project root · npm projects
node --version
npm ci
bundle install
(cd ios && bundle exec pod install)
xcrun simctl list devices available

Limit the number of simulators running tests in parallel.Get two actual UDIDs from the available device list and set SIM_UDID_AandSIM_UDID_B. Run the test below in a project configured with IOS_WORKSPACEandIOS_SCHEME . Start with one destination, then add the second and monitor memory pressure and disk writes.

Xcode testing · Use two simulator destinations supported by the project
xcodebuild test \
  -workspace "$IOS_WORKSPACE" \
  -scheme "$IOS_SCHEME" \
  -destination "platform=iOS Simulator,id=$SIM_UDID_A" \
  -destination "platform=iOS Simulator,id=$SIM_UDID_B" \
  -parallel-testing-enabled YES \
  -maximum-concurrent-test-simulator-destinations 2 \
  -resultBundlePath "build/TestResults-$(date +%s).xcresult"

Container limits · Split Linux services from macOS builds

Containers can run supporting services, but they can’t replace an Xcode host

The node is a dedicated physical Mac. Linux containers provided by Docker Desktop or OrbStack still run in a Linux environment; a physical node doesn’t mean containers and macOS share the same kernel.

Workload Where to run it What to consider
Test databases, caches and internal APIs Linux container Prefer arm64 images and limit memory and network exposure.
Xcode archives and simulator tests macOS host Requires the macOS SDK, code-signing tools and simulator runtime.
Services with amd64-only images Test compatibility first Translation can add overhead, so don’t assume arm64 performance.
Long-running supporting services Choose based on isolation needs These share physical memory, SSD storage and network with builds. Set resource limits and keep backups.
After installing and starting your chosen container tool, check
uname -m
docker version
docker info --format '{{.Architecture}}'
docker system df
docker stats --no-stream

Use either Docker Desktop or OrbStack to manage containers, so you always know which Docker context is active. Record available memory and disk space before adding services. Don’t treat deleting container data as a default troubleshooting step.

Expansion options · Identify devices before writing

Move caches to expanded storage; don’t treat multiple machines as one

An external SSD and Thunderbolt 5 networking are separate configuration options. Check your order details for the actual device name, mount path, interface support and network topology. Don’t format or partition a drive until you’ve verified them.

External SSD: Check the mount point and permissions

Use diskutil list external to identify the external device, then inspect the relevant volume. If it’s formatted but not mounted, verify its identifier and run diskutil mount "$DISK_IDENTIFIER". Don’t assume a disk is the external SSD based on its disk number alone.

Use the drive for dependency caches, DerivedData and temporary build artifacts. Set the actual path in the CI variable BUILD_CACHE_ROOT, check that the build user can write to it, and verify the path remains available after a restart. Keep separate backups of source code and signing assets.

More storage isn’t a backup

Thunderbolt 5 networking: Check each connection path

Networking multiple machines together doesn’t automatically combine their CPUs or unified memory into a resource pool for one process. Split workloads through your application or CI scheduler, and configure the toolchain, runner and working directory separately on each machine.

If your setup uses Thunderbolt networking, check each machine’s network interface, address and routes, then test point-to-point connectivity. Before ordering, open a support ticket with the number of machines, models and intended data transfer use. Don’t assume every model has the same interface capabilities.

Distributed workloads require support in the application
Read-only remote checks · No changes to disks or network
diskutil list external
ls -la /Volumes
df -h
system_profiler SPThunderboltDataType
networksetup -listallhardwareports

See the plans page for pricing across all four expansion terms. To change an existing order, open a support ticket first to confirm the process and data migration plan. Don’t change disk configurations during a production build.

Final step · Gather evidence to pinpoint the issue

Collect the minimum diagnostics before changing configuration

Note your order number, node, incident time and time zone, and the commit from your last successful build. Keep the first error and exit code—don’t send only the last line of a log. The commands below help narrow down the issue; they won’t automatically fix every problem.

01

Can’t connect over SSH

Check the address, port, username and private key path first. If the connection times out, start by checking network reachability. If authentication fails, check the username, public key authorization and permissions. Verify a changed host fingerprint through a support ticket; don’t simply ignore it.

Local terminal · Output may include host details
ssh -vv -o ConnectTimeout=10 \
  -i "$HOME/.ssh/osxhost_build" \
  -p "$MAC_PORT" "$MAC_USER@$MAC_HOST"
Open a connection support ticket
02

Certificate expired or signing identity not found

Check that the certificate includes a private key and hasn’t expired, and that the provisioning profile matches the app and team. The CI keychain must also be accessible during the build session. Fix signing assets in a controlled environment first, then sync with match in read-only mode.

Remote terminal · Check signing identities and keychains
security find-identity -v -p codesigning
security list-keychains -d user
bundle exec fastlane match appstore --readonly
Open a code-signing support ticket
03

Disk full; archive can’t be written

First, distinguish the system volume from any external volume. Find out how much space DerivedData, simulators and build artifacts use. Only clear caches you’ve confirmed can be rebuilt; don’t bulk-delete keychains, signing files or active working directories.

Remote terminal · Check disk usage
df -h
du -sh "$HOME/Library/Developer/Xcode/DerivedData" \
  "$HOME/Library/Developer/CoreSimulator" \
  "$HOME/build-workspace" 2>/dev/null
xcrun simctl list devices unavailable
Open a storage support ticket
04

Xcode stuck on indexing

Check whether indexing processes are still active, then verify dependencies are installed and disk space is available. GUI indexing and a command-line build have different completion criteria; use logs to confirm whether the archive is actually blocked.

Remote terminal · Check processes and build settings
ps -axo pid,%cpu,%mem,etime,comm | grep -E 'Xcode|SourceKit|swift'
xcode-select -p
xcodebuild -list -workspace "$IOS_WORKSPACE"
df -h "$HOME"
Open an Xcode support ticket

Never attach private keys, certificate passwords, access tokens or the full contents of a signing repository to a support ticket.Redact credentials and sensitive paths from logs before sharing them. If you can’t sign in to the console, email support@osxhost.comwith your order number and a description of the issue. Email and console support tickets are public support channels.

Get one archive working before increasing concurrency

Add toolchain versions, signing injection, cache keys and artifact directories to your pipeline. Order a machine when you need one; if you get stuck, open a support ticket with diagnostic details.