Use the hosted macOS Agent first. Deploy a self-hosted Azure DevOps macOS Agent on a remote Mac only when the pipeline needs persistent caches, a fixed Xcode toolchain, private network access, or tightly controlled signing credentials. An agent marked Online is registered, not production-ready: real builds, reboot recovery, and credential isolation must pass before release work moves to it.

This guide is for:

  • Developers running iOS or macOS builds and tests through Azure Pipelines.
  • DevOps engineers who need a remote Mac to remain available in a team Agent Pool.
  • Mobile platform owners responsible for certificates, provisioning profiles, publishing permissions, and build-node isolation.

Start with the hosted-versus-self-hosted decision

Microsoft describes hosted and self-hosted agents as different operating models, not interchangeable labels. A hosted agent offers a controlled, short-lived environment that is suitable when the source and build process can run inside the available image. A self-hosted agent gives the team control over installed tools, network reachability, caches, and local state, but also transfers patching, cleanup, access control, and recovery responsibilities to the team. Microsoft’s agent type guidance sets out this basic boundary.

Choose the hosted option when:

  • The repository does not contain untrusted code that needs access to private infrastructure.
  • The required Xcode version is available in the selected hosted image.
  • Builds can tolerate rebuilding dependencies instead of keeping a local cache.
  • The job does not need a private signing keychain or a persistent simulator state.
  • The pipeline can install its dependencies reproducibly on every run.

Choose a remote Mac self-hosted agent when at least one of these conditions is real and documented:

  • A fixed Xcode installation must remain available between jobs.
  • Dependency or build caches are large, slow, or difficult to recreate.
  • The job must reach an internal package registry, test service, device lab, or network endpoint.
  • Signing must happen inside a controlled host boundary with a dedicated account.
  • The team needs a continuously available Apple Silicon build node rather than an ephemeral image.

The trade-off is operational. Persistent state can shorten setup time, but it can also preserve a bad tool selection, stale credentials, failed simulator data, or another project’s files. Microsoft’s hosted-agent documentation also emphasizes the isolated and temporary nature of hosted execution, which is an important reason to keep untrusted external code away from a sensitive self-hosted node. Review the hosted-agent execution model before moving a repository.

Warning: A remote Mac should not become a general-purpose desktop shared by developers and CI. Mixing interactive work, signing material, and unattended jobs makes ownership and forensic cleanup difficult.

Step one: create a separate pool and operating account

A clean registration starts with ownership boundaries, not with the download command.

Create a dedicated Agent Pool for the workload, such as <IOS_POOL_NAME>. Grant only the projects and pipelines that need this pool. Avoid placing a signing-capable Mac in the default pool where unrelated jobs can select it accidentally. Azure Pipelines chooses an agent by pool membership and demand matching, so pool design directly affects job placement. Microsoft’s run and agent matching documentation explains how jobs wait for a compatible agent.

On the Mac, create a dedicated system account such as <CI_ACCOUNT>. Its name should identify the automation role, not a human owner. Keep administrative access separate from the account that runs builds. The account needs access to the installed developer tools and work directory, but it should not be used for routine browsing, email, or unrelated development.

From the Azure DevOps organization settings:

  1. Open the target Agent Pool.
  2. Choose the option to add a new agent.
  3. Select macOS and copy the package and registration instructions shown by the console.
  4. Create a working directory such as <AGENT_WORK_DIR>.
  5. Run the generated registration command with placeholders for <ORG_URL>, <POOL_NAME>, <AGENT_NAME>, <WORK_DIR>, and the authentication value supplied during registration.
  6. Confirm the agent name, pool, account, and work directory in the console.

The authentication method must follow the current organization policy and the options shown in the official documentation. Do not publish a token inside a repository, shell history, image, or team wiki. Check the supported self-hosted agent authentication options immediately before registration because authentication choices and console prompts can change.

Initial evidence is limited to four checks:

  • The agent appears in the intended pool.
  • Its status is Online.
  • Its reported version is accepted by the organization.
  • Its capabilities include the basic tools expected on the host.

That evidence confirms registration. It does not confirm Xcode routing, signing safety, graphical test behavior, or restart recovery.

Step two: keep command-line and graphical sessions separate

A pure xcodebuild job can often run as a background service under the dedicated account. Simulator and UI tests have a different boundary: they may depend on an active macOS login session, WindowServer access, simulator services, or permissions that are not available to a process started only through SSH.

The macOS agent documentation describes the supported service setup and the svc.sh commands used to install, start, stop, and inspect the service. Use the official macOS agent service instructions rather than copying an old command from a blog post.

A command-line build should pass these tests:

  • Start the agent through the supported service mechanism.
  • Disconnect the SSH session.
  • Submit a small pipeline job.
  • Stop and start the service.
  • Reboot the Mac and submit another job.

