Issue #1002
I recently set up self-hosted runners on EC2 Mac instances for iOS builds. Here are my notes on what actually works—plus all the gotchas I hit along the way.
First, grab the runner package from your GitHub organization. For complete official instructions, check out GitHub’s guide on adding self-hosted runners.
Navigate to: Settings → Actions → Runners → New runner
Select macOS and arm64 (crucial if you’re using Apple Silicon). GitHub gives you a download link and a registration token—grab it quick, it expires in 1 hour.
Extract it somewhere clean:
mkdir actions-runner && cd actions-runner
curl -o actions-runner-osx-arm64-2.331.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.331.0/actions-runner-osx-arm64-2.331.0.tar.gz
tar xzf ./actions-runner-osx-arm64-2.331.0.tar.gz
After extraction, here’s what you’ll see in your actions-runner folder:
_diag/
_work/
actions-runner-osx-arm64-2.331.0.tar.gz
bin/
config.sh
env.sh
externals/
run-helper.cmd.template
run-helper.sh
run-helper.sh.template
run.sh
runsvc.sh
safe_sleep.sh
svc.sh // Appears after config.sh runs successfully
Key files to remember:
config.sh- Configure the runnerrun.sh- Run interactively (great for testing)svc.sh- Install as a servicerunsvc.sh- Start the service
Configuring the Runner
This is straightforward, but there are a few things to get right:
./config.sh --url https://github.com/your-org --token YOUR_TOKEN
You’ll be prompted for:
- Runner name: Something descriptive like
mac-m1-runner-1 - Runner group: Optional, but useful if you have multiple runners
- Labels: This is important—use labels like
ios-build,m1,self-hostedto target specific runners in your workflows
If you want to skip the prompts (useful for automation):
./config.sh --unattended --url https://github.com/your-org --token YOUR_TOKEN --name mac-runner-1 --labels ios-build
After config completes, check GitHub and you’ll see your runner registered—but it’ll be offline. That’s normal; we’ll fix that next.
Running as a Service
Here’s where things get interesting. On macOS, you want this running in the background, surviving reboots and all that.
GitHub’s documentation suggests using ./svc.sh install USERNAME followed by sudo ./svc.sh start, and that approach is cleaner in theory. But what I found working is to run as ec2-user directly with runsvc.sh:
sudo su - ec2-user ./svc.sh install
sudo su - ec2-user ./runsvc.sh start &
Wait a little while for your runner to connect, then check GitHub—it should be online now.
Note: The & backgrounds the process. Not the most elegant solution, but it’s reliable on Mac instances when the standard approach doesn’t work.
EC2 User Data
If you’re launching Mac instances dynamically, throw this in your user data script. Since I pre-bake the runner into the AMI via Packer, user data just handles the startup:
#!/bin/bash
# Configure labels based on instance type, availability zone, etc.
LABELS="ios-build,mac-m1"
# Navigate to runner directory and start as ec2-user
cd /Users/ec2-user/actions-runner
sudo su - ec2-user bash -c "cd ~/actions-runner && ./svc.sh install && ./runsvc.sh start &"
To verify everything worked, SSH into your instance and check the log:
sudo cat /var/log/amazon/ec2/ec2-macos-init.log
Your user data output lives at (remember to replace with your actual instance ID):
/usr/local/aws/ec2-macos-init/instances/i-xxxxx/userdata
Keeping Your Instance Awake (Important!)
By default, macOS instances will sleep or shut down to conserve resources. This breaks your runner. Disable sleep and ensure the instance stays alive:
systemsetup -setallowpowerbuttontosleepcomputer off > /dev/null 2>&1
pmset sleep 0
pmset disksleep 0
pmset displaysleep 0
Auto-restart if there’s a power failure:
pmset autorestart 1
Disable power nap:
pmset -c powernap 0
You can also use caffeinate to keep the system awake long-term:
caffeinate -i -m -u &
This will keep the system awake forever (or until you kill the process).
Troubleshooting
“Http response code: UnprocessableEntity (422)” or 404 errors
Problem: Token is invalid or repo URL is wrong.
Fix: Double-check your token (it expires after 1 hour!) and your GitHub URL. Generate a fresh token if needed.
“no such file or directory: ./config.sh”
Problem: You’re not in the actions-runner directory.
Fix: Make sure you’re actually in the folder. This happens a lot with sudo -u -i ec2-user because -i changes to the user’s home directory (/Users/ec2-user), not where your runner is. Use sudo su - ec2-user instead, then navigate to your runner folder.
“Runner.Listener: cannot execute binary file”
Problem: Downloaded the wrong architecture (x64 instead of arm64) or wrong OS.
Fix: Go back to GitHub and explicitly select macOS and arm64 when generating the runner. Download the correct version.
“Must not run with sudo” error during config
Problem: You ran sudo ./config.sh.
Fix: Don’t use sudo for config.sh. If you need to configure as a specific user (like ec2-user), do:
sudo su - ec2-user bash -c "cd ~/actions-runner && ./config.sh --unattended --url ... --token ..."
“./bin/actions.runner.service.temp: Permission denied”
Problem: The user running the script doesn’t have permission to the actions-runner directory.
Fix: This happens if you pre-baked the runner in an AMI but then login as ssm-user (AWS Session Manager). Either:
- Ensure ssm-user has access:
sudo chown -R ssm-user:ssm-user /Users/ec2-user/actions-runner - Or use ec2-user consistently throughout your setup
Using Organization-Level Runners
Here’s a tip: set up runners at the organization level instead of per-repository. This way, all your iOS repos can use the same runners without duplicating setup. Just register at the org level, and it’s available everywhere.
Final Thoughts
Setting up self-hosted Mac runners requires a bit more elbow grease than just using GitHub-hosted runners, but once it’s working, you’ll wonder how you lived without it. The key is getting the service configuration right and automating it so new instances just work.
Start the conversation