Developer Tools Azure

GitHub Actions Runner on Ubuntu 24.04 on Azure User Guide

| Product: GitHub Actions Runner 2.335.1 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of GitHub Actions Runner on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. GitHub Actions Runner is the official self hosted runner agent from GitHub. It lets you execute your GitHub Actions workflows on machines you control instead of on GitHub hosted runners, which is what you need when a job must reach a private network, use hardware or software GitHub does not offer, run inside your own compliance boundary, or simply run longer or more often than hosted minutes allow.

The image installs runner 2.335.1 from the official GitHub release, verified against a pinned SHA256 checksum, with its .NET dependencies resolved by the runner's own installdependencies.sh. It runs under a dedicated non root service account, ghrunner, which has no password and no sudo rights — a runner executes whatever code your workflows contain, so it should never hold administrative privilege.

A self hosted runner works by opening an outbound long poll connection to GitHub and waiting to be handed a job. That has a useful consequence for your network: the runner never listens for inbound CI traffic, so this image opens no inbound port other than SSH for your own management.

No credentials by design — and why the runner does not start on its own. A runner is useless until it is registered to a repository, organisation or enterprise, and registration requires a short lived registration token that is specific to your account. An image that shipped a token, or the long lived credentials that token is exchanged for, would let anyone launching that image join your continuous integration and execute against your repositories. So this image ships with no token, no credentials and no registration state whatsoever. The github-actions-runner service is installed but deliberately left disabled, and it is gated so that it physically cannot start until you have registered. You register it once, with a token you generate yourself, and then enable the service.

What is included:

  • GitHub Actions Runner 2.335.1 from the official GitHub release, SHA256 verified, installed at /opt/actions-runner

  • A dedicated non root service account ghrunner with no password, no sudo rights, and membership of the docker group so container jobs work

  • A systemd unit github-actions-runner.service, installed and ready to enable, gated so it cannot start before registration

  • A pre installed CI toolchain, listed in full below — nothing is installed speculatively

  • A per VM runner name generated on first boot, so several of these images can join one organisation without colliding, recorded in /root/github-actions-runner-credentials.txt (root only)

  • A built in self test (/opt/actions-runner/runner-selftest.sh) that verifies the runner, its dependencies, its service state, and that no credential artifact exists anywhere on the image

What is pre installed

Everything below is present so that a typical workflow does not have to install it on every job run. This list is exhaustive — the image does not carry anything else on top of Ubuntu 24.04 LTS.

Package Why it is here
git Every workflow that uses actions/checkout needs it
docker.io Container jobs and service containers; ghrunner is in the docker group
build-essential, make, pkg-config Compiling C and C++ projects and native extensions
python3, python3-pip, python3-venv Python jobs and tooling
jq Parsing JSON in workflow steps
unzip, zip Artifact and cache handling
rsync Deployment and file sync steps
curl, wget, tar, gzip Fetching and unpacking in workflow steps
gnupg, openssl Signing and checksum verification steps

Node.js is not listed separately because the runner ships its own bundled Node runtimes, which is what JavaScript actions execute against.

Prerequisites

  • Active Azure subscription, SSH public key, VNet + subnet in target region

  • Subscription to the GitHub Actions Runner listing on Azure Marketplace

  • A Network Security Group rule allowing TCP 22 for administration. No inbound CI port is required — the runner connects outbound to GitHub

  • Admin rights on the GitHub repository, organisation or enterprise you want to register the runner to, so you can generate a registration token

  • Outbound HTTPS (TCP 443) egress to github.com and its Actions endpoints

Recommended virtual machine size: Standard_D4s_v5 (4 vCPU, 16 GB RAM). A runner doing real work — compiling, running test suites, building container images — is materially more demanding than an idle appliance, and burstable B series credits are the wrong shape for sustained build load. Scale up or out from there according to your job mix.

Step 1: Deploy from the Azure Portal

Search GitHub Actions Runner in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 22 for administration. You do not need to open any other inbound port.