A graphical test requires additional checks:

  • Log in through the intended macOS session.
  • Confirm that the simulator can boot for the agent account.
  • Run the UI test without an open SSH terminal.
  • Log out or disconnect the graphical session according to the team’s supported operating procedure.
  • Record whether the job fails because of the session boundary, not because of Xcode.

launchd and the agent service are not a universal solution for graphical automation. A service can reconnect the agent while the simulator remains unavailable to the process. If UI tests need a persistent logged-in session, that requirement belongs in the node design and security review.

Step three: route Xcode jobs by verified capabilities

An Azure Pipelines job should not select a Mac merely because the machine is online. It should select a node whose capabilities match the build’s actual toolchain.

First inspect the agent capabilities in Azure DevOps. Then verify the developer directory locally:

xcode-select -p
xcodebuild -version
xcodebuild -showsdks

These commands are part of the Xcode command-line tool workflow documented by Apple. Compare the commands with Apple’s Xcode command-line tool reference before using their output as acceptance evidence.

A minimal project test should confirm:

  • The expected Xcode installation is selected.
  • The project or workspace opens without a missing SDK error.
  • The scheme is shared and visible to command-line tools.
  • Tests execute on the intended simulator or destination.
  • Test results are published in the pipeline.
  • The archive or build artifact is created at the expected path.
  • The workspace is clean enough for the next job.

If a pipeline cannot find an agent with an Xcode capability, inspect three layers in order:

  1. Pool: the job may be requesting a different pool.
  2. Capability: the agent may expose a different name or value than the demand expects.
  3. Tool state: Xcode may have been installed or switched after agent registration.

After installing or changing Xcode, restart the agent and inspect capabilities again. Do not treat the previous capability list as current. In the YAML, keep demands narrow and explicit, using placeholders where the organization’s displayed capability names differ:

pool:
  name: <IOS_POOL_NAME>
  demands:
    - <XCODE_CAPABILITY> -equals <EXPECTED_XCODE_VALUE>

A demand that is too broad can route a job to a technically compatible but operationally wrong Mac. A demand that is too strict can leave jobs queued even though a suitable node exists. The correct value comes from the live agent record, not from a generic example.

Compare the operating choices before committing a Mac

The decision is not simply “cloud versus hardware.” It is a choice between control, repeatability, and the amount of infrastructure the team is willing to own.

Operating choice Best fit Main control benefit Main risk or cost
Hosted macOS Agent Reproducible, short-lived builds with supported tools Clean environment managed outside the project Limited persistence, image availability, and private-network reach
Remote Mac self-hosted Agent Fixed Xcode, private dependencies, persistent caches, controlled signing Direct control of tools, account, pool, and host state The team owns patching, cleanup, recovery, and access review
A dedicated physical Mac owned by the team Long-lived workloads with physical-device or local-network requirements Full hardware and network ownership Upfront purchase, hardware lifecycle, and single-host failure handling
A temporary RUVCLOUD Mac rental Time-limited build capacity, migration, testing, or a separately controlled node No need to purchase and maintain another Mac in-house Subscription planning, network latency, and the need to validate the service boundary

The table is a selection aid, not a performance promise. The right choice depends on the project’s toolchain, network path, signing model, and expected maintenance ownership.

For teams evaluating a remote Mac, the remote Mac build-node configuration and rental-period guidance can be reviewed alongside the pipeline acceptance checks below. A rental node is still a self-hosted machine from Azure Pipelines’ point of view. The CI team remains responsible for pool permissions, agent registration, secrets, and job hygiene.

Step four: isolate signing from ordinary builds

Signing is where a convenient self-hosted node can become a high-impact security boundary. Certificates, provisioning profiles, App Store credentials, and keychain access should not be treated as ordinary build inputs.

Use protected Secure Files for certificates and provisioning profiles. Their permissions should be granted to only the pipelines that need them, and the files should be installed for the controlled signing stage rather than copied into the repository or stored in ordinary pipeline variables. Read the Secure Files permission and usage model.

A safer pipeline separates the validation stages:

Unsigned build

Run compilation and unit tests without importing release credentials. This proves that the source, dependencies, Xcode selection, and test commands work without exposing signing material.

Controlled archive

Download the required protected files, import them into the intended keychain, build the archive, and export only the required artifact. Restrict the pipeline identity and agent pool used for this stage.

Cleanup and negative test

After the archive, remove temporary profiles, certificate files, exported credentials, and unnecessary keychain entries. Then inspect the work directory and logs to confirm that secrets were not printed or left behind. Run a job that should not have signing access and confirm that it cannot select the signing pool or download the protected files.

