← Blog
An illustrated CPU server connected to an input tile and two checkmark tiles.
Published on
2026-10-30

Run a GitHub Actions self-hosted runner on a CPU VM

A GitHub Actions self-hosted runner is a machine you manage that executes jobs from your GitHub workflows. To run one on a CPU VM, prepare a supported Linux environment, register the runner with your repository, and route a workflow to its labels. You also take responsibility for software updates, access, cleanup, and the machine's bill.

This guide walks developers through a small pilot: one private test repository, one disposable Linux x64 VM, and one manually triggered Python job. It includes registration, a workflow you can copy, troubleshooting, and a cost calculation. A CPU instance fits this example because the job uses ordinary Python arithmetic and needs no GPU.

Validation note, October 30, 2026: This is a documentation-based walkthrough. YAML parsing, workflow structure, the embedded Python calculation, and the illustrative cost arithmetic passed local checks on Windows. Linux shell execution, runner installation and registration, GitHub job execution, and Hivenet VM cleanup and billing have not been tested. The cloud steps below describe expected behavior, not an executed deployment or benchmark.

Self-hosted or GitHub-hosted runners?

Start with the reason you need your own machine. A custom compiler, a particular CPU architecture, or a controlled build environment can justify self-hosting. An occasional test that runs comfortably on a standard GitHub-hosted runner usually does not need another server to maintain.

GitHub's runner overview explains the division of responsibility: you manage the machine and its software; the runner application receives GitHub jobs. Existing resources can be useful, but a runner should have a defined purpose and owner. Putting it on the server that hosts your production API would give build code access to an environment it should not need.

For a CPU pilot, begin with a small configuration and measure your actual build later. More vCPUs may help parallel compilation, while a dependency download may spend most of its time waiting on the network. Memory use and disk growth can matter more than CPU count. The guide to workloads that do not need a GPU can help make that first choice. Use the vCPU sizing guide to plan measurements for the real build.

‍

Set the trusted-code boundary first

Use a new private repository with only trusted maintainers and no deployment credentials. Review who can change workflows or cause code to run. A private repository can still expose a runner to untrusted contributions, and manual triggering alone does not make a workflow safe.

GitHub's secure-use guidance warns that workflow code can compromise a self-hosted environment. Keep production secrets, private SSH keys, and access to internal services off this VM. Use a dedicated Linux account without administrative privileges, and do not give it Docker socket access for this Python example. Treat any later addition of checkout steps, dependencies, actions, or secrets as a change to the trust boundary.

Use a repository-level runner for this pilot. If a team later shares runners across repositories, review GitHub's runner group access controls. Labels select eligible machines; they are not an authorization boundary.

1. Prepare a disposable CPU VM

Choose a current Ubuntu LTS image supported by the runner, such as Ubuntu 24.04, on an x64 CPU VM. GitHub lists supported systems and network requirements in its self-hosted runner reference. This walkthrough assumes Bash, Python 3, and an administrator who can prepare the machine.

On Hivenet, follow the CPU instance documentation and check capacity and the selected configuration in the console. A 2-vCPU, 4-GB configuration is a reasonable starting point for this small smoke test, not a sizing recommendation for your build pipeline. Record the instance ID and start time so you can identify and retire the correct VM later.

From your administrator session, update the package index with sudo apt-get update, then install the basic tools with sudo apt-get install -y python3 curl ca-certificates. Apply the image's normal security updates before registration. Create the dedicated account with sudo adduser --disabled-password --gecos "" ci-runner. Keep it out of administrative groups, then switch to it with sudo -iu ci-runner.

The runner needs outbound HTTPS on port 443 to GitHub's documented domains. Follow the maintained domain list, including endpoints for logs and updates, if you restrict egress. No public inbound job-listener port is needed for this workflow. Keep administrative SSH access limited to the people managing the VM.

2. Download and register the GitHub runner

In the private repository, open Settings → Actions → Runners → New self-hosted runner. Choose Linux and x64. You need the appropriate repository owner or administrator permissions, and organization policy must allow the runner.

Follow GitHub's current installation instructions in the ci-runner shell. Use the download, checksum verification, and extraction commands shown for your architecture. This keeps the runner version and checksum together rather than relying on a version copied from an older article.

If the runner reports missing operating-system dependencies, have the administrator install them using the official package's dependency instructions. Return to the unprivileged account for configuration and execution.

Before running the configuration command supplied by GitHub, append --ephemeral --name cpu-demo-01 --labels cpu-demo. Keep the default operating-system and architecture labels. Use a fresh registration token if the supplied one has expired; it is valid for one hour. Keep the real token and command out of repositories, shared notes, recordings, and logs, including shell history that other people can access.

GitHub's ephemeral runner documentation describes --ephemeral as a one-job registration. GitHub removes that registration after it processes a job; it does not wipe the disk or delete the VM. Only register this runner when the private test workflow is ready, and ensure no other workflow in the repository can consume it unexpectedly.

‍

From the extracted runner directory, start it with ./run.sh. Keep the shell open for this short pilot. Check that the runner appears online and idle in GitHub before dispatching the test. A long-running service is a separate operational choice; this disposable example does not need one.

3. Add a small manual workflow

Create .github/workflows/cpu-runner-smoke.yml in the private test repository with the content below. Review it and commit it to the default branch. The file has one job, uses no external actions, checks out no repository files, and needs no secrets.

name: CPU runner smoke test

"on":
  workflow_dispatch:

permissions: {}

concurrency:
  group: cpu-runner-smoke
  cancel-in-progress: false

