How to Set Up a GitHub Actions Self-Hosted Runner on a VPS

Build a dependable Ubuntu CI runner with systemd, Docker builds, useful labels, controlled access, safe updates and a workflow that proves the setup works.

Explore Cloud VPSStart the setup
HYEHOST mascot running a successful build, test and deploy pipeline on a self-hosted CI runner

A GitHub Actions self-hosted runner is a machine you manage that accepts workflow jobs from GitHub. It can provide predictable CPU and memory, warm dependency caches, access to private services and complete control over installed build tools. The trade-off is equally direct: you own patching, isolation, credentials, capacity and recovery.

This guide sets up one persistent Linux runner on Ubuntu. That model is practical for a private repository or a small trusted team. It is not the right pattern for arbitrary public pull requests, where each job should run in a disposable and strongly isolated environment.

When a Self-Hosted GitHub Actions Runner Makes Sense

RequirementWhy a VPS runner helpsWatch out for
Faster repeat buildsDependencies and Docker layers can remain cachedStale state can hide reproducibility problems
Custom toolchainInstall exact compilers, SDKs and servicesYou must patch every installed component
Private network accessReach staging services over a private LAN or WireGuardRestrict which repositories can use the runner
Predictable resourcesSelect the CPU, RAM and disk needed by the workloadOne persistent runner handles one job at a time by default
Deployment automationKeep build and deployment paths close to infrastructureDeployment credentials need narrow permissions

GitHub-hosted runners remain the simpler choice when standard images, clean environments and automatic scaling already meet the need. Self-host because the workload benefits from it, not simply because a runner can be installed.

Choose a VPS for CI/CD

The runner application itself needs little capacity; the workflow determines the server size. A practical starting point for Node.js, Python or lightweight Docker builds is 2 vCPU, 4 GB RAM and at least 40 GB SSD. Compilation, browser tests, large container images and multiple services can require 4-8 vCPU, 8-16 GB RAM and more disk.

  • CPU: sustained compilation benefits from guaranteed CPU on a VDS.
  • Memory: leave room for the operating system, build process and any test databases.
  • Storage: allow space for source trees, package caches, Docker layers and artefacts.
  • Network: outbound HTTPS is required; HYEHOST Cloud VPS includes IPv4, IPv6 and DDoS-protected connectivity.
  • Location: put deployment runners near the systems they manage, while keeping production access explicitly controlled.

Start from a current Ubuntu image and create a non-root user dedicated to Actions:

sudo apt update
sudo apt full-upgrade -y
sudo adduser --disabled-password --gecos "" actions
sudo usermod -aG sudo actions

Use SSH keys, disable password login after confirming key access, and keep the firewall closed except for restricted SSH. The runner initiates outbound HTTPS connections to GitHub; it does not require a public inbound runner port.

Decide the Security Boundary Before Registration

Workflow code is code execution. Anyone who can change an authorised workflow can potentially read files available to the runner user, use accessible credentials and reach permitted network services. GitHub explicitly recommends self-hosted runners only for private repositories because forks of a public repository can submit pull requests containing dangerous workflow code.

  • Dedicate the VPS to CI instead of installing the runner on a production host.
  • Register it only to repositories that genuinely require it.
  • Protect workflow files with branch rules and CODEOWNERS review.
  • Use environment approvals for production deployment jobs.
  • Give tokens, SSH keys and cloud credentials the narrowest possible permissions.
  • Never run untrusted pull-request code with production secrets or Docker access.

Install and Register the Runner

In the target repository, open Settings > Actions > Runners, choose New self-hosted runner, then select Linux and the server architecture. GitHub generates current download, checksum and registration commands for that repository. Use those exact commands rather than copying an old runner version from a third-party guide.

Log in as the dedicated user and create the runner directory:

sudo -iu actions
mkdir -p ~/actions-runner
cd ~/actions-runner

Run the download and checksum commands displayed by GitHub, extract the archive, then register it with the short-lived token shown on the same page:

./config.sh \
  --url https://github.com/YOUR-ORG/YOUR-REPO \
  --token YOUR_ONE_HOUR_TOKEN \
  --name hyehost-ci-01 \
  --labels ubuntu,vps,docker \
  --work _work

The registration token expires after one hour and is used only to enrol the runner. Do not store it as a long-lived secret. Choose labels that describe capabilities, not vague environments. A job can then require [self-hosted, linux, x64, docker] rather than landing on an unsuitable machine.

Run the GitHub Actions Runner with systemd

Test interactively first:

./run.sh

The terminal should report that it is connected and listening for jobs. Stop it with Ctrl+C, leave the login shell, and install the bundled systemd service from the runner directory:

exit
cd /home/actions/actions-runner
sudo ./svc.sh install actions
sudo ./svc.sh start
sudo ./svc.sh status

Confirm the runner appears as Idle in the GitHub settings page. For service logs, identify the generated unit and inspect it with journalctl:

systemctl list-units --type=service | grep actions.runner
sudo journalctl -u actions.runner.OWNER-REPO.NAME.service --since today

Prove the Runner with a Small Workflow

Create .github/workflows/self-hosted-check.yml in a private test branch:

name: Self-hosted runner check

