From first login to your first build

Get your first iOS build pipeline running

First, confirm you can connect. Then pin your toolchain and hook it up to CI. This guide follows the steps you’ll take to configure a dedicated physical Mac as a reliable, repeatable build runner. It’s not a virtual machine—you can use both the macOS graphical interface and the command line.

Already have a host? Start with “Provisioning and login” below. Still choosing a setup? Validate a build with one job first, then decide how much concurrency you need.

Provisioning & login

Check your order and host credentials, connect over SSH for the first time, and add your own public key.

First connection steps

Remote access

Use SSH for scripts and CI. For Xcode, simulators, and the graphical interface, connect over VNC screen sharing.

Connection setup

CI setup

Register a self-hosted runner, use distinct toolchain labels, and start with one concurrent build per host.

Set up a runner

Billing & renewals

Check your billing period, add-ons, and expiry date. The console is the source of truth for order, payment, and renewal status.

Billing guide
Start by checking your connection

Your first hour: take these four steps

These are recommended steps, not a commitment to provision or resolve anything within a specific timeframe. First, confirm in the console that your service is ready. Then use the address, port, and username returned for your instance. Don’t guess host credentials based on the examples.

  1. Verify your instance

    Sign in to the console and check the plan, node, and rental term in the service details. Note the host ID; use it to reference the service in future troubleshooting, renewal requests, and support tickets. Connection details and initial credentials are visible only to the authorised account.

  2. Connect over SSH

    Use the details returned by the console in your local connection settings. On first connection, verify the host key fingerprint. If you don’t have a trusted source for the fingerprint, confirm it through a support ticket rather than skipping verification. Once connected, check the system and user.

  3. Change your login password

    Run passwd in an interactive SSH session and follow the prompts to change the current user’s password. Never put passwords in command arguments, repositories, chat logs, or build logs. Keep your current session open and use a second session to verify the new credentials.

  4. Add your public key

    Generate a dedicated key on your own computer and add only the public key to the host. Once public-key login works, you can adjust the password login policy. Keep an active session open before changing SSH settings so you don’t lock yourself out.

Local connection settings and host checks
ssh -p "$MAC_SSH_PORT" "$MAC_USER@$MAC_HOST"

whoami
sw_vers
uname -m
xcode-select -p
df -h
passwd

Before connecting, set MAC_SSH_PORT, MAC_USER, and MAC_HOST locally using the values shown in the console. Run the remaining commands on the host after you log in. xcode-select -p checks the current developer tools path.

On the host · Add your copied public key
install -d -m 700 "$HOME/.ssh"
touch "$HOME/.ssh/authorized_keys"
chmod 600 "$HOME/.ssh/authorized_keys"
printf '%s\n' "$LOGIN_PUBLIC_KEY" >> "$HOME/.ssh/authorized_keys"

Set LOGIN_PUBLIC_KEY to your complete public key before running the append command. Never upload your private key, and don’t add the same public key twice. Connection timeouts, authentication failures, and host key changes are different issues; describe them separately in your support ticket.

Make your environment reproducible

Pin Xcode, then set up your simulators

Start by noting the Xcode, SDK, and runtime versions your project requires—not by upgrading straight to the latest release. macOS and Xcode compatibility determines which versions you can install. For an existing project, stick with the combination that builds successfully on your local machine, then test upgrades separately.

If xcodes isn’t installed on the host, set it up using your team’s approved software installation process. To install a specific version, use a locally available Xcode installer obtained through legitimate channels and matching your project requirements. XCODE_VERSION and XCODE_XIP below refer to the verified version number and the installer’s absolute path, respectively. Don’t put interactive installation steps in unattended build jobs.

On the host · Install and verify developer tools
xcodes installed
xcodes install "$XCODE_VERSION" --path "$XCODE_XIP"
xcodes installed

export DEVELOPER_DIR="$XCODE_APP/Contents/Developer"
xcodebuild -version
xcodebuild -showsdks
xcrun simctl list runtimes

XCODE_APP should point to the actual path of the installed app. For CI, set DEVELOPER_DIR in the job environment where possible, rather than having jobs compete to change the global selection. If you need to switch the system-wide default toolchain, run sudo xcode-select --switch "$XCODE_APP/Contents/Developer", then verify it again with xcodebuild -version.

Launch the selected version of Xcode once and complete any required component setup. You can use a remote connection to access the graphical interface, then install the target runtime in Xcode’s component settings. Run xcrun simctl list devices available and choose a simulator that matches your test target. Launch it interactively once and wait for startup to finish before running a minimal test job. Archiving a device build and running simulator tests are separate steps; don’t mistake a missing runtime for an archive toolchain issue.

Log toolchain checks at the start of each CI job: include at least the Xcode version, SDK list, and commit ID. After changing Xcode, generate a new cache key and verify third-party dependencies, signing, and export. Changing only the runner label without checking the actual tool path won’t ensure a consistent build environment.

Start with one job, then scale

Register your host as a self-hosted runner

Jobs on the same physical node share CPU, memory, disk, and user environment. Start with one build job per host. Once archiving, signing, and cleanup are reliable, increase concurrency based on actual peak usage or add a separate node.

GitLab Runner: Check labels and executor together

Create a project runner in your project’s runner settings, get its authentication token, and assign the labels macos, arm64, and ios-archive. Temporarily set the instance URL and authentication token as GITLAB_URL and GITLAB_RUNNER_TOKEN, then register a shell executor under a dedicated build user. Remove the temporary token variable when you’re done.

GitLab Runner · Registration and concurrency settings
gitlab-runner register \
  --non-interactive \
  --url "$GITLAB_URL" \
  --token "$GITLAB_RUNNER_TOKEN" \
  --executor "shell" \
  --description "armrdp-ios-archive"

unset GITLAB_RUNNER_TOKEN
gitlab-runner verify

In the config.toml used by this runner, set the global concurrent = 1 and set limit = 1 for the runner. Start the runner using the service management method for your installed version. A successful interactive check doesn’t mean the background service is running.

In your project · Archive job in .gitlab-ci.yml
ios_archive:
  tags:
    - macos
    - arm64
    - ios-archive
  script:
    - xcodebuild -version
    - xcodebuild archive -workspace "$IOS_WORKSPACE" -scheme "$IOS_SCHEME" -destination 'generic/platform=iOS' -archivePath "$CI_PROJECT_DIR/output/App.xcarchive"
  artifacts:
    paths:
      - output/App.xcarchive
    expire_in: 7 days

Set IOS_WORKSPACE and IOS_SCHEME to the values used by your project, and complete the signing setup in the next section first. This example archives the app only; it doesn’t export or upload it. The seven-day artifact retention is just an example—adjust it to your team’s retention policy. Don’t put certificates, private keys, or temporary Keychains in artifacts.

GitHub Actions: One job per runner process

Follow the runner installation steps in your repository settings to get the software for macOS ARM64, then move into the extracted directory. Set REPOSITORY_URL to your repository URL and RUNNER_TOKEN to the token for this registration. Use the same labels in the runner configuration and workflow to avoid routing jobs to the wrong toolchain.

GitHub Actions · Register the host
./config.sh \
  --url "$REPOSITORY_URL" \
  --token "$RUNNER_TOKEN" \
  --name "armrdp-ios-archive" \
  --labels "macos,arm64,ios-archive" \
  --work "_work"

unset RUNNER_TOKEN
./run.sh
In your project · GitHub Actions workflow
name: iOS archive
on:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: ios-archive
  cancel-in-progress: false

jobs:
  archive:
    runs-on: [self-hosted, macos, arm64, ios-archive]
    env:
      DEVELOPER_DIR: ${{ vars.DEVELOPER_DIR }}
      IOS_WORKSPACE: ${{ vars.IOS_WORKSPACE }}
      IOS_SCHEME: ${{ vars.IOS_SCHEME }}
    steps:
      - uses: actions/checkout@v4
      - name: Archive
        run: |
          xcodebuild -version
          xcodebuild archive \
            -workspace "$IOS_WORKSPACE" \
            -scheme "$IOS_SCHEME" \
            -destination 'generic/platform=iOS' \
            -archivePath "$RUNNER_TEMP/App.xcarchive"

Set the tool path, workspace, and scheme in your repository variables first. This example uses manual triggering so you can validate the environment before automating it. Don’t expose a runner with signing credentials to untrusted external code. Each runner process takes one job at a time, while concurrency limits workflows in the same group; it doesn’t impose host-level resource limits.

Once everything checks out, use the supplied service script to keep the runner running, and make sure the service user matches the user you tested with manually. If you set up signing under one user but run the service as another, interactive builds may succeed while CI can’t find the certificate or provisioning profile.

Keep credentials separate from code

Set up signing as the build user

Your distribution certificate, matching private key, and provisioning profile must match the team and Bundle Identifier used by the project. Verify signing in a controlled job before adding it to an unattended workflow. Don’t disable signing checks to hide an export failure.

Use a separate build Keychain for automation, with its password supplied through a protected CI secret. After importing the certificate, grant access only to the signing tools that need it; don’t allow every app to access the private key. The checks below confirm that the current user can see a valid signing identity. They don’t guarantee that the provisioning profile, export settings, and project configuration all match.

On the host · Check signing identities and profile directories
security list-keychains -d user
security find-identity -v -p codesigning
printf '%s\n' "$HOME/Library/MobileDevice/Provisioning Profiles"
printf '%s\n' "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"

Provisioning profile locations can vary between Xcode versions, so use the path read by your current toolchain. After importing a profile, check its expiry date, app identifier, and signing type. If archiving succeeds but exporting fails, check the distribution method, team ID, and profile mapping in the export settings instead of repeatedly reinstalling Xcode.

If your team already uses fastlane match, stick with its existing encrypted certificate repository and access policy. During validation, use bundle exec fastlane match appstore --readonly to read existing signing assets without accidentally changing team certificates from the build host. Manage repository credentials, the decryption password, and the Keychain password separately. Expose production signing secrets only to protected branches or controlled manual jobs.