Step 2: Deploy from the Azure CLI

RG="ci-runners"; LOCATION="eastus"; VM_NAME="ghrunner1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/github-actions-runner-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$GALLERY_IMAGE_ID" \
  --size Standard_D4s_v5 \
  --admin-username azureuser \
  --ssh-key-values "$SSH_KEY" \
  --public-ip-sku Standard
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1001

Step 3: First boot — read your per VM runner identity

On first boot the image generates a unique runner name for this VM, resolves this VM's reachable address, records the runner version and the registration instructions, and writes them to a root only file. This completes within seconds. SSH in as azureuser and read it:

sudo cat /root/github-actions-runner-credentials.txt

The file records runner.name (the per VM name to register with), runner.work_dir, runner.service_user, runner.version, and runner.registered=false. It contains no token and no secret, because there is none to contain — the only credential in this product's life cycle is the one GitHub issues to you at registration.

Step 4: Confirm the runner is installed and correctly idle

The runner binary reports its real version, and the service is installed but not enabled and not running — which is the correct state for an unregistered runner, not a fault.

sudo -u ghrunner /opt/actions-runner/bin/Runner.Listener --version
systemctl is-enabled github-actions-runner.service
systemctl is-active github-actions-runner.service
systemctl is-active github-actions-runner-firstboot.service

is-enabled returns disabled and is-active returns inactive. That is by design: there is nothing for the runner to do until you register it, and the unit is gated on the registration state file so it cannot start early.

the runner binary reports version 2.335.1, the github-actions-runner service is installed but reports is-enabled disabled and is-active inactive because the runner is deliberately unregistered, and the first boot service reports active having written the per VM runner identity

Step 5: Confirm the pre installed toolchain

Every tool the table above advertises is present and on the path.

git --version
docker --version
python3 --version
jq --version
gcc --version | sed -n '1p'
rsync --version | sed -n '1p'

git, docker, gcc, python3, jq and rsync each report their installed version, confirming the pre installed continuous integration toolchain advertised in the guide is present on the image and ready for workflow jobs

Step 6: Verify the image carries no credentials

The image ships a self test that proves what matters most about this product: that it contains no runner credential of any kind. It checks the runner executes and reports its real version, that its dependencies resolve, that the service is installed and correctly disabled, that the runner refuses to start unregistered, and that no credential artifact exists — both by checking the exact files registration would create and by sweeping the whole filesystem for credential shaped files and GitHub token shaped strings.

It also runs a negative control: it plants a decoy credential file and a decoy token in a temporary directory and requires both sweeps to detect them before it trusts the real sweeps. Without that, a sweep that silently matched nothing would pass for the wrong reason.

sudo /opt/actions-runner/runner-selftest.sh

It prints RUNNER_SELFTEST_OK when every check passes.

the runner self test reports the version check, dependency resolution, the service correctly disabled, the runner refusing to start unregistered, the negative control detecting a planted credential and token, and the filesystem wide sweeps finding no credential artifacts, ending with RUNNER_SELFTEST_OK

You can confirm the same by hand. None of the files that registration would create exist yet:

sudo ls -a /opt/actions-runner | grep -E '^\.(runner|credentials|env|path)' || echo "no registration artifacts present"
sudo ls /etc/systemd/system/actions.runner.*.service 2>/dev/null || echo "no generated runner units present"

listing the runner directory shows none of the registration artifacts dot runner, dot credentials, dot env or dot path exist, and no generated actions dot runner service units are present, confirming the image ships completely unregistered with no GitHub credentials

Step 7: Register the runner to your repository or organisation

This is the one step only you can do, because the token belongs to your account.

Generate a registration token in GitHub:

  • For a repository: go to your repository, then Settings > Actions > Runners > New self hosted runner, and choose Linux / x64. The token is shown in the configure command on that page.
  • For an organisation: go to your organisation, then Settings > Actions > Runners > New self hosted runner.

The token is short lived — it expires within the hour — and it is single use for registration. You do not need to keep it afterwards.

Then run the configure command on the VM as the ghrunner service account, substituting your URL, your token and the per VM runner name from Step 3:

sudo -u ghrunner /opt/actions-runner/config.sh \
  --url https://github.com/<your-org>/<your-repo> \
  --token <your-token> \
  --name <RUNNER_NAME> \
  --work <RUNNER_WORK_DIR> \
  --labels self-hosted,linux,x64,azure \
  --unattended
sudo systemctl enable --now github-actions-runner.service

Run it as the <RUNNER_SERVICE_USER> account, never as root — the runner refuses to configure under sudo without an explicit override, and that refusal is deliberate.

Once registered, config.sh writes the registration state file, which satisfies the service's start gate, and systemctl enable --now starts the runner. Your runner then appears under Settings > Actions > Runners in GitHub with an Idle status, and it will pick up any workflow job that targets it.

Step 8: Send a job to your runner

Target the runner from a workflow with runs-on. The self-hosted label is applied automatically; the extra labels you passed above let you route jobs more precisely.

name: build-on-self-hosted
on: [push, workflow_dispatch]
jobs:
  build:
    runs-on: [self-hosted, linux, x64]
    steps:
      - uses: actions/checkout@v4
      - name: Show the runner environment
        run: |
          hostname
          git --version
          docker --version

Push that workflow, or trigger it from the Actions tab, and watch the job execute on your VM. On the VM you can follow the runner's own logs:

sudo systemctl status github-actions-runner.service --no-pager || true
sudo journalctl -u github-actions-runner.service -n 50 --no-pager

systemctl status exits non zero while the unit is stopped, which is why the || true is there — it is not an error.

Step 9: Operating the runner

Scaling out. Launch more of these images and register each one. Each VM generates its own unique runner name on first boot, so they never collide inside a single organisation. Group them with runner groups and route work with labels.

Untrusted code. A self hosted runner executes whatever a workflow contains. GitHub's own guidance is to avoid self hosted runners on public repositories, where anyone can open a pull request that runs code on your machine. Keep these runners on private repositories, or isolate them thoroughly.

Ephemeral runners. For stronger isolation between jobs, register with --ephemeral so the runner accepts exactly one job and then deregisters. Pair that with launching a fresh VM per job.

Upgrades. The runner auto updates itself by default when GitHub releases a new version, so a registered runner keeps current on its own. To pin a version instead, register with --disableupdate and manage upgrades yourself.

Deregistering. To remove a runner cleanly, stop the service and remove the registration with a fresh removal token from the same GitHub settings page.

sudo systemctl disable --now github-actions-runner.service
sudo -u ghrunner /opt/actions-runner/config.sh remove --token <your-token>

Troubleshooting

  • The service will not start. If systemctl start github-actions-runner appears to do nothing and the unit stays inactive, the runner is not registered yet. The unit is gated on /opt/actions-runner/.runner, which config.sh creates. Complete Step 7 first. Check with systemctl status github-actions-runner — a skipped start condition is reported there.

  • config.sh refuses to run. Run it as the service account, sudo -u ghrunner, not as root. The runner deliberately refuses to configure as root.

  • The runner does not appear in GitHub. Registration tokens expire within the hour — generate a fresh one and retry. Confirm the VM has outbound HTTPS egress to github.com.

  • Container jobs fail. Confirm docker.service is active and that the runner account is in the docker group with id ghrunner. Group membership applies to the service on start, so restart the runner service after any change.

  • Check what the image contains. sudo /opt/actions-runner/runner-selftest.sh re-runs every image level check, including the credential sweeps, at any time.

Support

cloudimg provides 24/7 technical support for this GitHub Actions Runner image. Our engineers help with registration, runner groups and labels, scaling to a fleet, workflow and caching design, container and service container jobs, hardening the execution environment for untrusted code, network and firewall rules, and runner version upgrades. Critical issues receive a one hour average response time. Contact support@cloudimg.co.uk.

All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.