on:
  workflow_dispatch:

jobs:
  verify:
    runs-on: [self-hosted, linux, x64, vps]
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - name: Show runner resources
        run: |
          uname -a
          nproc
          free -h
          df -h /
      - name: Run project tests
        run: echo "Replace this with your test command"

Trigger it from the Actions tab. Check that the expected runner receives the job, the checkout succeeds and resource output matches the VPS. Add a timeout to every job so a deadlocked build cannot occupy the runner forever.

Enable Docker Builds Carefully

GitHub requires Docker on Linux when workflows use container actions or service containers. Install Docker from its official repository using our Ubuntu Docker guide, then add the runner account to the Docker group:

sudo usermod -aG docker actions
cd /home/actions/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start
sudo -iu actions docker version

Membership of the Docker group is effectively root-level access. Only grant it when trusted workflows need Docker, and do not treat a container as a security boundary against hostile repository code. For public or mixed-trust workloads, use ephemeral runners, separate VMs or an autoscaling solution designed to destroy the environment after each job.

Control disk growth because image layers and build caches accumulate:

docker system df
docker builder prune --filter "until=168h"
docker image prune --filter "until=168h"

Do not run an indiscriminate prune during an active job. Schedule maintenance for a quiet period and preserve images required by concurrent services.

Handle Deployment Credentials Without Spreading Them

Store repository or environment secrets in GitHub, scope them to the jobs that need them, and use protected environments for production. Prefer a deployment account that can update one application over a broad root key. If the runner can reach production over a HYEHOST private LAN or WireGuard tunnel, restrict the destination firewall to the runner's private address and required port.

Separate build and deployment jobs where possible. The build runner can produce a signed or checksummed artefact; a more restricted deployment runner can consume only approved artefacts after an environment review. This reduces the amount of production authority present during ordinary test jobs.

Keep the Runner Supported and Recoverable

The runner application normally updates automatically, while Ubuntu, Docker, language runtimes and build dependencies remain your responsibility. GitHub's 2026 minimum-version enforcement is a timely reason to verify that runners are current: the published schedule begins full enforcement for Enterprise Cloud with Data Residency on 31 July 2026 and for Enterprise Cloud on 25 September 2026. Treat those dates as current operational context and check GitHub's changelog for later revisions.

  • Review runner status and version in GitHub settings.
  • Patch Ubuntu and reboot when the kernel requires it.
  • Monitor disk, memory, load and failed systemd services.
  • Take a VPS snapshot before major toolchain changes.
  • Keep setup scripts in source control so the runner can be rebuilt.
  • Test removal and re-registration before an emergency demands it.

If automatic runner updates are disabled, updating the runner image becomes a scheduled operational task. GitHub states that sufficiently old runner versions can be rejected from executing workflows, so version drift can become an outage rather than a cosmetic warning.

Common Self-Hosted Runner Problems

SymptomLikely causeWhat to check
Job stays queuedLabels do not match or runner is offlineCompare runs-on labels with the runner settings
Runner shows offlineService stopped or outbound access blockedsvc.sh status, journal logs and HTTPS egress
Docker permission deniedRunner user lacks group membershipRun id actions and restart the service after group changes
Disk fills rapidlyDocker layers, caches or artefacts retaineddocker system df and the runner work directory
Build works once, then failsPersistent state or stale cacheClean the workspace and test from a fresh checkout
Registration failsExpired token or unsupported runner versionGenerate a new token and use GitHub's current download commands

GitHub Actions Self-Hosted Runner FAQ

How much RAM does a GitHub Actions self-hosted runner need?

The runner application is light, but workflow requirements determine capacity. A 2 vCPU and 4 GB RAM VPS is a practical starting point for ordinary builds. Compilation, browser tests and parallel services need more.

Can a self-hosted runner run Docker builds?

Yes. Install Docker on Linux and grant the dedicated runner user access only for trusted workflows. Docker group membership is effectively privileged and should not be available to untrusted pull requests.

Should I use one for a public repository?

GitHub recommends self-hosted runners only for private repositories because forked pull requests can contain dangerous workflow code. Public projects need disposable runners and stronger isolation.

Does the runner need inbound ports?

No inbound runner port is required. The service connects outbound to GitHub over HTTPS. Restrict SSH to trusted addresses and expose other ports only when a workflow genuinely needs them.

Can multiple jobs run at once?

One runner processes one job at a time. Register additional runner instances on separate machines for straightforward concurrency, or evaluate Actions Runner Controller for production Kubernetes autoscaling.

How do I keep it updated?

The runner normally updates itself, but the operating system, Docker and build tools remain your responsibility. Monitor the runner version and GitHub changelog, especially when automatic updates are disabled.

Build CI You Can Recreate

A good self-hosted runner is deliberately boring: one clear trust boundary, reproducible provisioning, narrow credentials, visible resource use and a tested rebuild path. Start with a private repository and one dedicated VPS, prove the workflow, then add capacity only when queue time or build requirements justify it.

Use a HYEHOST Cloud VPS for general CI/CD workloads or choose a VDS when sustained compilation needs guaranteed CPU. Both give you the root access needed to control the runner and its toolchain.

Official Resources