At the end of each job, remove temporary files and lock the temporary Keychain. Keep only the necessary error codes and step results in logs; never print private key contents, certificate passwords, registration tokens, or secret variable values. A dedicated physical machine provides resource isolation, but credentials within your project should still follow the principle of least privilege.

Treat caches as rebuildable data

Verify the extra volume, then move DerivedData

You can add a +1TB SSD or +2TB SSD. First, confirm the expansion is complete in your order and on the host, then plan your data migration. Use the volume name, mount path, and filesystem provided for your instance—don’t copy disk identifiers from another host.

Run diskutil list and df -h to check capacity and mount points. Don’t erase or partition an unfamiliar disk. Create a cache directory that the build user can write to and test it with a small file. Once you’ve confirmed how the volume mounts after a restart, point your persistent runner to the new path.

Pause incoming jobs before migration and wait for any running archive or signing tasks to finish. DerivedData is a rebuildable cache, so generate it at the new location rather than copying an old cache that may still be in use. The example below uses an explicit -derivedDataPath; you don’t need to move the entire user directory to the extra volume.

On the host · Check the mount and set a build cache path
diskutil list
df -h

test -d "$BUILD_VOLUME" && test -w "$BUILD_VOLUME"
mkdir -p "$BUILD_VOLUME/DerivedData/$CACHE_KEY"

xcodebuild archive \
  -workspace "$IOS_WORKSPACE" \
  -scheme "$IOS_SCHEME" \
  -destination 'generic/platform=iOS' \
  -derivedDataPath "$BUILD_VOLUME/DerivedData/$CACHE_KEY" \
  -archivePath "$BUILD_VOLUME/Archives/$BUILD_ID/App.xcarchive"

Before running the commands, set BUILD_VOLUME to a confirmed writable mount point. CACHE_KEY should distinguish the repository, Xcode version, and build configuration. Use a unique ID for the current job as BUILD_ID so jobs don’t overwrite each other’s archives. Don’t share a writable DerivedData directory between parallel jobs.

Run one build without a cache first, then monitor cache hits and disk usage in subsequent jobs. Set separate retention policies for archives, simulator data, and dependency caches, and check that no job is using the files before deleting them. Store build artifacts separately in storage controlled by your team; host caches aren’t a backup. Export what you need before your rental term ends, and verify that your backup can be read.

Make issues easy to reproduce

A clear set of steps beats an error message alone

For host, order, or payment issues, sign in to the console and submit a support ticket so it can be linked to your service. If you can’t sign in, email us at support@armrdp.com. These are the official ways to get in touch.

Include details relevant to the issue. Never share passwords, private keys, or complete payment credentials.
FieldWhat to includeWhy it helps
Host or order IDThe relevant ID from the console and the current nodeIdentifies the service and avoids checking the wrong instance
When it happenedStart and end times, time zone, and whether the issue is ongoingHelps match service records with job logs
Issue typeConnection timeout, authentication failure, build or signing failure, or billing statusHelps distinguish network, toolchain, and payment issues
Steps to reproduceOrder of operations, minimal command, expected result, and actual resultReduces the back-and-forth needed to gather context
Environment and logsXcode version, runner service user, and redacted logs from before and after the errorShows whether the issue may be version- or permission-related
ImpactWhether one job or the entire host is affected, and whether another build path is availableClarifies the scope and what you’ve tried so far

Response time and resolution time are different

Check the actual updates in the console for ticket progress and response times. An initial reply doesn’t mean an issue has been resolved. Connection problems may require checks of host status and network access; build issues often need job logs and reproduction steps; payment questions need the related order and transaction records. These cases don’t share a single fixed resolution time.

While you wait, add any new evidence to the same ticket and note how long you’ve been waiting and whether the impact has grown. Avoid reinstalling, wiping disks, or running bulk cleanup commands that could change the state of the host. If an error occurs only in CI, note the difference between an interactive run and a service run under the same user. For signing issues, share redacted error details only.

Billing, renewals, and checks before expiry

Services are prepaid by the day, week, month, or quarter. When reviewing an order, check the plan, node, billing period dates, and add-ons. Renewal settings, whether auto-renewal is enabled, and payment results are based on the current settings and status returned by the console. Submitting a payment doesn’t mean the order is active—don’t rely on a screenshot of the payment page to confirm service status.

We accept USDT-TRC20 and Visa / Mastercard / Amex (via Stripe) only. All charges are in USD, and available payment gateways are determined by the response from the backend. For on-chain payments, verify the network, recipient details, and amount. If you need help investigating a payment, provide the order ID and transaction ID—never share your wallet’s private key or full card details.

Before expiry, stop starting new builds and export code changes, archived builds, and any necessary configuration. Verify that you can restore your backup from your own storage. If you plan to keep using the service, renew it and confirm the new expiry date. If you’re ending your rental, don’t rely on the host as a long-term archive after expiry.

Get one scheme building reliably first

Start with one host and one job. Verify your toolchain, signing, and build artifacts before adding more runners. Rent a dedicated physical Mac by the day, starting at $21.1/day. Availability is shown in real time in the console.