Self-hosted Runner as macOS VM Image
Self-hosted runner installation is explained at Appcircle docs in detail. You can install runner in your self-hosted environment by yourself, following instructions on there.
We're also providing ready-to-use runner VM image that you can download from Appcircle CDN. Especially for enterprise installation, it might be more practical than installing from scratch.
Here are some reasons for choosing VM image for self-hosted runner;
- It's more quick and easy way of deploying runner. Just download VM image and make some minor configurations for your environment.
- While installing runner, we need several packages from various sources on internet and runner installation requires to be online. Since all iOS and android build pipeline tools are included in VM image, you don't need complex firewall rules while deploying runner.
- It minimizes effects of strict macOS policies applied to host, especially at enterprise environments. Your runner environment will be same as used in Appcircle cloud.
- Since we use virtual machine for runner, it provides a clean, isolated instance for every build job. It means, known and more stable runner environment. Your runner won't persist any macOS changes done in build pipeline which can make runner unstable in time.
- You can install and run only one instance of self-hosted runner on a physical machine. On the other hand, using virtualization infrastructure brings us concurrency. We can run multiple instances of runner on the same host.
Requirements​
In order to use macOS VM, we need to install some dependencies on macOS host.
The macOS VM runs on Apple's Virtualization framework, which supports macOS guests only on Apple silicon (M-series) Macs. Intel-based Macs can't be used as a host.
Xcode 27 and later also run only on Apple silicon, so Intel Macs are no longer an option for iOS builds either.
Homebrew, Tart and Pigz are installed from the internet in the steps below. If the host has no internet access, install them before you continue, for example through temporary internet access or through an HTTP(S) proxy. The macOS VM image, the Xcode images and the runner scripts can then be downloaded through an artifact proxy in your network, as described in the download step.
1. Install Homebrew​
Script explains what it will do, follow instructions on there.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
When installation complete, test:
brew doctor
brew --version
2. Install Tart​
Tap Appcircle repository and install tart (Tart is a registered trademark of Cirrus Labs, Inc.).
Use appcircleio/cli, not cirruslabs/cli. The upstream tap ships a different Tart version under a different license, is not tested with the Appcircle runner, and currently fails to install on Homebrew 7.x.
brew tap appcircleio/cli
Homebrew 6.0+ requires tap trust before it will load a dependency's formula. appcircleio/cli/tart is trusted automatically because it's installed by its full name, but its dependency appcircleio/cli/softnet is not, so trust it explicitly first:
brew trust --formula appcircleio/cli/softnet
brew install appcircleio/cli/tart
When installation complete, test:
tart --version
Run tart to touch initial folders.
tart list
Create VMs root folder in tart home.
mkdir $HOME/.tart/vms
3. Install Pigz​
Pigz is an acronym for Parallel Implementation of GZip, which is a fully functional replacement for gzip that helps us compress and extract files faster and more stably.
brew install pigz
When installation complete, test:
pigz --version
4. Configure Power Settings​
Configuring power settings on macOS to prevent the system from entering sleep mode is vital when deploying it as an Appcircle runner.
By keeping the system awake, you ensure uninterrupted accessibility to your Appcircle runners, preventing any potential offline cases caused by the system going to sleep.
This continuous availability is critical for running builds, as it guarantees that your builds can find runners online.
We suggest disabling those power settings to make it behave like a server station.
sudo pmset -a sleep 0
sudo pmset -a powernap 0
sudo pmset -a disksleep 0
sudo pmset -a displaysleep 0
5. Configure Power Failure Settings​
Power failure settings allow a Mac to restart automatically after a power outage or failure. Activating this on a Mac ensures the host comes back online automatically if power is lost, avoiding downtime.
Restarting the host is only half of the recovery. Something also has to start the runner VMs afterwards.
Install the runners as a launchd service and they start on their own at boot, with no SSH session and no desktop login. See Running macOS VM Runners as a Service.
Without that service you have to connect to the host with SSH and start the VMs manually after every restart.
To configure power failure settings, you can run the command below.
sudo /usr/sbin/systemsetup -setrestartpowerfailure on
You should see "setrestartpowerfailure: On" in the command output after successful execution.
If your host doesn't support this feature, you will get the message below.
Restart After Power Failure: Not supported on this machine.
You can ignore power failure settings if they are not supported.
Download MacOS VM​
First choose where the host downloads the files from. Then download the images manually or automatically.
To perform these tasks manually, you can follow our step-by-step guide on downloading the macOS VM image manually.
Alternatively, you can automate this process in the background by following our instructions on downloading the macOS VM and Xcode images automatically.
Choose the Download Source​
The commands on this page download the macOS VM image, the Xcode images and the runner scripts from Appcircle by default. If the host has no internet access, it can download the same files through an artifact proxy in your network, such as Sonatype Nexus or JFrog Artifactory.
- Appcircle (internet or HTTP proxy)
- Artifact proxy (no internet access)
No action is needed. Continue with the next step.
Before you start, note what works differently when the runners have no internet access:
- Publishing to App Store, Google Play, Huawei AppGallery and Microsoft Intune, App Store Connect certificate management, and Slack or Microsoft Teams notifications need the internet and don't work.
- The Android emulator doesn't run inside the macOS VM, so emulator-based UI tests can't run on these runners.
- Builds download their dependencies, such as Gradle, CocoaPods and npm packages, through the same artifact proxy. You point the build tools at it in the base VM configuration later in this guide.
- Workflow steps that call an internet service fail. For example, the Check Network Access step fails with its default inputs, because it tests public addresses.
Ask your artifact proxy administrator to create two proxy repositories for the Appcircle download locations:
| Repository name (example) | Sonatype Nexus type | JFrog Artifactory type | Remote URL |
|---|---|---|---|
appcircle-gcs | raw (proxy) | Generic remote | https://storage.googleapis.com/appcircle-dev-common/self-hosted/ |
appcircle-cdn | raw (proxy) | Generic remote | https://cdn.appcircle.io/self-hosted/ |
Notes for the artifact proxy administrator
- The proxy server needs outbound access to these two remote URLs. The host only needs access to the proxy.
- Every image has a
.md5checksum file next to it. The proxy serves it from the same remote URL; no extra repository is needed. - A macOS VM image is about 85 GB and an Xcode image package is several GB. Appcircle releases a new image every one to two months, and most organizations upgrade when they need a new Xcode version. The proxy keeps every image it has served, so plan the storage and a cleanup policy for old images.
- The first download of a new image takes longer, because the proxy fetches it from Appcircle at the same time.
Set the URLs of these two repositories on your artifact proxy on the host. Replace nexus.example.com and the repository names with your own. Writing them to ~/.zprofile keeps them for new terminal sessions and for later image upgrades.
echo 'export AC_RUNNER_IMAGE_BASE_URL="https://nexus.example.com/repository/appcircle-gcs"' >> ~/.zprofile
echo 'export AC_RUNNER_CDN_BASE_URL="https://nexus.example.com/repository/appcircle-cdn"' >> ~/.zprofile
source ~/.zprofile
If your artifact proxy requires a username and password to read these repositories, store the credentials in ~/.netrc and let curl read that file. Replace nexus.example.com and <username> with your own; the password is asked for and isn't kept in the shell history.
printf "Artifact proxy password: " && read -rs AC_PROXY_PASSWORD && echo
printf 'machine %s login %s password %s\n' "nexus.example.com" "<username>" "$AC_PROXY_PASSWORD" >> ~/.netrc
unset AC_PROXY_PASSWORD
chmod 600 ~/.netrc
echo 'netrc-optional' >> ~/.curlrc
With netrc-optional in ~/.curlrc, every curl command on this page sends the credentials to the proxy host only. Skip this step if your artifact proxy allows anonymous read access to these repositories.
Check that the host can download through both repositories on your artifact proxy:
curl -fsS -o /dev/null "$AC_RUNNER_IMAGE_BASE_URL/run.sh" && \
curl -fsS -o /dev/null "$AC_RUNNER_CDN_BASE_URL/download-runner.sh" && \
echo "Artifact proxy is reachable."
The download commands on this page use these URLs from now on. You don't need to change them.
Download the macOS VM Image Manually​
MacOS VM image has a versioning convention based on release date instead of arbitrary numbers. This date-based approach is called calendar versioning, or CalVer for short.
Our calendar versioning scheme for the macOS image is YY0M0D. For example, a macOS image that's released on March 6, 2026, should have version 260306.
The versions are listed in reverse chronological order, from the most recent to the earliest, in the tabs below.
macOS VM images that ship only Xcode versions earlier than Xcode 16 are no longer listed, since apps built with those Xcode versions can no longer be submitted to the App Store. Images that run on Sonoma or Ventura hosts are no longer listed either; the minimum supported host is macOS Sequoia. Images whose Xcode versions are all included in a newer listed image with the same host requirement are not listed either; use that newer image instead.
If you still need an image that is no longer listed, contact us through our support channels.
Download the macOS VM image.
- 261102
- 260723
- 260303
- 251015
curl -L -O -C - "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/macOS_261102.tar.gz"
curl -L -O -C - "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/macOS_260723.tar.gz"
curl -L -O -C - "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/macOS_260303.tar.gz"
curl -L -O -C - "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/macOS_251015.tar.gz"
If you encounter network interruption, just run the same command again. It should continue download for remaining part. It will result in saving both time and bandwidth.
Host macOS Requirements​
We're constantly bumping the VM macOS version according to Xcode requirements.
Below is the table of required host versions for currently supported macOS VMs:
| Version | Guest macOS | Host macOS |
|---|---|---|
261102 | Tahoe 26.6.2 | Tahoe or later |
260723 | Tahoe 26.3.2 | Tahoe or later |
260303 | Sequoia 15.6.1 | Sequoia or later |
251015 | Sequoia 15.6.1 | Sequoia or later |
If you don't need the latest Xcode and you want to run an older version of the macOS VM image that supports running on an older host, contact us through our support channels.
Disk Space Requirements​
The recommended disk space for the macOS VM installation is 1TB.
If your host machine has a total storage capacity of 512GB, be aware that this will not be sufficient to download and extract the Appcircle runner macOS VM and the Xcode images compressed file directly on the host machine, especially with the currently listed versions. Although the extracted macOS VM and Xcode image files will fit within the 512GB capacity, the download and extraction process requires additional temporary space to process.
We recommend the following solutions for a possible workaround:
- Use an external disk formatted in
ExFATorAPFS. Please note thatFAT32is not a suitable option because it has a maximum file size limit of 4GB. - Use a network-based file system such as
SMBorNFS.SMBis recommended as it is a more compatible option with macOS thanNFS.
You can use the following commands in macOS Terminal to verify your total disk space.
df -h
system_profiler SPNVMeDataType
Please don't forget to change the working directory to the externally attached or network-based disk and run the macOS VM installation commands in that directory.
cd /path/where/the/external/disk/mounted
Note: You can check the integrity of downloaded file by comparing the MD5 checksum.
- 261102
- 260723
- 260303
- 251015
md5 macOS_261102.tar.gz
md5 macOS_260723.tar.gz
md5 macOS_260303.tar.gz
md5 macOS_251015.tar.gz
After a couple of minutes later you should see the output below.
- 261102
- 260723
- 260303
- 251015
MD5 (macOS_261102.tar.gz) = 2c4f8732a24b6ff4b1064c21d980703b
MD5 (macOS_260723.tar.gz) = 4fc936c6859b05db0fcb0f270875e329
MD5 (macOS_260303.tar.gz) = 425ad8bff9189a156d09b308cebd54a3
MD5 (macOS_251015.tar.gz) = cad8bcb22c5f7207fcbb25e5b78a6a88
Create folder for VM.
- 261102
- 260723
- 260303
- 251015
mkdir -p $HOME/.tart/vms/macOS_261102
mkdir -p $HOME/.tart/vms/macOS_260723
mkdir -p $HOME/.tart/vms/macOS_260303
mkdir -p $HOME/.tart/vms/macOS_251015
Extract archive into VMs folder.
- 261102
- 260723
- 260303
- 251015
pigz -cvdp 4 macOS_261102.tar.gz | tar -S -xvf - --directory $HOME/.tart/vms/macOS_261102
pigz -cvdp 4 macOS_260723.tar.gz | tar -S -xvf - --directory $HOME/.tart/vms/macOS_260723
pigz -cvdp 4 macOS_260303.tar.gz | tar -S -xvf - --directory $HOME/.tart/vms/macOS_260303
pigz -cvdp 4 macOS_251015.tar.gz | tar -S -xvf - --directory $HOME/.tart/vms/macOS_251015
It may take a little to complete. Be patient and wait return of command.
You can track progress of extraction by monitoring VM folder size.
- 261102
- 260723
- 260303
- 251015
du -sh $HOME/.tart/vms/macOS_261102
du -sh $HOME/.tart/vms/macOS_260723
du -sh $HOME/.tart/vms/macOS_260303
du -sh $HOME/.tart/vms/macOS_251015
Download Xcode Images Manually​
Download the Xcode images. They are disk images for each Xcode version archived in a package.
- 261102
- 260723
- 260303
- 251015
curl -L -O -C - "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/xcodes_261102.tar.gz"
curl -L -O -C - "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/xcodes_260723.tar.gz"
curl -L -O -C - "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/xcodes_260303.tar.gz"
curl -L -O -C - "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/xcodes_251015.tar.gz"
If you encounter network interruption, just run the same command again. It should continue download for remaining part. It will result in saving both time and bandwidth.
Note: You can check the integrity of downloaded file by comparing the MD5 checksum.
- 261102
- 260723
- 260303
- 251015
md5 xcodes_261102.tar.gz
md5 xcodes_260723.tar.gz
md5 xcodes_260303.tar.gz
md5 xcodes_251015.tar.gz
After a couple of minutes later you should see the output below.
- 261102
- 260723
- 260303
- 251015
MD5 (xcodes_261102.tar.gz) = 753c5f7fab1d97641ce37f39965bbfa0
MD5 (xcodes_260723.tar.gz) = 2c800dfc958bde8b92dd7090a6db08f1
MD5 (xcodes_260303.tar.gz) = fdccb3a8611306932248556a763a0e94
MD5 (xcodes_251015.tar.gz) = ee09cc713cbfaf276dbc9008fad403c0
Create folder for the Xcode disk images.
mkdir -p $HOME/images
Extract archive into the folder.
- 261102
- 260723
- 260303
- 251015
pigz -cvdp 4 xcodes_261102.tar.gz | tar -S -xvf - --directory $HOME/images
pigz -cvdp 4 xcodes_260723.tar.gz | tar -S -xvf - --directory $HOME/images
pigz -cvdp 4 xcodes_260303.tar.gz | tar -S -xvf - --directory $HOME/images
pigz -cvdp 4 xcodes_251015.tar.gz | tar -S -xvf - --directory $HOME/images
It may take a little to complete. Be patient and wait return of command.
- 261102
- 260723
- 260303
- 251015
Note: This macOS VM image is the Tahoe (26.6.2) stack and comes with the Xcode versions below:
| Version | Build |
|---|---|
| 27.2 | 27B52 |
| 27.1 | 27A9275 |
| 27.0 | 27A266a |
| 26.6 | 17F113 |
This image replaces 261016 (same Tahoe 26.6.2 base, Xcode 27.2 added). If you are on 261016 or 260914, Xcode 26.6, 27.0 and 27.1 are unchanged, so builds pinned to any of them keep working after the upgrade. Xcode 27.1 is required for iPhone Duo targets.
The image comes with Appcircle runner 1.9.3 installed. Use run.sh 1.11.0 with it (the download commands below fetch it): it mounts the Xcode 27.2 disk, which older run.sh versions do not.
Compared to the Tahoe 26.3.2 line (260723 and earlier), this image ships a different Xcode set (26.6, 27.0, 27.1 and 27.2). If your builds are pinned to Xcode 26.3, 26.4 or 26.5, move them to 26.6, 27.0, 27.1 or 27.2 before upgrading. Xcode 26.6 is available on both image lines, so it is the safe version to migrate on.
Note: This macOS VM image is the Tahoe (26.3.2) stack and comes with the Xcode versions below:
| Version | Build |
|---|---|
| 26.6 | 17F113 |
| 26.5 | 17F42 |
| 26.4.1 | 17E202 |
| 26.3 | 17C529 |
Note: This macOS VM image is the Sequoia (15.6.1) stack and comes with the Xcode versions below:
| Version | Build |
|---|---|
| 26.3 | 17C529 |
| 26.2 | 17C52 |
| 26.1.1 | 17B100 |
| 26.0.1 | 17A400 |
| 16.4 | 16F6 |
Note: This macOS VM image is the Sequoia (15.6.1) stack and comes with the Xcode versions below:
| Version | Build |
|---|---|
| 26.0.1 | 17A400 |
| 16.4 | 16F6 |
| 16.3 | 16E140 |
| 16.2 | 16C5032a |
| 16.1 | 16B40 |
| 16.0 | 16A242d |
In order to keep free disk space sufficient for build pipelines, we're packaging the latest and most frequently used Xcode versions. But you can also install other Xcode versions manually on your base VMs if required. See Update Base Images.
You can find more information about the build infrastructure in the documents below:
Download the macOS VM and Xcode Images Automatically​
To download and extract the Appcircle runner VM and Xcode images in the background automatically, you can run the command below.
- 261102
- 260723
- 260303
- 251015
curl -fsSL "${AC_RUNNER_CDN_BASE_URL:-https://cdn.appcircle.io/self-hosted}/download-runner.sh" -o download-runner.sh && \
chmod +x download-runner.sh && \
nohup ./download-runner.sh "261102" &
curl -fsSL "${AC_RUNNER_CDN_BASE_URL:-https://cdn.appcircle.io/self-hosted}/download-runner.sh" -o download-runner.sh && \
chmod +x download-runner.sh && \
nohup ./download-runner.sh "260723" &
curl -fsSL "${AC_RUNNER_CDN_BASE_URL:-https://cdn.appcircle.io/self-hosted}/download-runner.sh" -o download-runner.sh && \
chmod +x download-runner.sh && \
nohup ./download-runner.sh "260303" &
curl -fsSL "${AC_RUNNER_CDN_BASE_URL:-https://cdn.appcircle.io/self-hosted}/download-runner.sh" -o download-runner.sh && \
chmod +x download-runner.sh && \
nohup ./download-runner.sh "251015" &
If you face any errors while downloading the files, please delete the corrupted file and re-run the command block above.
Host macOS Requirements​
We're constantly bumping the VM macOS version according to Xcode requirements.
Below is the table of required host versions for currently supported macOS VMs:
| Version | Guest macOS | Host macOS |
|---|---|---|
261102 | Tahoe 26.6.2 | Tahoe or later |
260723 | Tahoe 26.3.2 | Tahoe or later |
260303 | Sequoia 15.6.1 | Sequoia or later |
251015 | Sequoia 15.6.1 | Sequoia or later |
If you don't need the latest Xcode and you want to run an older version of the macOS VM image that supports running on an older host, contact us through our support channels.
Disk Space Requirements​
The recommended disk space for the macOS VM installation is 1TB.
If your host machine has a total storage capacity of 512GB, be aware that this will not be sufficient to download and extract the Appcircle runner macOS VM and the Xcode images compressed file directly on the host machine, especially with the currently listed versions. Although the extracted macOS VM and Xcode image files will fit within the 512GB capacity, the download and extraction process requires additional temporary space to process.
We recommend the following solutions for a possible workaround:
- Use an external disk formatted in
ExFATorAPFS. Please note thatFAT32is not a suitable option because it has a maximum file size limit of 4GB. - Use a network-based file system such as
SMBorNFS.SMBis recommended as it is a more compatible option with macOS thanNFS.
You can use the following commands in macOS Terminal to verify your total disk space.
df -h
system_profiler SPNVMeDataType
Please don't forget to change the working directory to the externally attached or network-based disk and run the macOS VM installation commands in that directory.
cd /path/where/the/external/disk/mounted
It may take some time to complete with respect to your network speed. You can see and follow the logs with the command below.
tail -f nohup.out
You can close the SSH or terminal session while the tool is running. The download and extract process will go on in the background.
But be aware that there might be some errors while downloading and extracting the VM image, such as network or disk errors. Please keep an eye on the logs.
If no specific image identifier is provided when executing the download-runner.sh tool, it will automatically attempt to download the most recent runner images.
If you chose the artifact proxy as the download source, the tool downloads the images through the proxy too. Always give the image identifier in that case, as in the commands above. Finding the most recent image works only with the Appcircle download location.
Create Base Images​
Create Base Runner VMs​
Apple's virtualization framework allows us to run up to two macOS VMs on host.
If you have installed the macOS VM image previously and you're currently trying to upgrade your self-hosted runner environment to another release, first stop the runners if they're online.
Since the below steps will create new vm01 and vm02 from the base image, you should also cleanup the current ones using tart delete command.
tart delete vm01
tart delete vm02
Each runner must register to the self-hosted Appcircle server with a unique name and configuration. So we will need two VM base images.
When you list VMs with tart list command, you should see our extracted VM image in list.
In the steps below, we will create 2 base images named vm01 and vm02.
The vm01 base image is derived from our base image, and subsequently, the vm02 base image will be created from the vm01 base image.
This approach eliminates the need to redo all the configurations applied to vm01 when setting up vm02, ensuring efficiency and consistency across both virtual machines.
Create VM image for runner1.
- 261102
- 260723
- 260303
- 251015
tart clone macOS_261102 vm01
tart clone macOS_260723 vm01
tart clone macOS_260303 vm01
tart clone macOS_251015 vm01
It's not recommended to delete the base image (macOS_YY0M0D) as it won't save disk space due to copy-on-write file system on macOS. You can safely re-create vm01 from the same base image macOS_YY0M0D without downloading and extracting again from network if needed.
In docker terminology, vm01 and vm02 will be our docker images. We will configure them separately, persist our changes and then create containers to execute build pipelines. On every build, fresh containers will be used for both runners.
Configure Runner VM Resources​
You can adjust the resource limits for runner VMs based on your needs.
By default, our runner images are configured with an 8GB memory limit and 4 CPU cores.
Total allocated resources (memory, CPU) for all VMs combined, should not exceed host machine's physical capacity for optimal performance.
Set Memory Limits​
To configure the memory limit for a VM, run the following command:
tart set <vm_name> --memory <size_in_mb>
Replace vm_name with your VM's name and size_in_mb with the desired memory size in MB. E.g.,
tart set vm01 --memory 8192(8GB)
tart set vm01 --memory 16384(16GB)
Example configurations:
| Host Memory Size | Runner Memory Config. |
|---|---|
| 8GB | 1 x VM with 8GB |
| 16GB | 2 x VM with 8GB |
| 16GB | 1 x VM with 16GB |
| 32GB | 2 x VM with 16GB |
Set CPU Limits​
To configure the number of CPU cores for a VM, run the following command:
tart set <vm_name> --cpu <count>
Replace vm_name with your VM's name and count with the desired number of CPU cores. E.g.,
tart set vm01 --cpu 4
tart set vm01 --cpu 8
To check the total number of CPU cores on your system, use the following command:
sysctl -n hw.ncpu
If you have an 8 core CPU according to the command output, you can run 2 VMs and allocate half of the cores to each one, which means 4 cores per VM. Or you can run a single VM using all 8 cores.
Example configurations:
| Host CPU Cores | Runner CPU Config. |
|---|---|
| 8 | 2 x VM with 4 Cores |
| 8 | 1 x VM with 8 Cores |
Mac devices that have M-series chips (Apple silicon) have two types of CPU cores that you should take into consideration when you configure CPU for the VMs. See the FAQ below for details.
Backing Up and Restoring VM Configuration​
When recreating VMs or upgrading to a new image, it's important to preserve your custom settings.
-
Before making changes, backup your base VM's configuration file at
~/.tart/vms/<vm_name>/config.json. -
After recreating or upgrading the VM, you can either:
- Restore the entire configuration:
cp /path/to/your/backup/config.json ~/.tart/vms/<vm_name>/config.json
- Or, manually set the values again using the
tart setcommands.
- Restore the entire configuration:
Configure Base Runner VMs​
Be cautious when updating the base VMs (vm01 and vm02). Any changes made on these base VMs are persisted and may affect disk usage, keychain, and cache files on the runner VMs created from them.
If you're freshly creating the base VMs, you can ignore this warning. However, if you've already registered runners to your Appcircle server and want to make updates to the base VMs, it's highly recommended to disable the runner from the Appcircle dashboard to prevent builds from running on the base VMs.
Configure Runner 1​
Start runner1 VM image for configuration.
- 261102
- 260723
- 260303
- 251015
screen -d -m tart run vm01 --no-graphics \
--disk=$HOME/images/xcode.26.6.dmg:ro \
--disk=$HOME/images/xcode.27.0.dmg:ro \
--disk=$HOME/images/xcode.27.1.dmg:ro \
--disk=$HOME/images/xcode.27.2.dmg:ro
screen -d -m tart run vm01 --no-graphics \
--disk=$HOME/images/xcode.26.3.dmg:ro \
--disk=$HOME/images/xcode.26.4.dmg:ro \
--disk=$HOME/images/xcode.26.5.dmg:ro \
--disk=$HOME/images/xcode.26.6.dmg:ro
screen -d -m tart run vm01 --no-graphics \
--disk=$HOME/images/xcode.16.4.dmg:ro \
--disk=$HOME/images/xcode.26.0.dmg:ro \
--disk=$HOME/images/xcode.26.1.dmg:ro \
--disk=$HOME/images/xcode.26.2.dmg:ro \
--disk=$HOME/images/xcode.26.3.dmg:ro
screen -d -m tart run vm01 --no-graphics \
--disk=$HOME/images/xcode.16.0.dmg:ro \
--disk=$HOME/images/xcode.16.1.dmg:ro \
--disk=$HOME/images/xcode.16.2.dmg:ro \
--disk=$HOME/images/xcode.16.3.dmg:ro \
--disk=$HOME/images/xcode.16.4.dmg:ro \
--disk=$HOME/images/xcode.26.0.dmg:ro
SSH login into running macOS VM.
ssh -o StrictHostKeyChecking=no appcircle@$(tart ip vm01)
Note: You should use "cicd" as SSH login password.
While trying to connect VM you can get an SSH connection error as below.
ssh: Could not resolve hostname err: nodename nor servname provided, or not known
Wait a couple of seconds and let the VM start its internal services. You can try the same command until you connect successfully.
Since the VM IPs are rotating, it's possible to see the below error when you try to connect to the VM in the long term.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:f6CfksJoc0/ZIqItwH5IJDN87SP6RiOo9q1irzDxawU.
Please contact your system administrator.
Add correct host key in /Users/appcircle/.ssh/known_hosts to get rid of this message.
Offending ED25519 key in /Users/appcircle/.ssh/known_hosts:5
Password authentication is disabled to avoid man-in-the-middle attacks.
Keyboard-interactive authentication is disabled to avoid man-in-the-middle attacks.
UpdateHostkeys is disabled because the host key is not trusted.
appcircle@192.168.64.2: Permission denied (publickey,password,keyboard-interactive).
The above example error message indicates that there is an entry for the server 192.168.64.2 located on line 5 of the known_hosts file that needs to be removed.
You can delete the old host key entry with the following command and then try re-connecting.
ssh-keygen -R $(tart ip vm01)
In the macOS VM, /Volumes/agent-disk/appcircle-runner is the root folder of runner.
cd /Volumes/agent-disk/appcircle-runner
So, the following commands will assume that current working directory is /Volumes/agent-disk/appcircle-runner.
1. Check the Runner Version​
You may have installed the latest Appcircle runner VM image, but the Appcircle runner may not be up-to-date.
You can follow the steps below to check the Appcircle runner version and upgrade if it isn't the latest version.
- Appcircle (internet or HTTP proxy)
- Artifact proxy (no internet access)
No extra action is needed.
The runner package is downloaded through your artifact proxy too. Set the URL of the appcircle-cdn repository on your artifact proxy inside the VM as well. The base VM keeps it for every runner VM created from it.
echo 'export AC_RUNNER_CDN_BASE_URL="https://nexus.example.com/repository/appcircle-cdn"' >> ~/.zprofile
source ~/.zprofile
If your artifact proxy requires a username and password to read its repositories, run the ~/.netrc and ~/.curlrc commands from Choose the Download Source inside the VM as well. Use an account with read access only, because every build runs on a clone of this VM and can read that file.
Workflow steps come with the runner package. Update the runner to the latest version after every Appcircle server upgrade, so that it includes the step versions your server offers. A step version that isn't in the runner package is downloaded from GitHub during the build and fails without internet access.
- Check the version of the current installation.
./ac-runner --version
-
Check the latest version from the Upgrade Runner page.
-
If your version is not up to date, please follow the Update Runner section in the page.
You should run the curl and unzip commands on the /Volumes/agent-disk/ path.
Since you're in the appcircle-runner directory now, please change the directory one level up.
You don't need to apply the Reconfigure Runner section. But the Reinstall Service section is necessary since the latest version may also have some service updates.
When you complete update successfully, you should see the updated version in --version output.
Go to the appcircle-runner directory.
cd appcircle-runner
Check the version of the runner.
./ac-runner --version
2. Configure Base Runner's NTP Settings​
MacOS VMs try to update their date and time using the network time protocol (NTP) by default.
If your organization has limited network access for the Appcircle runner machine, the VM may be unable to reach the servers responsible for updating its date and time settings.
In a situation like that, your organization might have an NTP server for internal usage.
You can configure your macOS runner VM to use your organization's own NTP server.
You can use the helper script named configure_ntp.sh that comes with the runner package to configure the NTP settings.
To configure NTP settings:
-
The IP address or URL of the NTP server should be known.
-
Network access should be allowed from the Appcircle runner to the NTP server.
-
You will find a script named
configure_ntp.shin thescriptsfolder inside theappcircle-runnerdirectory. -
Run the script and give the NTP server IP (or URL) as an argument, like the example below:
./scripts/configure_ntp.sh "10.10.1.50"
You should change "10.10.1.50" to the NTP server address of your organization in the example above.
3. Trust The Root Certificates of Your Organization​
If the resources you want to connect use a self-signed certificate, you should also trust the root certificate of your organization in your Appcircle runner VMs. These resources can be:
- Git providers (GitLab, Bitbucket, Azure DevOps, etc.)
- Self-hosted Appcircle server
- Proxy server for network access
Trusting your organization's root certificate on the OS is crucial.
Because the runner will try to connect to these resources over HTTPS and the SSL certificate will be signed with your organization's root certificate.
Furthermore, if the runner attempts to access external web sites, the requests will most likely be intercepted by the proxy and re-signed with a self-issued certificate that is also signed by the root certificate.
You can use the helper script named install_cert.sh that comes with your runner package to configure the certificates.
-
You will find a script named
install_cert.shin thescriptsfolder inside theappcircle-runnerdirectory. -
Run the script with the host name of each resource the runner connects to:
./scripts/install_cert.sh --url api.appcircle.spacetech.com --url git.spacetech.com
- Your organization's root CA certificate is now trusted on macOS, Xcode, Git, Ruby, Java, and Node.js.
For more detailed usage, you can check the Self-signed Certificates page.
4. Point Build Tools to Your Artifact Proxy​
Builds download their dependencies, such as npm packages, Ruby gems, CocoaPods and Gradle dependencies, while they run.
- Appcircle (internet or HTTP proxy)
- Artifact proxy (no internet access)
No action is needed. Builds download their dependencies from the public registries.
Point the build tools at your artifact proxy in the base VM. Every runner VM is cloned from this base VM, so the settings apply to every build. Repeat this step after every macOS VM image upgrade, because a new image starts without them.
Ask your artifact proxy administrator for proxy repositories of the package registries your projects use:
| Registry | Sonatype Nexus type | JFrog Artifactory type | Remote URL |
|---|---|---|---|
| npm (npm, Yarn 1) | npm (proxy) | npm remote | https://registry.npmjs.org |
| RubyGems (CocoaPods, Fastlane, Bundler) | rubygems (proxy) | Gems remote | https://rubygems.org |
| PyPI | pypi (proxy) | PyPI remote | https://pypi.org |
| Maven (Google, Maven Central, Gradle plugins) | maven2 (proxy) per remote, combined in a maven2 (group) | Maven remote per remote, combined in a Maven virtual | https://maven.google.com, https://repo1.maven.org/maven2, https://plugins.gradle.org/m2 |
| Gradle distributions | raw (proxy) | Generic remote | https://services.gradle.org/distributions |
| CocoaPods | cocoapods (proxy) | CocoaPods remote | https://cdn.cocoapods.org |
| Dart and Flutter packages | pub (proxy), Nexus 3.92.0 or later | Pub remote | https://pub.dev |
| Flutter SDK artifacts | raw (proxy) | Generic remote | https://storage.googleapis.com |
| Node.js distributions | raw (proxy) | Generic remote | https://nodejs.org/dist |
Notes for the artifact proxy administrator
- Several registries redirect to another host, so the proxy must follow redirects and reach the target host too: Gradle distributions redirect to
github.comandrelease-assets.githubusercontent.com, CocoaPods specs tocdn.jsdelivr.net,maven.google.comtodl.google.com, andplugins.gradle.orgtoplugins-artifacts.gradle.orgorrepo.maven.apache.org. - The full list of hosts the proxy needs is on the Network Access page.
Run the commands below inside the VM. They cover the tools that workflow steps and your builds use outside the project configuration. Replace https://nexus.example.com/repository and the repository names with your own, and skip the tools your projects don't use. The settings are written to files in the home folder of the appcircle user, which builds also read when a step runs a command with sudo.
PROXY="https://nexus.example.com/repository"
# npm and Yarn 1
npm config set registry "$PROXY/npm/"
# RubyGems and Bundler (CocoaPods, Fastlane)
gem sources --add "$PROXY/rubygems/" --remove "https://rubygems.org/"
bundle config set --global mirror.https://rubygems.org "$PROXY/rubygems/"
# pip
pip config set global.index-url "$PROXY/pypi/simple"
# Homebrew analytics
brew analytics off
Some tools read their settings only from environment variables. The runner service loads the ~/.zshrc file of the appcircle user when it starts, so add the variables there. Skip the lines of the tools your projects don't use.
cat >> ~/.zshrc <<'ZSHRC'
# Package mirrors
export PUB_HOSTED_URL="https://nexus.example.com/repository/pub"
export FLUTTER_STORAGE_BASE_URL="https://nexus.example.com/repository/flutter-storage"
export N_NODE_MIRROR="https://nexus.example.com/repository/nodejs-dist"
# Update checks and usage reports
export FASTLANE_SKIP_UPDATE_CHECK=1
export FASTLANE_OPT_OUT_USAGE=1
export TUIST_STATS_OPT_OUT=1
export SNYK_DISABLE_ANALYTICS=1
ZSHRC
The first three variables point Dart and Flutter packages, Flutter SDK artifacts and the Node.js versions installed with n to the proxy. Without the last four, Fastlane, Tuist and Snyk still work, but each build waits for their update checks and usage reports to time out.
Commands that a step runs with sudo don't get these variables. The files written above are still read, because sudo keeps the home folder of the appcircle user.
Some settings live in your projects, not in the VM. Set them in every project that builds on these runners. Without them, the build tries the public addresses and fails, because the runner can't reach them.
Gradle repositories: in settings.gradle or settings.gradle.kts, replace the repositories in pluginManagement and dependencyResolutionManagement, such as google(), mavenCentral() and gradlePluginPortal(), with your Maven group repository. If your project declares its repositories in build.gradle instead, change them there.
pluginManagement {
repositories {
maven { url = uri("https://nexus.example.com/repository/maven-public/") }
}
}
dependencyResolutionManagement {
repositories {
maven { url = uri("https://nexus.example.com/repository/maven-public/") }
}
}
Gradle wrapper: point distributionUrl in gradle/wrapper/gradle-wrapper.properties to your Gradle distributions repository, for example distributionUrl=https\://nexus.example.com/repository/gradle-dist/gradle-8.9-bin.zip.
CocoaPods: add your CocoaPods repository as the first line of the Podfile, for example source 'https://nexus.example.com/repository/cocoapods/'.
Android SDK packages: if your Android projects need an SDK package that the image doesn't contain, such as another NDK version, install it into the base VM from your artifact proxy. See Android SDK Packages.
5. Configure Runner Service​
Note: Runner logs are kept in $HOME/appcircle-runner folder.
Stop runner service.
./ac-runner service -c stop
Edit appsettings.json with your favorite editor. (nano, vi etc.)
{
...
"ASPNETCORE_NOSHUTDOWN": "false",
...
"ASPNETCORE_REDIS_STREAM_ENDPOINT": "redis.appcircle.spacetech.com:443,ssl=true",
...
"ASPNETCORE_BASE_API_URL": "https://api.appcircle.spacetech.com/build/v1"
}
ASPNETCORE_NOSHUTDOWN: It should befalse. So, it will shutdown VM when build complete.ASPNETCORE_BASE_API_URL: It should be your self-hosted Appcircle server API URL.- The runner will register to server defined here and take the build jobs from there.
All listed macOS VM images have the ASPNETCORE_NOSHUTDOWN setting as false by default and have no pre-defined ASPNETCORE_BASE_API_URL setting in the appsettings.json file.
So, if you did not upgrade the packaged self-hosted runner at previous steps above, only modifying the ASPNETCORE_BASE_API_URL value with the following command should be enough for the configuration up-to-here.
echo "$(jq '.ASPNETCORE_BASE_API_URL="https://api.appcircle.spacetech.com/build/v1"' appsettings.json)" > appsettings.json
If you upgraded the self-hosted runner, you must also modify the ASPNETCORE_NOSHUTDOWN setting as well.
echo "$(jq '.ASPNETCORE_NOSHUTDOWN="false"' appsettings.json)" > appsettings.json
ASPNETCORE_REDIS_STREAM_ENDPOINT: It should be your self-hosted Appcircle server's Redis URL, port, and SSLÂ settings.- If you are using the Appcircle server with HTTPS, then the port should be
443and thesslargument should be set totrue. - If you are using the Appcircle server with HTTP, then the port should be the external port of Redis which is
6379by default. And thesslargument should be set tofalse.- For instance,
redis.appcircle.spacetech.com:6379,ssl=false.
- For instance,
- If you are using the Appcircle server with HTTPS, then the port should be
Using an Up-to-Date Runner with an Out-of-Date Server​
By default, the latest runner (1.7.0 or later) is compatible with the Appcircle server 3.18.0 or later. For this reason, if you are installing the latest runner or upgrading the runner from older versions and using an older server, you need to make extra configurations.
To make the Appcircle runner compatible with an older server, you must change the two settings below in the Appcircle runner appsettings.json, which will make it compatible with older servers.
echo "$(jq '.ASPNETCORE_SEND_BUILD_LOG_TO_REDIS="false"' appsettings.json)" > appsettings.json && \
echo "$(jq '.ASPNETCORE_WORKFLOW_UPDATES_DISABLED="false"' appsettings.json)" > appsettings.json
Create runner access token from appcircle server and register runner to server. See details in here.
For example,
./ac-runner register -t aat_eev4NQdG_7F2jodmMShBFhh_DgabOJSsWSMojX5_lo4 -n runner1 -p macOS_pool
It won't print anything to CLI on success. You can also check its exit value with echo $?. It should be 0 on success.
Finally run below command to edit self-hosted runner configuration for pre-installed platforms.
echo "$(jq '.OsValues = ["ios","android"]' selfHosted.json)" > selfHosted.json
Start runner service.
./ac-runner service -c start
Now you should see "runner1" in "Build > Self-hosted Runners" list. It may take a couple of seconds to become online.
If "runner1" is online, we can shutdown VM since configuration is done with success.
sudo shutdown -h now
Configure Runner 2​
As we configured the runner1 (vm01), we can clone vm01 to vm02.
So we won't need to reconfigure NTP settings, self-signed SSL certificates, or other configurations that we made for vm01.
Create VM image for runner2 from runner1.
tart clone vm01 vm02
Start runner2 image for configuration.
- 261102
- 260723
- 260303
- 251015
screen -d -m tart run vm02 --no-graphics \
--disk=$HOME/images/xcode.26.6.dmg:ro \
--disk=$HOME/images/xcode.27.0.dmg:ro \
--disk=$HOME/images/xcode.27.1.dmg:ro \
--disk=$HOME/images/xcode.27.2.dmg:ro
screen -d -m tart run vm02 --no-graphics \
--disk=$HOME/images/xcode.26.3.dmg:ro \
--disk=$HOME/images/xcode.26.4.dmg:ro \
--disk=$HOME/images/xcode.26.5.dmg:ro \
--disk=$HOME/images/xcode.26.6.dmg:ro
screen -d -m tart run vm02 --no-graphics \
--disk=$HOME/images/xcode.16.4.dmg:ro \
--disk=$HOME/images/xcode.26.0.dmg:ro \
--disk=$HOME/images/xcode.26.1.dmg:ro \
--disk=$HOME/images/xcode.26.2.dmg:ro \
--disk=$HOME/images/xcode.26.3.dmg:ro
screen -d -m tart run vm02 --no-graphics \
--disk=$HOME/images/xcode.16.0.dmg:ro \
--disk=$HOME/images/xcode.16.1.dmg:ro \
--disk=$HOME/images/xcode.16.2.dmg:ro \
--disk=$HOME/images/xcode.16.3.dmg:ro \
--disk=$HOME/images/xcode.16.4.dmg:ro \
--disk=$HOME/images/xcode.26.0.dmg:ro
SSH login into running macOS VM.
ssh -o StrictHostKeyChecking=no appcircle@$(tart ip vm02)
After login, configuration steps for Appcircle runner service are the same as "runner1". So, we don't need to repeat same commands again.
The only difference should be runner naming. It must be unique. For the second runner, just give a different name. For example, "runner2".
Refer to the Configure Runner Service for detailed Appcircle runner service configuration.
After shutdown, we're ready to run instances from vm01 and vm02 base VM images.
At this stage, your VM list returned by tart list might be like below, according to your preferred macOS VM image version.
Source Name Size
local macOS_261102 167
local vm01 130
local vm02 130
Operating MacOS VMs​
Prerequisites​
We need to create two separate folders for two runners. These will be their working directories on runtime.
Create folder for "runner1".
mkdir $HOME/runner1
Create folder for "runner2".
mkdir $HOME/runner2
We have a simple bash script that will be used to run VM instances in loop.
Download the script into runner folders you created and make script executable.
For "runner1" use below commands.
- 261102
- 260723
- 260303
- 251015
curl -L -o $HOME/runner1/run.sh "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/run-1.11.0.sh" && \
chmod u+x $HOME/runner1/run.sh
curl -L -o $HOME/runner1/run.sh "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/run-1.6.0.sh" && \
chmod u+x $HOME/runner1/run.sh
curl -L -o $HOME/runner1/run.sh "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/run-1.3.0.sh" && \
chmod u+x $HOME/runner1/run.sh
curl -L -o $HOME/runner1/run.sh "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/run-1.0.9.sh" && \
chmod u+x $HOME/runner1/run.sh
For "runner2" use below commands.
- 261102
- 260723
- 260303
- 251015
curl -L -o $HOME/runner2/run.sh "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/run-1.11.0.sh" && \
chmod u+x $HOME/runner2/run.sh
curl -L -o $HOME/runner2/run.sh "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/run-1.6.0.sh" && \
chmod u+x $HOME/runner2/run.sh
curl -L -o $HOME/runner2/run.sh "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/run-1.3.0.sh" && \
chmod u+x $HOME/runner2/run.sh
curl -L -o $HOME/runner2/run.sh "${AC_RUNNER_IMAGE_BASE_URL:-https://storage.googleapis.com/appcircle-dev-common/self-hosted}/run-1.0.9.sh" && \
chmod u+x $HOME/runner2/run.sh
With new versions of the macOS VM image, we're also constantly updating the run.sh tool for fixes and improvements.
So, there might be a new version of run.sh that's compatible with the latest macOS VM image.
The above commands should be executed on every macOS VM image upgrade in order to get the latest run.sh version that's compatible with the latest macOS VM image.
Start VM​
The commands in this section start a runner inside your current SSH session. The runner stops when that session is closed, and it does not come back after a host reboot.
For a host that should run unattended, install the runner as a launchd service instead of using screen. See Running macOS VM Runners as a Service.
In order to start "runner1", use below command.
screen -d -m $HOME/runner1/run.sh vm01
It will start "runner1" in detached mode and will manage VM lifecycle continuously.
In a couple of minutes, you should see "runner1" as online in self-hosted runners list.
We can also start "runner2" with below command.
screen -d -m $HOME/runner2/run.sh vm02
In docker terminology, we're creating container from docker image at this stage. On build complete, runner will be shutdown automatically and run.sh will create a fresh one from macOS image. It continuously does the same operation until stopped.
We can see running instances on macOS host with tart list.
Source Name Size
local macOS_261102 167
local vm01 130
local vm01-4f496549-cfe8-462c-ba55-774f01c03b4f 130
local vm02 130
local vm02-9f1fc62a-f43c-40f3-98d0-523ed9a67042 130
As you can see in list, we have new VMs with long unique name. Those are actually online VMs that we started.
vm01 and vm02 are immutable VM images. On the other hand, others are instances created from VM images.
Stop VM​
If the runner is installed as a launchd service, stop it with sudo ./runner-service.sh stop instead of the steps below. Creating a .stop file on its own is not enough under the service, because run.sh clears that file when the service starts it again. See Running macOS VM Runners as a Service.
In order to stop VM, we need to mark runner as stopped and shutdown online runner over SSH.
Touch .stop file at runner's working directory.
touch $HOME/runner1/.stop
Creating .stop file prevents creating new instance by run.sh on shutdown.
If runner is executing build pipeline, you may prefer waiting completion of the build job. See stop section at self-hosted runner docs. When executing build pipeline completes, runner will be shutdown automatically.
On the other hand if you want to stop runner immediately for whatever reason or it's in idle state, you can SSH into runner and run shutdown command.
First you need to have to find out online runner's VM name from tart list.
Source Name Size
local macOS_261102 167
local vm01 130
local vm01-4f496549-cfe8-462c-ba55-774f01c03b4f 130
local vm02 130
local vm02-9f1fc62a-f43c-40f3-98d0-523ed9a67042 130
In above list, "vm01-4f496549-cfe8-462c-ba55-774f01c03b4f" is the name of runner that we will shutdown.
SSH login into that runner.
ssh -o StrictHostKeyChecking=no appcircle@$(tart ip vm01-4f496549-cfe8-462c-ba55-774f01c03b4f)
Note: You should use "cicd" as SSH login password.
Execute shutdown command.
sudo shutdown -h now
Shutdown process may take a couple of seconds.
After shutdown, you won't see anymore instance from vm01 on tart list.
Source Name Size
local macOS_261102 167
local vm01 130
local vm02 130
local vm02-9f1fc62a-f43c-40f3-98d0-523ed9a67042 130
After a couple of minutes later, "runner1" will also become offline at self-hosted runners list.
Steps are also similar for "runner2". You need to touch .stop file in its working directory and after getting its VM name from tart list, you can shutdown "runner2" over SSH.
Update Base Images​
On some cases, you may need to update to your macOS base images in order to make your changes permanent.
Below are the ones that frequently occur, but not limited to them.
- Your team might use a tool frequently in build pipeline, that's not included in Appcircle macOS image. Installing that tool into the image once will save build time. Your build pipeline will be more efficient and optimized.
- You may prefer to update the Appcircle runner by using the self-hosted runner update method instead of getting a fresh macOS VM image. When you get a fresh macOS image you may need to make your custom configurations again. Note that Xcode versions are not updated this way; newer Xcode versions come with newer macOS VM image releases, or you can install them manually on your base VMs.
- You may need to make persistent proxy configuration for your internal network requirements.
- You may need to add your corporate's self-signed root CAs to macOS VM image in order to succeed SSL connections.
Steps, that we need to take, are technically similar as in Create Base Images section. So, a conceptual overview of the steps will be sufficient.
- Stop all online runners as explained in Stop VM section.
- Run
vm01base image.
- 261102
- 260723
- 260303
- 251015
screen -d -m tart run vm01 --no-graphics \
--disk=$HOME/images/xcode.26.6.dmg:ro \
--disk=$HOME/images/xcode.27.0.dmg:ro \
--disk=$HOME/images/xcode.27.1.dmg:ro \
--disk=$HOME/images/xcode.27.2.dmg:ro
screen -d -m tart run vm01 --no-graphics \
--disk=$HOME/images/xcode.26.3.dmg:ro \
--disk=$HOME/images/xcode.26.4.dmg:ro \
--disk=$HOME/images/xcode.26.5.dmg:ro \
--disk=$HOME/images/xcode.26.6.dmg:ro
screen -d -m tart run vm01 --no-graphics \
--disk=$HOME/images/xcode.16.4.dmg:ro \
--disk=$HOME/images/xcode.26.0.dmg:ro \
--disk=$HOME/images/xcode.26.1.dmg:ro \
--disk=$HOME/images/xcode.26.2.dmg:ro \
--disk=$HOME/images/xcode.26.3.dmg:ro
screen -d -m tart run vm01 --no-graphics \
--disk=$HOME/images/xcode.16.0.dmg:ro \
--disk=$HOME/images/xcode.16.1.dmg:ro \
--disk=$HOME/images/xcode.16.2.dmg:ro \
--disk=$HOME/images/xcode.16.3.dmg:ro \
--disk=$HOME/images/xcode.16.4.dmg:ro \
--disk=$HOME/images/xcode.26.0.dmg:ro
- SSH into
vm01.
ssh -o StrictHostKeyChecking=no appcircle@$(tart ip vm01)
- Make your modifications, configurations or updates in macOS.
- Shutdown
vm01. - Run
vm02base image.
- 261102
- 260723
- 260303
- 251015
screen -d -m tart run vm02 --no-graphics \
--disk=$HOME/images/xcode.26.6.dmg:ro \
--disk=$HOME/images/xcode.27.0.dmg:ro \
--disk=$HOME/images/xcode.27.1.dmg:ro \
--disk=$HOME/images/xcode.27.2.dmg:ro
screen -d -m tart run vm02 --no-graphics \
--disk=$HOME/images/xcode.26.3.dmg:ro \
--disk=$HOME/images/xcode.26.4.dmg:ro \
--disk=$HOME/images/xcode.26.5.dmg:ro \
--disk=$HOME/images/xcode.26.6.dmg:ro
screen -d -m tart run vm02 --no-graphics \
--disk=$HOME/images/xcode.16.4.dmg:ro \
--disk=$HOME/images/xcode.26.0.dmg:ro \
--disk=$HOME/images/xcode.26.1.dmg:ro \
--disk=$HOME/images/xcode.26.2.dmg:ro \
--disk=$HOME/images/xcode.26.3.dmg:ro
screen -d -m tart run vm02 --no-graphics \
--disk=$HOME/images/xcode.16.0.dmg:ro \
--disk=$HOME/images/xcode.16.1.dmg:ro \
--disk=$HOME/images/xcode.16.2.dmg:ro \
--disk=$HOME/images/xcode.16.3.dmg:ro \
--disk=$HOME/images/xcode.16.4.dmg:ro \
--disk=$HOME/images/xcode.26.0.dmg:ro
- SSH into
vm02.
ssh -o StrictHostKeyChecking=no appcircle@$(tart ip vm02)
- Make your modifications, configurations or updates in macOS.
- Shutdown
vm02. - Start offline runners as explained in Start VM section.
Troubleshooting​
Tart list has runner instance in list but I can not SSH into the runner​
Rarely your runner might hang or might become offline for some reason.
- You can get its IP with
tart ipbut might not connect. - Although it's in
tart listyou might not get its IP. - Detached
screensession might be terminated because of an error.
You need to check your macOS host for possible system issues. (disk space, network connectivity, OS reboot etc.)
You can also check runner's working directory for any error that might happen at virtualization. (stderr.log, stdout.log)
In this case, you can make your runners operational again by following below steps.
1- Make sure detached screen session and all its child processes terminated for the runner.
For instance, you can find list of PIDs for "runner2" as seen below.
% ps aux | grep vm02 | grep -v grep
appcircle 35640 0.0 0.1 408653856 18320 s002 S+ 2:35PM 0:00.68 tart run vm02-3a003bce-a2ee-4ae7-9500-7754e181c314 --no-graphics
appcircle 35174 0.0 0.0 408628512 2352 s002 S+ 2:20PM 0:00.01 bash /Users/appcircle/runner2/run.sh vm02
root 35173 0.0 0.0 408515456 6848 s002 Ss+ 2:20PM 0:00.01 login -pflq appcircle /Users/appcircle/runner2/run.sh vm02
appcircle 35172 0.0 0.0 408654848 1264 ?? Ss 2:20PM 0:00.00 SCREEN -d -m /Users/appcircle/runner2/run.sh vm02
2- Make sure there is no active virtualization framework process for the runner.
For instance, you can filter like this.
% ps aux | grep -i virtualmachine | grep -v grep
appcircle 35641 0.3 31.9 503225216 10699872 ?? Ss 2:35PM 42:07.38 /System/Library/Frameworks/Virtualization.framework/Versions/A/XPCServices/com.apple.Virtualization.VirtualMachine.xpc/Contents/MacOS/com.apple.Virtualization.VirtualMachine
3- Make sure there is no instance for the runner at VM list. If exists, remove it.
For instance, we have an instance for "runner2" seen below.
% tart list
Source Name Size
local macOS_261102 167
local vm01 130
local vm02 130
local vm02-3a003bce-a2ee-4ae7-9500-7754e181c314 130
Remove dangling "runner2".
tart delete vm02-3a003bce-a2ee-4ae7-9500-7754e181c314
4- Start VM for the runner as usual by following steps at start VM.
For instance, start "runner2" with command below.
screen -d -m $HOME/runner2/run.sh vm02
I can SSH into runner but runner is offline at self-hosted runners list​
In this case, you need to focus on self-hosted runner issues inside macOS VM (guest).
In order to be able to investigate root cause, you should learn the basics of self-hosted runner. Check our online docs details.
- You can check your macOS guest for possible system issues. (disk space, network connectivity etc.)
- If you have custom proxy settings on macOS guest, check these settings.
- If you added custom root CAs to macOS guest, verify their validity for SSL connection errors.
- You can check runner launchd service and its logs. (
stdout.log,stderr.log,logsetc.) - You should check network access from self-hosted runner to server. (firewall, open ports etc.)
Datetime I see in build logs is not correct​
This case is related with broken datetime synchronization between runner and server.
Both server and runner should synchronize their times with relevant NTP services. Timezone difference is not important. Datetime must be correct in their timezones.
If runner doesn't have network access to an NTP server on the internet, you can also configure it to use your internal NTP server.
For updating macOS base image see related section above.
For configuring NTP settings, see Configure Base Runner's NTP Settings section above.
I am facing "SSL cert is not valid yet" error in our builds​
This problem is again related to your macOS VM date and time being out of date.
To fix that, you should sync the VMs' date and time with your organization's NTP server.
For updating macOS base image see related section above.
For configuring NTP settings, see Configure Base Runner's NTP Settings section above.
Runners are offline and I noticed that macOS host has been reboot​
Runners started with screen do not survive a reboot. If you expect them to come back on their own, install them as a launchd service. See Running macOS VM Runners as a Service.
If the service is already installed and the runners still did not come back, the login keychain is the usual cause on macOS 15 and newer. See the troubleshooting section on that page.
As for the reboot itself, if there is no system crash, one reason for an unintentional reboot may be caused by automatic updates.
We suggest disabling automatic updates on macOS host, and get them manually when required.
Below are the CLI commands to disable all automatic updates on macOS.
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -bool false
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate CriticalUpdateInstall -int 0
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool false
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticallyInstallMacOSUpdates -int 0
sudo defaults write /Library/Preferences/com.apple.commerce AutoUpdate -int 0
Runners are offline but when I SSH into host, it suddenly becomes online​
When you install a fresh macOS on a mac device, it comes with predefined power settings which makes it energy efficient.
So, most probably, your macOS host sleeps when there is no UI interaction, and awakes on SSH login.
To do that, please configure your power settings on the host machine.
You can re-check the Configure Power Settings title for power management on the host.
I want to make some configurations to macOS base image but need desktop UI for them​
If you're not comfortable with CLI, you can also make your customizations using macOS desktop.
For this purpose, remove --no-graphics argument from tart run commands.
For example, the "Run vm01 base image" step in the update base images section starts the VM like this:
screen -d -m tart run vm01 --no-graphics --disk=...
To get the desktop UI, run it without --no-graphics (keep the same --disk arguments for your image version):
screen -d -m tart run vm01 --disk=...
Deleting Xcode simulator runtimes to create free disk space​
Occasionally, you may need to manage disk space on your macOS base VM due to storage constraints or other reasons. One way to free up disk space is by deleting unused Xcode simulator runtimes.
To list the installed Xcode simulator runtimes, run the following command on your base VM:
xcrun simctl runtime list 2>/dev/null
If you determine that certain iOS, watchOS, tvOS or visionOS(xrOS) runtimes are not needed, you can delete them to free up disk space:
xcrun simctl runtime delete <runtime_id>
Xcode simulator runtimes are essential for testing and debugging iOS, watchOS, tvOS, and visionOS applications on virtual devices. Deleting a runtime will prevent you from running or debugging an app on that specific device. Other simulators and runtimes will remain unaffected.
Be cautious when deleting Xcode simulator runtimes, as this action is irreversible. Removing a simulator runtime can impact the Xcode build process. For example, if you delete a watchOS runtime, you will no longer be able to build an iOS app that targets the deleted watchOS runtime. Ensure that the runtime you plan to delete is not required for your build pipeline.
Why do I have different build durations in the pipeline, especially when they run concurrently on the same Mac host?​
As we mentioned earlier in the documentation, you can add more than one runner on the same Mac host and run build pipelines concurrently. You can configure their CPU and memory resources as your needs and make different types of runner pools for different workloads.
Mac devices that have M-series chips (Apple silicon) have two types of CPU cores: E (efficiency) cores and P (performance) cores. For instance, an M1 Mac mini device has 4 E cores and 4 P cores.
You can see the number of cores and its distribution on your host using the command below:
system_profiler SPHardwareDataType
E (efficiency) cores are energy-efficient but slower cores that are significantly different than P (performance) cores. Apps cannot decide directly which cores they will be run on. For this reason, we cannot assign specific cores to our workload while configuring the macOS VM.
Under macOS lightweight virtualization, virtual machines (VMs) are allocated a number of virtual CPU cores, all of which are the same type. So, the virtual CPU cores in macOS VMs do not differentiate between the high-performance and high-efficiency cores of the host CPU. Instead, macOS VMs automatically alternate between these types of cores depending on the workload being executed within the virtual machines.
You can have two VMs running, and one can use all the performance cores and the other can use all the efficiency cores if the second workload is not CPU intensive. Or, they can share performance cores if both are CPU intensive, which results in some amount of workload being handled using efficiency cores for both of them, which will lead us to a 2x-3x slower build pipeline when they run concurrently.
You can analyze the CPU consumption grouped by CPU clusters by running the command below on the macOS host.
sudo powermetrics -s cpu_power
For this reason, if you need your build pipelines to be at peak performance in all conditions and do not want to see fluctuation in build pipeline durations, you should consider using one VM per one Mac device (host), especially for CPU-intensive CI workloads.
Using concurrency on the same Mac device (host) by configuring more than one VM will cause fluctuation in build pipeline durations. When one VM is idle, there will not be a significant degradation. But when both are in the "running" state, you can see significant divergence.
Build Cache​
When you hit the hardware barriers and try to find a way to improve build pipeline duration with existing runner configuration, you can consider using Build Cache.
Using build cache can optimize resource usage by reducing repetitive build tasks and reducing demand on CPU, memory, and bandwidth, which can result in significant performance improvement depending on your build workflow.
You can specifically cache the build and test outputs to minimize how much work is done in subsequent builds, which is expected to make the build pipeline more efficient, especially in repositories with frequent updates.