Microsoft’s Apple signing guidance and pipeline security recommendations should be reviewed together because file protection alone does not correct an overly broad pool or project permission. Follow the Apple platform signing workflow and the Azure Pipelines security guidance.

Operational rule: If several projects share one remote Mac, separate sensitive and non-sensitive pools unless the team can prove workspace cleanup, keychain isolation, and project-level permissions after every run.

FAQ: resolve the common deployment decisions

How should a remote Mac run an Azure Pipelines iOS build?

Register the Mac in a dedicated pool, install the approved Xcode toolchain, and route jobs through explicit pool demands. Start with an unsigned test project, then add simulator tests and signing as separate acceptance stages. This order identifies toolchain and session failures before certificates or publishing permissions make the diagnosis harder.

Why does the pipeline report no matching Xcode agent?

An Online agent may still lack the capability value requested by the job. The agent could be in the wrong pool, Xcode may have changed after registration, or the demand may use an outdated capability name. Inspect the live capability list, run xcode-select and xcodebuild -version, restart the agent, and repeat the small project test.

What is needed for automatic recovery after a Mac reboot?

The service must be installed for the intended account and checked with the supported macOS agent service commands. Recovery is accepted only after a real reboot, an SSH disconnect, and a new pipeline run. Command-line jobs and graphical tests must be tested separately because reconnecting the agent does not guarantee that a simulator or UI session is available.

How can a team add a self-hosted macOS Agent without creating a security gap?

Use a dedicated account, a separate pool, least-privilege project authorization, and authentication values generated by the organization console. Keep registration material out of source control and shared shell history. Add Secure Files only to the signing pipeline, then inspect logs, workspaces, and keychains after the job to verify that temporary credentials were removed.

When is a remote Mac the wrong long-term option?

A hosted agent is usually the better fallback when builds are untrusted, reproducibility is already strong, and no private network or fixed toolchain is required. A purchased Mac may be better for stable heavy workloads, physical USB access, or local device-lab integration. A rental Mac is less suitable when the team needs permanent hardware ownership or has no capacity to operate a self-hosted node.

Step five: accept the node through failure and recovery tests

A production decision should use evidence from the complete workflow, not a green registration screen.

Run the following acceptance checklist:

  • [ ] The agent belongs to the intended dedicated pool.
  • [ ] The build account is separate from human administration accounts.
  • [ ] The agent reconnects after the supported service is restarted.
  • [ ] A command-line build completes after an SSH session is closed.
  • [ ] A simulator or UI test has a documented login-session requirement.
  • [ ] xcode-select, xcodebuild, SDK visibility, and shared schemes match the pipeline.
  • [ ] Pool demands select the intended Xcode-capable node.
  • [ ] Unsigned builds work without signing credentials.
  • [ ] Controlled signing can download only the required Secure Files.
  • [ ] Certificates, profiles, temporary keychains, and exported credentials are removed after the job.
  • [ ] A failed job does not leave another project’s workspace available to the next job.
  • [ ] Repeated jobs do not create uncontrolled disk growth.
  • [ ] A retry after a transient failure produces the expected artifact.
  • [ ] A system reboot is followed by a successful real pipeline run.
  • [ ] An agent update is followed by capability inspection and build retesting.

Workspaces deserve special attention. Persistent caches should be kept only when their ownership and invalidation rules are clear. Dependency caches can contain source fragments, generated files, or credentials embedded by a faulty tool. A cleanup policy should define what survives a successful job, what is removed after failure, and who reviews disk growth.

A shared node should be downgraded to isolated use when projects need incompatible Xcode versions, different signing identities, conflicting simulator data, or separate private-network permissions. If the team cannot prove clean handoff between projects, return the workload to hosted execution or split the node into dedicated pools.

Choose the next step from the evidence

If hosted execution passes the toolchain, network, and security tests, keep it as the default. If the pipeline requires a fixed Xcode installation, persistent caches, private dependencies, or a controlled signing account, continue with a dedicated remote Mac and treat it as infrastructure rather than a spare desktop. If reboot recovery, workspace cleanup, or signing isolation fails, do not promote the node; fix the boundary or return the workload to a hosted agent.

Compared with buying and operating another Mac, a remote Mac can avoid hardware procurement, local maintenance, and a machine sitting unused between project phases. Compared with hosted execution, it introduces persistent state, network latency, account administration, and cleanup responsibility. Those are real drawbacks, not details to hide. For a team that needs temporary capacity, a fixed Xcode environment, or a separately controlled build node, RUVCLOUD remote Mac options can be evaluated after the minimum pipeline has passed; the rental period should match the testing or release window instead of replacing a long-term ownership decision without evidence.

The sensible final gate is simple: if the Mac can build, recover, route the correct Xcode workload, and protect signing material under a real pipeline, it is ready for controlled production use. If it only shows Online, it is still being configured.