jobs:
  smoke:
    runs-on: [self-hosted, linux, x64, cpu-demo]
    timeout-minutes: 5
    steps:
      - name: Check Python and CPU execution
        shell: bash
        run: |
          set -euo pipefail
          uname -m
          python3 --version
          python3 - <<'PY'
          n = 100_000
          result = sum(i * i for i in range(1, n + 1))
          expected = n * (n + 1) * (2 * n + 1) // 6
          if result != expected:
              raise SystemExit("CPU smoke test failed")
          print("CPU smoke test passed")
          PY

The runs-on list requires all four labels. GitHub's label routing documentation describes how default and custom labels work together. Use a label unique to this pilot if cpu-demo already identifies other machines, and update the workflow to match.

The workflow requests no GITHUB_TOKEN permissions through permissions: {}. Its five-minute job timeout bounds a stuck test after execution starts; it does not cap how long a job waits for a runner or how long a VM remains running. Concurrency prevents this workflow group from executing multiple runs together. These controls do not isolate hostile code from the host. See the workflow syntax reference when adapting them.

The Python code compares a loop calculation with a closed-form result. It confirms that the selected environment can execute the sample and report failure. It does not measure build throughput or prove that a larger application will fit.

4. Run the job and record the result

Open the repository's Actions tab, select CPU runner smoke test, and choose Run workflow on the reviewed default branch. GitHub's manual workflow instructions explain the required trigger and repository access. If the button is missing, confirm that the file is on the default branch and contains workflow_dispatch.

Inspect the job details and logs. Confirm the intended runner handled the job, its architecture is the expected x64 Linux environment, Python starts successfully, and the step reports CPU smoke test passed. Save the workflow commit, run URL, runner name, and timestamps. Those records distinguish a reproducible result from a successful installation screen.

Because the runner is ephemeral, expect its registration to disappear after this one job. A second run needs a new runner prepared on a fresh environment. If the first run fails, collect the diagnostic evidence before retiring the VM; do not silently turn the same host into a persistent runner to keep retrying.

If the job stays queued or fails

For a queued job, compare every runs-on label with the available runner and check that it is online, idle, and accessible to this repository. A label mismatch leaves the job waiting. Confirm the foreground process is still running and outbound access has not been blocked. Cancel an unwanted queued run when ending the pilot.

For python3: command not found, have the administrator install Python on the VM. Do not grant the job sudo access to repair the image. A download or registration error needs a check of the selected architecture, current token, dependency message, and GitHub connectivity before another attempt.

Keep diagnostic output free of credentials. GitHub provides separate runner monitoring and troubleshooting instructions for application logs and connectivity checks. A failed smoke test is useful evidence; record the failure instead of treating registration as end-to-end validation.

5. Clean up ephemeral runners and their VMs

Save the run result and any needed diagnostic logs before removing the environment. Check the repository's runner list to confirm the ephemeral registration is gone. If setup was abandoned before a job ran, follow GitHub's runner removal procedure for the remaining registration. Stopping run.sh alone leaves an ordinary registration offline.

Then retire the dedicated VM. On Hivenet, verify the recorded instance ID and follow the stop or terminate instructions. Terminate permanently removes the instance and local data, so export what you need first. Stopping preserves the environment temporarily and is not a clean reset for another untrusted job.

A green GitHub job does not stop the VM's billing. Check its final state and the organization's usage records. For production ephemeral runners, log retention and reliable machine disposal need to be designed into the process. Deleting a working directory is not equivalent to rebuilding a compromised machine.

Calculate cost per successful build

Count the entire time the VM is running: preparation, job execution, troubleshooting, and idle time before cleanup. Divide the attributable infrastructure and operating costs by successful builds. Include failed attempts and retries in the cost total so unreliable runs do not make the average look artificially cheap. Bring those results into the monthly cloud cost optimization review.

‍

As an illustrative calculation, Hivenet's CPU instance page lists 2 vCPUs at €0.035 per hour, checked October 30, 2026. If a disposable VM remains running for 20 minutes and completes one successful job, its compute charge at that rate is about €0.0117: €0.035 × 20 ÷ 60. Twenty minutes is an assumption, not an observed runtime or recommended build duration.

‍

This excludes additional services and the time spent operating the runner. Confirm the console quote and applicable charges before a pilot; the Compute pricing guide explains the broader billing model. GitHub currently describes self-hosted runner usage as free in its Actions billing overview, while related storage and other metered services can still add costs. Check your account's current terms rather than assuming the whole pipeline is free.

Compare this with the GitHub-hosted option using the same workload, successful-run count, and required environment. Include your plan's allowances and the maintenance work a managed runner removes. A lower VM rate by itself does not establish a lower cost per build.

Move from a smoke test to your actual pipeline

After the basic pilot is verified, choose one trusted build and measure it on a fresh runner. Record queue time separately from execution time, peak memory, disk use, dependency setup, and cleanup. Review every new action and credential before adding it. Keep the experiment small enough to explain why it succeeded or failed.

‍

A production setup also needs patching, logs, access ownership, and a way to replace machines reliably. Hivenet's instance API documentation, checked October 30, 2026, says the public API manages existing instances and does not create them. Create the pilot VM in the console when capacity is available, and confirm a supported provisioning path before planning a runner fleet.

‍

If that work matches your team's needs, evaluate an available Hivenet CPU configuration against the measured build. If your workload fits GitHub-hosted runners and your team does not need to own the execution environment, keeping the managed option is a sound result of the evaluation.

‍

Your next workload belongs on Hivenet.

Pick one AI, compute, or storage workload and see the difference for yourself. Spin it up in minutes, or let our team map your fastest path to production.