Deployment guide

GitHub Actions Runner

Overview

GitHub Actions Runner is the official self-hosted runner agent from GitHub. It runs your GitHub Actions workflows on machines you control instead of on GitHub-hosted runners. Use it when a job must reach a private network, use hardware or software that GitHub does not offer, run inside your own compliance boundary, or run longer or more often than hosted minutes allow.

This guide covers the deployment and configuration of GitHub Actions Runner on Ubuntu 24.04 on Azure, using the SecureAnyCloud Azure Marketplace image. The image installs runner 2.335.1 from the official GitHub release, verified against a pinned SHA256 checksum. The runner's own installdependencies.sh resolves its .NET dependencies.

The runner 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 opens an outbound long-poll connection to GitHub and waits to be handed a job. It 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. 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 the token is exchanged for, would let anyone who launches that image join your continuous integration and execute against your repositories.

This image therefore 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 cannot start until you have registered. You register the runner once, with a token you generate yourself, and then enable the service.

What you get:

  • 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 that container jobs work
  • A systemd unit, github-actions-runner.service, installed and ready to enable, and gated so that it cannot start before registration
  • A preinstalled CI toolchain, listed in full below, with nothing installed speculatively
  • A per-VM runner name generated on first boot, so that 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 and its service state, and that no credential artifact exists anywhere on the image
  • 24/7 SecureAnyCloud support

What is preinstalled

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

  • An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region
  • A subscription to the GitHub Actions Runner listing on Azure Marketplace
  • A network security group (NSG) rule that allows TCP 22 for administration. No inbound CI port is required, because the runner connects outbound to GitHub
  • Admin rights on the GitHub repository, organisation or enterprise that you want to register the runner to, so that 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 that does real work, such as compiling code, running test suites and building container images, is much more demanding than an idle appliance. Burstable B-series credits are the wrong shape for sustained build load. Scale up or out from there according to your job mix.

1

Deploy from the Azure Portal

Search the Azure Marketplace for GitHub Actions Runner, select the SecureAnyCloud publisher, and select Create.

Configure the network security group to allow TCP 22 for administration. You do not need to open any other inbound port.

2

Deploy from the Azure CLI

Create the VM with the Azure CLI:

RG="ci-runners"; LOCATION="eastus"; VM_NAME="ghrunner1" IMAGE_URN="sac1647283583153:github-actions-runner-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:github-actions-runner-ubuntu-24-04:default:latest
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$IMAGE_URN" \
  --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
3

First boot - read your per-VM runner identity

On first boot, the image generates a unique runner name for this VM and resolves the VM's reachable address. It 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 the file:

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. The only credential in this product's life cycle is the one that GitHub issues to you at registration.

4

Confirm the runner is installed and correctly idle

The runner binary reports its real version. The service is installed but not enabled and not running, which is the correct state for an unregistered runner and 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

The is-enabled command returns disabled and is-active returns inactive. This is by design. The runner has nothing to do until you register it, and the unit is gated on the registration state file so that it cannot start early.

5

Confirm the preinstalled toolchain

Every tool listed in the table above is present and on the path:

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

Verify the image carries no credentials

The image ships a self-test that checks the most important property of this product: that it contains no runner credential of any kind. The self-test checks that:

  • The runner executes and reports its real version.
  • The runner's dependencies resolve.
  • The service is installed and correctly disabled.
  • The runner refuses to start unregistered.
  • No credential artifact exists. It checks the exact files that registration would create, and it sweeps the whole filesystem for credential-shaped files and GitHub token-shaped strings.

The self-test 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 this control, a sweep that silently matched nothing would pass for the wrong reason.

Run the self-test:

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

The self-test prints RUNNER_SELFTEST_OK when every check passes.

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"
7

Register the runner to your repository or organisation

This is the one step that 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 select Settings > Actions > Runners > New self hosted runner, and choose Linux / x64. The token appears in the configure command on that page.
  • For an organisation: go to your organisation, then select Settings > Actions > Runners > New self hosted runner.

The token is short-lived. It expires within the hour and 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. Substitute 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 the command 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.

After registration, 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. It picks up any workflow job that targets it.

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 that you passed during registration 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 run 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

The systemctl status command exits with a non-zero status while the unit is stopped, which is why the || true is there. It is not an error.

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 the runners 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 that the runner accepts exactly one job and then deregisters. Pair this with launching a fresh VM per job.

Upgrades. By default, the runner updates itself when GitHub releases a new version, so a registered runner stays 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. Run systemctl status github-actions-runner to check, because it reports a skipped start condition.

config.sh refuses to run. Run it as the service account with 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 token and retry. Confirm that the VM has outbound HTTPS egress to github.com.

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

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

Support

SecureAnyCloud 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 sales@secureanycloud.com.

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

Deploy on Azure

Launch GitHub Actions Runner 2.335.1 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure