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
| Requirement | Why a VPS runner helps | Watch out for |
|---|---|---|
| Faster repeat builds | Dependencies and Docker layers can remain cached | Stale state can hide reproducibility problems |
| Custom toolchain | Install exact compilers, SDKs and services | You must patch every installed component |
| Private network access | Reach staging services over a private LAN or WireGuard | Restrict which repositories can use the runner |
| Predictable resources | Select the CPU, RAM and disk needed by the workload | One persistent runner handles one job at a time by default |
| Deployment automation | Keep build and deployment paths close to infrastructure | Deployment 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
| Symptom | Likely cause | What to check |
|---|---|---|
| Job stays queued | Labels do not match or runner is offline | Compare runs-on labels with the runner settings |
| Runner shows offline | Service stopped or outbound access blocked | svc.sh status, journal logs and HTTPS egress |
| Docker permission denied | Runner user lacks group membership | Run id actions and restart the service after group changes |
| Disk fills rapidly | Docker layers, caches or artefacts retained | docker system df and the runner work directory |
| Build works once, then fails | Persistent state or stale cache | Clean the workspace and test from a fresh checkout |
| Registration fails | Expired token or unsupported runner version | Generate 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.

