## How to set up an **ARM64 Ubuntu** self‑hosted runner for GitHub Actions

> The steps below work on **Ubuntu 20.04, 22.04 or 24.04 ARM64** (for example, a Raspberry Pi 5, AWS Graviton, Ampere Altra, or Oracle ARM instance). GitHub officially lists “Ubuntu 20.04 or later” and the **ARM64** architecture as supported for self‑hosted runners 🡒 [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/supported-architectures-and-operating-systems-for-self-hosted-runners)

* * *

### 1. Why run your own ARM64 runner?

| When it helps | Why |
| --- | --- |
| **Native ARM builds/tests** | Avoid cross‑compiling and catch architecture‑specific bugs early. |
| **CI costs** | ARM64 hardware is generally cheaper and more energy‑efficient than x86 equivalents. |
| **Custom tooling / networking** | You fully control the software stack, caches, and private resources. |
| **Long‑running or privileged jobs** | No idle‑time limits beyond what you impose yourself. |

Hosted ARM64 runners now exist, but they are still limited (public repos only, no privileged Docker, etc.) [GitHub](https://github.com/orgs/community/discussions/148648?utm_source=chatgpt.com).

* * *

### 2. Prerequisites

1. **Ubuntu ARM64 host** with ≥2 vCPU, ≥2 GiB RAM, outbound HTTPS on port 443 and the following packages:
    
    ```bash
    sudo apt update && sudo apt install -y curl tar git ca-certificates jq
    ```
    
2. **GitHub access:** admin rights on the repository (or organisation) where the runner will live.
    
3. **Dedicated system user (recommended):**
    
    ```bash
    sudo adduser --disabled-login --gecos '' github
    sudo usermod -aG docker github   # if your workflows need Docker
    ```
    
4. Optional but wise: set up a firewall that allows only the GitHub domains required for runners [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/supported-architectures-and-operating-systems-for-self-hosted-runners).
    

* * *

### 3. Download the latest ARM64 runner

```bash
sudo mkdir -p /opt/actions-runner && sudo chown github:github /opt/actions-runner
cd /opt/actions-runner
LATEST=$(curl -s https://api.github.com/repos/actions/runner/releases/latest | jq -r '.tag_name|ltrimstr("v")')
curl -L -o actions-runner-linux-arm64-"$LATEST".tar.gz \
  https://github.com/actions/runner/releases/download/v${LATEST}/actions-runner-linux-arm64-${LATEST}.tar.gz
tar -xzf actions-runner-linux-arm64-"$LATEST".tar.gz
```

_The download URL and packaging are provided by the official `actions/runner` release page_ .

* * *

### 4. Register the runner with GitHub

1. In your repository (or organisation) click **Settings → Actions → Runners → _New self‑hosted runner_**.
    
2. Choose **Linux → ARM64** and **copy the tokenised URL** shown. The token expires after one hour [GitHub Docs](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/add-runners).
    

Now, as the **github** user:

```bash
./config.sh \
  --url https://github.com/<OWNER>/<REPO> \
  --token <TOKEN_FROM_UI> \
  --name $(hostname)-arm64 \
  --labels self-hosted,linux,arm64,ubuntu \
  --unattended \
  --work _work \
  --ephemeral           # Optional: auto‑deregister after one job
#  --disableupdate      # Optional: you will update the binary yourself
```

**Flags explained**

| Flag | Purpose |
| --- | --- |
| `--labels` | Match jobs that use `runs-on: [self-hosted, arm64]`. |
| `--unattended` | No interactive prompts (good for scripts). |
| `--ephemeral` | Recommended for auto‑scaled or immutable runners (GitHub erases it after one job) [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/supported-architectures-and-operating-systems-for-self-hosted-runners). |
| `--disableupdate` | Pin the runner version and update on your own schedule [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/supported-architectures-and-operating-systems-for-self-hosted-runners). |

At the end you should see **“√ Connected to GitHub – Listening for Jobs”** [GitHub Docs](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/add-runners).

* * *

### 5. Run the runner as a systemd service

From inside `/opt/actions-runner`:

```bash
sudo ./svc.sh install      # installs actions.runner.<host>.service
sudo ./svc.sh start        # starts it immediately
sudo ./svc.sh status       # verify it is active
```

These helper scripts are generated by the runner itself and wrap a standard systemd unit [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service).

The service is now **autostarted on boot** and restarts on failure. Logs live in `journalctl -u actions.runner.*`.

* * *

### 6. Using the runner in workflows

```yaml
# .github/workflows/build-arm64.yml
name: Build on ARM64
on: [push]

jobs:
  build:
    runs-on: [self-hosted, linux, arm64]
    steps:
      - uses: actions/checkout@v4
      - run: uname -m    # prints aarch64
      - run: make test
```

Because the job asks for both `self-hosted` and `arm64`, it will only run on your new machine; GitHub‑hosted x86 runners won’t match.

* * *

### 7. Hardening recommendations

| Area | Recommendation |
| --- | --- |
| **Privilege** | Keep the runner user unprivileged; only add to `docker` if strictly needed. |
| **Sandboxing** | Prefer Docker‑in‑Docker or tool‑specific containers for untrusted code. |
| **Network** | Restrict outbound traffic to GitHub domains plus required package mirrors. |
| **Ephemeral** | Combine `--ephemeral` with cloud‑init or Ansible to recreate a clean instance per job. |
| **Secrets** | Store credentials in **GitHub Secrets**, never on the runner disk. |
| **Updates** | If you enabled `--disableupdate`, schedule a cron job to pull new runner versions within 30 days [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/supported-architectures-and-operating-systems-for-self-hosted-runners). |

* * *

### 8. Upgrading the runner

```bash
sudo systemctl stop actions.runner.*
cd /opt/actions-runner
LATEST=$(curl -s https://api.github.com/repos/actions/runner/releases/latest | jq -r '.tag_name|ltrimstr("v")')
curl -L -o actions-runner-linux-arm64-"$LATEST".tar.gz \
  https://github.com/actions/runner/releases/download/v${LATEST}/actions-runner-linux-arm64-${LATEST}.tar.gz
tar -xzf actions-runner-linux-arm64-"$LATEST".tar.gz
sudo systemctl start actions.runner.*
```

If you **did not** disable auto‑update, the runner will self‑upgrade when it next accepts a job.

* * *

### 9. Troubleshooting quick‑hits

| Symptom | Fix |
| --- | --- |
| Runner shows **offline** | `journalctl -u actions.runner.* |
| Job stuck in **“Waiting for runner to pick up”** | Ensure labels match and runner is **Idle** in the UI. |
| **libatomic / libicu** missing errors | `sudo apt install -y libatomic1 libicu-dev` (Raspberry Pi OS derivatives often need this). |
| Disk fills with `_work/_tool` cache | Periodically prune with `rm -rf /opt/actions-runner/_work/_tool/*`. |

* * *

### 10. Next steps

* **Scale out**: Use Terraform or cloud‑init to spin up N identical ARM64 runners; give them all the same label group.
    
* **Runner groups**: At the organisation level, place ARM64 runners in a locked‑down group and allow only specific repos to use them [GitHub Docs](https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/add-runners).
    
* **Monitor**: Enable the `workflow_job` webhook and track queue times; auto‑scale when the queue grows [GitHub Docs](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/supported-architectures-and-operating-systems-for-self-hosted-runners).
    

* * *