Install a self-hosted GitHub Actions runner
Register a dedicated Ubuntu 26.04 VPS as a GitHub Actions runner for a trusted private repository.
This guide registers one persistent runner for one trusted private repository. Workflow code runs commands on the VPS, so do not use the machine for production applications or databases.
Prerequisites
- An Ubuntu 26.04 VPS with the security baseline
- Admin access to the private GitHub repository
- A decision about which branches and users may change workflow files
GitHub recommends self-hosted runners only for private repositories. Public pull requests can expose a persistent runner to untrusted code.
Create an unprivileged account
sudo adduser --disabled-password --gecos '' github-runner
sudo install -d -o github-runner -g github-runner -m 0750 /opt/actions-runner
sudo apt update
sudo apt install curl ca-certificates tar
Do not add this account to sudo or the docker group unless a reviewed workflow requires that level of access. Docker group membership is equivalent to root-level control of the host.
Download and register the runner
In GitHub, open the repository and go to Settings → Actions → Runners → New self-hosted runner. Select Linux and x64. GitHub displays commands containing the current runner version, archive checksum, repository URL, and a registration token.
Run the displayed download, checksum, and extraction commands as github-runner in /opt/actions-runner. Use GitHub’s generated commands instead of copying a version or token from this guide. Registration tokens expire after one hour.
Run the generated configuration command and add a unique label such as shrp-build-01:
sudo -u github-runner ./config.sh --url REPOSITORY_URL --token REGISTRATION_TOKEN --labels shrp-build-01 --unattended
Replace both placeholders with the values GitHub shows. The command stores runner credentials on disk, so protect /opt/actions-runner and do not include its contents in general backups or support archives.
Install the service
From /opt/actions-runner, use the runner’s service helper:
sudo ./svc.sh install github-runner
sudo ./svc.sh start
sudo ./svc.sh status
Confirm the runner appears as Idle in GitHub. The runner makes outbound HTTPS connections over port 443 and does not need an inbound public port.
Run a controlled test job
Create .github/workflows/verify-self-hosted-runner.yml in the private repository:
name: Verify self-hosted runner
on:
workflow_dispatch:
permissions:
contents: read
jobs:
verify:
runs-on: [self-hosted, linux, x64, shrp-build-01]
steps:
- uses: actions/checkout@v4
- name: Show runner details
run: |
uname -a
test -n "$RUNNER_NAME"
Run it manually. Check that GitHub reports the expected runner name, the job succeeds, and the runner returns to Idle.
Limit credentials and access
- Set the default
GITHUB_TOKENpermission to read-only and grant write access only to jobs that need it. - Use environments and required reviewers for production deployments.
- Prefer OIDC or short-lived, narrowly scoped credentials over stored personal tokens.
- Do not install personal SSH keys on the runner.
- Restrict who can merge or change workflow files.
- Pin third-party actions to reviewed commit SHAs when stronger supply-chain control is needed.
Persistent runners can retain workspaces, caches, and files from previous jobs. Clean sensitive files after each job or use ephemeral runners for stronger isolation.
Update, diagnose, and remove
The runner normally updates itself, but GitHub may stop assigning jobs when a required security update is missing. Check service logs with:
sudo ./svc.sh status
sudo journalctl -u 'actions.runner.*' -n 100 --no-pager
To remove the runner, first use GitHub’s Remove action to obtain the current removal command. Stop and uninstall the service, run the generated removal command, then delete the runner directory only after confirming it is no longer registered.
See CI runners and build servers on a VPS for sizing and isolation considerations.
Sources: adding self-hosted runners, secure use reference, and self-hosted runner reference.