GitHub Actions Self-Hosted Runner on a VPS: Ephemeral Setup and Cost

Register an ephemeral GitHub Actions runner on an hourly VPS: setup with runner v2.337.0, service or one-shot, JIT runners, security for private repos, removal and cost vs hosted minutes.

Title card reading “Ephemeral CI runner” for the HourlyVPS guide to GitHub Actions self-hosted runners on a VPS

To run a GitHub Actions self-hosted runner on a VPS, deploy an Ubuntu 24.04 server, create a non-root user, download GitHub’s runner package and register it with ./config.sh --url … --token … --ephemeral --labels …. Start it with ./run.sh or as a systemd service and send jobs to it with runs-on: [self-hosted, linux, x64, your-label]; with --ephemeral it takes exactly one job, GitHub de-registers it, and you delete the VPS so billing stops.

Do this only for private repositories: GitHub warns that forks of a public repository can run dangerous code on a self-hosted runner through a pull request. Everything below uses runner v2.337.0, the current release on October 3, 2026.

Key takeaways

  • GitHub charges nothing for self-hosted runners as of October 2026; you pay only for the server, because the $0.002-per-minute platform charge GitHub announced for March 2026 was postponed.
  • Register with --ephemeral, or create a just-in-time runner through the REST API, so GitHub assigns that runner exactly one job; deleting the VPS after a job does not guarantee that on its own.
  • Use self-hosted runners only on private repositories: GitHub warns that pull requests from forks can run code on the runner, and public repositories get standard hosted runners free.
  • GitHub's standard 2-core Linux runner costs $0.006 per minute, or $0.36 per fully busy hour; a VPS bills every hour it exists, busy or idle, so it pays off for long, heavy or frequent builds.
  • The runner needs only outbound HTTPS on port 443, so no inbound port beyond SSH has to be opened; Docker service containers with published ports are the exception, because Docker bypasses UFW.

When is a self-hosted runner on a VPS worth it?

GitHub does not charge for self-hosted runners: its billing documentation lists Actions usage on them as free, so you pay only for the machine. GitHub announced on December 16, 2025 a $0.002-per-minute platform charge for self-hosted runners from March 1, 2026, then postponed it “to re-evaluate our approach”; as of October 2026 the docs still list self-hosted usage as free. What you take on is upkeep: a GitHub-hosted runner starts every job in a clean, isolated environment, and a self-hosted runner is a server you patch and protect.

Self-host on a VPS when at least one of these is true:

  • Your private repositories use more minutes than your plan includes (2,000 a month on GitHub Free, 3,000 on Pro and Team) and keep a runner busy for long stretches.
  • Jobs need more than a standard hosted runner’s 2 CPUs, 8 GB of RAM and 14 GB of SSD; the alternative, larger runners, is sold only on the Team and Enterprise Cloud plans.
  • A job needs a fixed public IP for a database or SSH firewall that allows one address, or for your organization’s IP allow list. GitHub’s own static IP ranges exist only for larger runners on GitHub Enterprise Cloud.
  • A job runs longer than 6 hours, the cap on GitHub-hosted runners. Self-hosted jobs can run for up to 5 days, per GitHub’s Actions limits.
  • You want builds to run next to the servers they deploy to, for example in Istanbul.

Stay on GitHub-hosted runners when:

  • The repository is public: standard hosted runners are free there with no minute cap and 4 CPUs and 16 GB of RAM, and GitHub advises against self-hosted runners on public repositories.
  • Your private-repository jobs fit inside the included minutes.
  • Jobs are short and light, such as linting: GitHub’s 1-core ubuntu-slim runner (5 GB of RAM) uses the included minutes and then costs $0.002 per minute.
  • Nobody on the team wants to own a server’s updates and firewall.

Ephemeral, persistent or JIT: which runner should you use?

A runner’s registration and the VPS under it have separate lifetimes. GitHub documents three ways to register a runner, and each fits a different VPS pattern:

Runner typeHow you create itJobs per registrationWhen GitHub drops itVPS pattern
Persistentconfig.sh without --ephemeral, usually run as a serviceAny number, one at a timeWhen you remove it, or after 14 days offlineA work-session VPS for a day of builds, or an always-on runner capped at the monthly price
Ephemeralconfig.sh --ephemeralOne; GitHub de-registers it after the jobAfter its job, or after 1 day offlineOne VPS per job
Just-in-time (JIT)REST API generate-jitconfig, then run.sh --jitconfigAt most oneAfter its job; removed automatically if it never runs oneOne VPS per job, created by a script that keeps the GitHub token off the VPS
Source: GitHub Docs, Removing self-hosted runners and Self-hosted runners reference, checked 2026-10-03.

GitHub recommends implementing autoscaling with ephemeral self-hosted runners; autoscaling with persistent self-hosted runners is not recommended. In certain cases, GitHub cannot guarantee that jobs are not assigned to persistent runners while they are shut down. With ephemeral runners, this can be guaranteed because GitHub only assigns one job to a runner.

GitHub Docs, Self-hosted runners reference

So deleting the VPS after a job is not enough on its own: a persistent runner can accept a second job before your script deletes the server, and GitHub’s hardening guide notes that jobs sharing a runner can read each other’s command-line secrets with ps x -w. Register with --ephemeral or as a JIT runner, and GitHub assigns that registration exactly one job.

The price of one job per runner is a cold start. A persistent runner keeps its _work folder, tools and Docker images between jobs, so builds start warm, but it also keeps whatever the last job left behind. A fresh VPS per job starts clean, which GitHub’s reference credits with limiting exposure to sensitive resources from previous jobs.

Lifecycle of an ephemeral GitHub Actions runner on an hourly VPS: deploy the VPS and billing starts; register the runner with --ephemeral or a JIT config; the runner takes exactly one job; GitHub de-registers the runner; delete the VPS and billing stops.1. Deploy VPSbilling starts2. Register--ephemeral / JIT3. One jobruns-on match4. De-registerdone by GitHub5. Delete VPSbilling stopsEphemeral runner on an hourly VPSSteps 1 and 5 are yours; steps 3 and 4 happen on GitHub's side.

Match the pattern to the meter. Billing is by the hour: every hour a server exists is charged at the plan’s hourly rate. The price tapes and cost tables on this site count every started hour as a full hour, so they show the most a duration can cost; the cost calculator charges a partial hour to the nearest cent, as the bill does. Charges are deducted in whole cents: a full hour costs exactly the hourly rate, and a partial first or last hour is rounded to the nearest cent, never more than a full hour. A one-job hourly VPS is billed for its setup minutes as well as the job, which suits jobs of tens of minutes or more. Each new server is also ordered with its own initial credit, prepaid and used for that server’s hours, so for many short jobs keep one VPS with a persistent runner for the working session and delete it in the evening (see how hourly VPS billing works).

Is a self-hosted runner safe? Keep it off public repositories

We recommend that you only use self-hosted runners with private repositories. This is because forks of your public repository can potentially run dangerous code on your self-hosted runner machine by creating a pull request that executes the code in a workflow.

GitHub Docs, Adding self-hosted runners

Private repositories are not risk-free either. GitHub’s hardening guide says anyone who can fork the repository and open a pull request can compromise a self-hosted runner, including its secrets and the GITHUB_TOKEN. Treat the runner VPS as a machine every workflow author controls:

  • One trust boundary per VPS. Nothing on it that a workflow should not read: no production SSH keys, no cloud credentials, no personal access token.
  • A dedicated runner user without sudo. config.sh refuses to run as root and prints Must not run with sudo.
  • No inbound ports except SSH. The runner only makes outbound HTTPS connections on port 443, so the firewall from our new-VPS security checklist needs no extra rule.
  • Least-privilege tokens. Set permissions: contents: read in workflows, and use OpenID Connect instead of long-lived cloud keys, as GitHub’s guide recommends.
  • No fork pull requests. In a private repository, workflows from forks run only if the fork pull request policy under Settings → Actions → General allows it. Leave Run workflows from fork pull requests off, or require approval.
  • Repository-level runners. An organization-level runner takes jobs from many repositories; if you need one, limit its runner group to the repositories that use it.
  • Logs off the box. GitHub advises forwarding ephemeral runners’ logs to external storage; the _diag folder dies with the VPS.
  • Mining is your problem too. A miner started by a hostile job still breaks our Acceptable Use Policy, which prohibits mining on every plan, and the server can be stopped.

Docker means root, and its ports skip UFW. Jobs that use container actions or service containers need Docker on a Linux runner. Adding the runner user to the docker group gives every job root-level access to the VPS, as our Docker install guide explains. Accept that only on a single-purpose runner VPS you delete after use.

When a job maps service-container ports with ports:, GitHub publishes them with Docker’s --publish, which binds to all host addresses and bypasses UFW, so a test database is reachable from the internet while the job runs. Run the job itself in a container:, and its service containers need no published ports.

What size VPS does a GitHub Actions runner need?

GitHub says the runner application “only requires minimal resources”, so size the VPS for your jobs. Guides that list 2 cores and 7 GB of RAM as a requirement are repeating an older spec of GitHub’s own hosted runners, not a runner requirement. Today GitHub’s standard Linux runner for private repositories has 2 CPUs, 8 GB of RAM and 14 GB of SSD, and its 4-core larger runner has 16 GB; use those as the bar for parity.

JobsPlanPer hourReasoning
Lint, unit tests, deploy scripts over SSHQuartz Q2: 1 vCPU, 2 GB, 50 GB NVMe$0.02/hourThe runner needs little; 2 GB leaves room for a Node.js or Python test run.
Docker builds, service containers, mid-size test suitesQuartz Q4: 2 vCPU, 4 GB, 80 GB NVMe$0.03/hourImages and build layers fill disk fast; 80 GB is several times a hosted runner’s 14 GB.
Sustained compiles; parity with GitHub’s 2-core, 8 GB runnerChrono C8: 2 dedicated vCPU, 8 GB, 100 GB NVMe$0.07/hourDedicated vCPUs are designed for sustained CPU work; under our AUP, a shared-vCPU Quartz server at full load for long periods may be CPU-limited.
Rust, C++, Android, large monoreposChrono C16: 4 dedicated vCPU, 16 GB, 200 GB NVMe$0.13/hourSame CPU count and RAM as GitHub’s 4-core larger runner.
Sizing is reasoning from GitHub’s documented runner specs and requirements, not a benchmark.

The network bar is low: at least 70 kilobits per second each way and outbound HTTPS to github.com, api.github.com, *.actions.githubusercontent.com and a few artifact and update domains. Istanbul plans include the monthly traffic allowance listed for each plan, prorated for a server that exists for part of a billing period. Docker Hub’s pull rate limit always applies to self-hosted runners, GitHub notes, so log in to Docker Hub in workflows that pull often. Pick the location closest to what your jobs talk to.

How to set up a GitHub Actions self-hosted runner on a VPS

These steps register a repository-level ephemeral runner on Ubuntu 24.04 LTS, following GitHub’s Adding self-hosted runners page. Debian 12 works the same way, except that UFW is not installed by default (sudo apt install ufw); the runner supports Ubuntu 20.04 or later and Debian 10 or later. You must own the repository, or have admin access to it if it belongs to an organization.

Step 1: Deploy the VPS and lock it down

Deploy Ubuntu 24.04, log in over SSH with your key, and work as a sudo user rather than root. Update the system and turn on the firewall with only SSH allowed:

sudo apt update && sudo apt upgrade -y
sudo ufw allow OpenSSH
sudo ufw enable

No other port is needed. The runner opens outbound connections to GitHub and never listens for inbound ones.

Step 2: Create a runner user without sudo

sudo adduser --disabled-password --comment "GitHub Actions runner" runner

--disabled-password skips the password prompt; you reach the account with sudo -iu runner, so it needs no password. Do not add it to the sudo group. GitHub’s hosted runners give jobs passwordless sudo, so a workflow step such as sudo apt-get install fails here; install those packages yourself in step 4.

Step 3: Download and verify the runner

Switch to the runner user and download the Linux x64 package. Version 2.337.0 was released on August 26, 2026 and is still the latest on October 3, 2026; the New self-hosted runner page in your repository settings always shows the current version and its checksum.

sudo -iu runner
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.337.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.337.0/actions-runner-linux-x64-2.337.0.tar.gz
echo "70920811a4f8ad4328818682bca5c6469c1c942fab52448868071d0063816613  actions-runner-linux-x64-2.337.0.tar.gz" | sha256sum -c

The check must print OK; the hash is the SHA-256 published on the release page. Then unpack:

tar xzf ./actions-runner-linux-x64-2.337.0.tar.gz

Step 4: Install the runner’s dependencies and your toolchain

The runner is a .NET application that needs libraries such as ICU and OpenSSL. The package ships a script that installs them with apt; it must run as root, so type exit to return to your sudo user first:

sudo /home/runner/actions-runner/bin/installdependencies.sh

Install what your jobs need now, as your sudo user: compilers, language runtimes, or Docker from our Docker on Ubuntu 24.04 guide. Jobs then never need sudo.

Step 5: Get a registration token

In the repository, open Settings → Actions → Runners → New self-hosted runner and copy the token from the configure command. It expires after one hour. To script it, call the REST API from your own computer with a fine-grained token whose repository permission Administration is set to write (GitHub permission table):

curl -L -X POST \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer $GH_TOKEN" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  https://api.github.com/repos/OWNER/REPO/actions/runners/registration-token

The token field of the response goes into the next step. Your personal access token stays on your computer and never touches the VPS.

Step 6: Register the runner as ephemeral

sudo -iu runner
cd actions-runner
./config.sh --unattended --url https://github.com/OWNER/REPO --token REGISTRATION_TOKEN --name hvps-ny-01 --labels hvps,ny --ephemeral
FlagWhat it does
--urlThe repository (or organization) the runner joins.
--tokenThe one-hour registration token from step 5.
--unattendedNo prompts; defaults fill anything you leave out.
--nameHow the runner appears under Settings → Runners. Add --replace to take over an existing name.
--labelsCustom labels, added to the defaults self-hosted, linux and x64.
--ephemeralOne job, then the runner is de-registered and its local configuration deleted.
Flags from the runner’s own help text (v2.337.0) and GitHub Docs.

Step 7: Start it, one-shot or as a service

For a one-job VPS, run the runner in the foreground, inside tmux if your SSH session might drop:

./run.sh

When it prints Connected to GitHub and Listening for Jobs, the runner shows as Idle under Settings → Actions → Runners. After its one job it de-registers and exits, and the VPS is ready to delete.

For a runner that should survive reboots, install the systemd service with the svc.sh script that config.sh created. svc.sh must run as root from the runner directory, so run it from your sudo user:

sudo bash -c 'cd /home/runner/actions-runner && ./svc.sh install runner && ./svc.sh start'

The unit is named actions.runner.OWNER-REPO.RUNNER-NAME.service, truncated past 80 characters, so read the exact name from the .service file the installer writes, then follow the log:

sudo cat /home/runner/actions-runner/.service
sudo journalctl -u actions.runner.OWNER-REPO.hvps-ny-01.service -f

With --ephemeral, the service stops after the first job instead of restarting: the runner deletes its local configuration and exits cleanly, and the service wrapper does not relaunch a clean exit. Use the service for a persistent runner (registered without --ephemeral) on a work-session or always-on VPS.

Ubuntu and Debian: where needrestart is enabled, it can restart the runner service in the middle of a job after package upgrades. GitHub’s docs give this exclusion, run as your sudo user:

echo '$nrconf{override_rc}{qr(^actions\.runner\..+\.service$)} = 0;' | sudo tee /etc/needrestart/conf.d/actions_runner_services.conf

How do jobs reach the runner? Using runs-on labels

A self-hosted runner gets three default labels: self-hosted, an OS label (linux) and an architecture label (x64, ARM or ARM64). runs-on lists labels, and a runner must carry all of them to receive the job:

name: ci
on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  test:
    runs-on: [self-hosted, linux, x64, hvps]
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v6
      - run: make test

GitHub’s routing rules decide what happens next:

  • A job goes only to a runner that is online and idle, and each runner registers to run one job at a time. For parallel jobs, register more runners (below).
  • If a runner does not pick up an assigned job within 60 seconds, the job is re-queued for another runner.
  • With no matching runner online, the job waits in the queue and fails after 24 hours. Jobs queued after you delete the VPS wait for a runner that will not come back.
  • A custom label such as hvps keeps jobs off your other self-hosted machines, and ny or ist labels let each job pick a location.
  • timeout-minutes stops a hung job from holding the runner, and the VPS hours, any longer than you allow.

Parallel jobs: several runners on one VPS

To run two jobs at once on one VPS, register a second runner in its own directory, because each runner keeps its registration files there. As the runner user, with the package from step 3 and a token from step 5:

mkdir ~/actions-runner-2 && cd ~/actions-runner-2
tar xzf ~/actions-runner/actions-runner-linux-x64-2.337.0.tar.gz
./config.sh --unattended --url https://github.com/OWNER/REPO --token REGISTRATION_TOKEN --name hvps-ny-01b --labels hvps,ny

Install its service from /home/runner/actions-runner-2 as in step 7; unit names include the runner name, so both coexist. Size for both jobs at once: two jobs that each fit GitHub’s 2-core, 8 GB runner fit a Chrono C16 (4 dedicated vCPU, 16 GB).

A long-lived runner also fills its disk with checkouts in _work and Docker leftovers. Between work sessions, as your sudo user:

sudo docker system prune -f

It removes stopped containers, unused networks, dangling images and unused build cache (Docker reference); -a also removes unused images. A one-job VPS never needs it: deleting the server discards the disk.

Just-in-time runners: one job, no registration token on the VPS

A just-in-time (JIT) runner skips config.sh. A script on your computer, or on a small controller that holds your GitHub token, asks the REST API for a runner configuration, and the VPS starts the runner with it. GitHub’s hardening guide offers JIT runners “to improve runner registration security”: each one performs at most one job and is then removed automatically.

This Python script uses only the standard library. It expects a fine-grained token with Administration: write on the repository in the GH_TOKEN environment variable:

#!/usr/bin/env python3
"""Print a just-in-time (JIT) runner config for one GitHub Actions job."""
import json
import os
import sys
import urllib.request

repo, name = sys.argv[1], sys.argv[2]  # "OWNER/REPO", unique runner name
body = {
    "name": name,
    "runner_group_id": 1,  # the default runner group
    "labels": ["self-hosted", "linux", "x64", "hvps"],
    "work_folder": "_work",
}
req = urllib.request.Request(
    f"https://api.github.com/repos/{repo}/actions/runners/generate-jitconfig",
    data=json.dumps(body).encode(),
    method="POST",
    headers={
        "Accept": "application/vnd.github+json",
        "Authorization": f"Bearer {os.environ['GH_TOKEN']}",
        "X-GitHub-Api-Version": "2026-03-10",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp)["encoded_jit_config"])

Run it on your computer with the repository and a unique runner name:

python3 jit_config.py OWNER/REPO hvps-ny-02

runner_group_id 1 is the value in GitHub’s REST example and the one Actions Runner Controller uses when no group is named; an organization can list its group IDs with GET /orgs/ORG/actions/runner-groups. Pass every label your runs-on line needs, self-hosted, linux and x64 included: config.sh is what adds those defaults, and a JIT runner never runs it. Copy the printed string, then on a VPS prepared with steps 1 to 4, as the runner user:

cd ~/actions-runner
read -rs JIT
./run.sh --jitconfig "$JIT"

read -rs waits for you to paste the string without echoing it, which keeps it off the screen and out of your shell history. The configuration is a credential for one runner, so never commit it. The runner exits after its job, and the VPS is ready to delete.

Past a handful of runners, GitHub points to the workflow_job webhook, its open-source Runner Scale Set Client (actions/scaleset) and, on Kubernetes, Actions Runner Controller. The HourlyVPS platform also has a REST API. Ask support whether your account can create and delete servers through it; if it can, one script can deploy the VPS, request the JIT config and delete the server afterwards. Until then, deploy and delete in the portal and script only the GitHub side.

How do you remove a self-hosted runner and stop billing?

An ephemeral or JIT runner removes itself after its job. A persistent runner needs four steps before you delete the VPS:

  1. Stop and uninstall the service, if you installed one.
  2. Get a remove token: open the runner under Settings → Actions → Runners and click Remove, or call POST /repos/OWNER/REPO/actions/runners/remove-token. It expires after one hour.
  3. Run ./config.sh remove as the runner user. It removes the runner from GitHub and deletes its configuration files on the machine; on Linux it stops with Uninstall service first while a service is still installed.
  4. Revoke any token you created for the setup, copy off the logs you need, and delete the VPS with our checklist for deleting a VPS.

The commands for steps 1 and 3:

sudo bash -c 'cd /home/runner/actions-runner && ./svc.sh stop && ./svc.sh uninstall'
sudo -iu runner
cd actions-runner
./config.sh remove --token REMOVE_TOKEN

If the VPS is already gone, click Force remove this runner on the same page, or call DELETE /repos/OWNER/REPO/actions/runners/RUNNER_ID. Leave it, and GitHub removes it automatically after 14 days offline, or after 1 day for an ephemeral runner.

Stopping is not deleting. A stopped server is still billed, because its vCPU, memory, disk and IP addresses stay reserved for you; only deleting the server stops billing. A shut-down runner VPS also stays in GitHub’s list as Offline until it is removed.

What does a CI hour cost: VPS vs GitHub-hosted minutes?

GitHub bills hosted runners per minute, rounds each job up to the next whole minute, and charges only while jobs run. A VPS bills for time, busy or idle, so the comparison turns on how busy the runner is. GitHub’s Linux x64 rates as of October 2026, next to the HourlyVPS plan with the same vCPU count and RAM:

SizeGitHub-hosted runnerGitHub rateGitHub, fully busy hourHourlyVPS planHourlyVPS, per hour
2 vCPU, 8 GBLinux 2-core standard (14 GB SSD)$0.006/min$0.36Chrono C8 (100 GB NVMe)$0.07/hour
4 vCPU, 16 GBLinux 4-core larger runner (150 GB SSD)$0.012/min$0.72Chrono C16 (200 GB NVMe)$0.13/hour
8 vCPU, 32 GBLinux 8-core larger runner (300 GB SSD)$0.022/min$1.32Chrono C32 (400 GB NVMe)$0.25/hour
16 vCPU, 64 GBLinux 16-core larger runner (600 GB SSD)$0.042/min$2.52Chrono C64 (800 GB NVMe)$0.48/hour
GitHub rates from Actions runner pricing and specs from GitHub Docs, checked 2026-10-03. Included plan minutes do not apply to larger runners. HourlyVPS prices render from our current price list.

Two rules turn the table into a decision:

  • Break-even: divide a plan’s hourly price by GitHub’s per-minute rate. The result is the number of busy build minutes per hour at which the VPS costs the same as hosted minutes; above it, the VPS costs less.
  • Included minutes first: inside your plan’s free minutes, standard hosted runners cost nothing, and in public repositories they are free entirely. Self-hosting saves money only on minutes you would otherwise pay for.

A worked example: one 8-hour working day on a 2 vCPU, 8 GB runner. Chrono C8 for those 8 hours costs $0.56 however many jobs run; the GitHub column grows with build minutes beyond your included quota.

Busy build minutes in the 8 hoursGitHub-hosted Linux 2-coreChrono C8 for the 8 hours
60$0.36$0.56
120$0.72$0.56
240$1.44$0.56
480 (busy all day)$2.88$0.56
GitHub figures are minutes × $0.006, before per-job rounding, for minutes beyond the included quota.

If the runner should stay up all week or all month, the table below shows what the same hourly billing adds up to. A day is 24 billed hours, and an always-on runner never costs more than the plan’s monthly price in a billing period (one month from your order date), so there is no plan to switch to when the runner gets busy. Model your own pattern in the VPS cost calculator, and see hourly vs monthly VPS billing for how a capped hourly bill compares with providers that bill hourly without a cap.

Chrono C8 as a CI runner: one job, a workday, a day, a week, always on
DurationHours on the meterCost $0.07/hour · cap $35.00/monthNote
1 hour1$0.07
8 hours8$0.56
1 day24$1.68
7 days168$11.76
30 days720$35.00Capped at the monthly price
Charges stop at $35.00 after 500 hours (about 20.8 days) in a billing period; the rest of that period is free.

Every plan’s hourly price and monthly cap are on the pricing page. Every server comes with one dedicated IPv4 address and IPv6, included in the plan price.

Troubleshooting a self-hosted runner on a VPS

SymptomLikely causeFix
Must not run with sudo or Must not run interactively with sudoconfig.sh or run.sh started as rootRun them as the runner user (sudo -iu runner).
Dependencies is missing for Dotnet Core 6.0ICU, OpenSSL or other libraries missingRun sudo /home/runner/actions-runner/bin/installdependencies.sh as your sudo user (step 4).
Registration failsToken older than one hour, or the URL points at the wrong repositoryRequest a new token and check --url.
Job stays queuedNo online, idle runner carries every runs-on label; or the ephemeral runner already used its one jobCompare labels with Settings → Runners; register a new runner. Queued jobs fail after 24 hours.
Runner shows Offlinerun.sh or the service stopped, or outbound HTTPS on 443 is blockedCheck the service with journalctl, then run the connectivity check below.
Service stopped after one jobExpected with --ephemeralRegister again, or delete the VPS.
Must run as sudosvc.sh started as the runner userRun it with sudo from your sudo user (step 7).
Uninstall service firstconfig.sh remove while the service is installedRun svc.sh stop and svc.sh uninstall with sudo, then remove.
permission denied on /var/run/docker.sockRunner user cannot reach DockerAdd it to the docker group (root-equivalent, see above) and restart the service.
Jobs stop being queued to the runnerRegistered with --disableupdate and more than 30 days behind a releaseUpdate the runner; GitHub also blocks runners that miss a critical security update.
Runner messages from its source (v2.337.0); behavior from GitHub’s monitoring and troubleshooting page.

For network problems, the runner tests every GitHub endpoint it needs and prints PASS or FAIL for each. It needs a classic token with the workflow scope, or a fine-grained token with workflows read and write access:

./config.sh --check --url https://github.com/OWNER/REPO --pat YOUR_TOKEN

The runner’s own logs are in the _diag folder of the runner directory: Runner_ files for the application and Worker_ files for each job.

Deploy this setup

Run an ephemeral GitHub Actions runner, then delete it

Chrono C8 · 2 dedicated vCPU · 8 GB RAM · 100 GB NVMe · Istanbul

  • Per hour$0.07/hourFor this job
  • Per day (24 h)$1.68/day
  • Monthly cap$35.00/month
Deploy Chrono C8

Starts with a $5 initial credit, which goes into the server’s balance and pays for its hours.

Billed by the hour, never more than $35.00 per billing period. Delete the server and billing stops.

FAQ

Are GitHub Actions self-hosted runners free?

Yes, on GitHub's side. GitHub's billing docs (checked October 2026) say Actions usage is free for self-hosted runners, so you pay only for the machine. A $0.002-per-minute platform charge announced for March 2026 was postponed and has not taken effect.

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

The runner application itself needs only minimal resources, according to GitHub, so size for your jobs. For parity with GitHub's standard Linux runner for private repositories, use 2 vCPUs and 8 GB of RAM; a 2 GB server leaves room for light lint and test jobs.

Can I use a self-hosted runner with a public repository?

GitHub recommends against it, because a pull request from a fork can run code on your runner machine. Public repositories get standard GitHub-hosted runners free, so use those instead.

Can one self-hosted runner run several jobs at the same time?

No. A runner takes one job at a time, and GitHub assigns jobs only to an online, idle runner. For parallel jobs, register several runners, each in its own directory with its own name, on one VPS or on several.

What is the difference between an ephemeral runner and a JIT runner?

Both run a single job. An ephemeral runner is registered with config.sh --ephemeral and a one-hour registration token on the machine; a JIT runner starts from a configuration created through the REST API, so no registration token is needed on the runner.

What happens to the runner if I delete the VPS?

It shows as Offline in your runner list. GitHub removes it automatically after 14 days offline, or after 1 day for an ephemeral runner, and you can force-remove it sooner from the runner settings or the REST API.

How long can a job run on a self-hosted runner?

Up to 5 days per job, against 6 hours on GitHub-hosted runners. A job that finds no matching runner waits in the queue for up to 24 hours and then fails.

Sources

  1. Adding self-hosted runnersGitHub Docs · docs.github.com · checked
  2. Self-hosted runners reference (requirements, ephemeral runners, communication)GitHub Docs · docs.github.com · checked
  3. Configuring the self-hosted runner application as a serviceGitHub Docs · docs.github.com · checked
  4. Using self-hosted runners in a workflowGitHub Docs · docs.github.com · checked
  5. Removing self-hosted runnersGitHub Docs · docs.github.com · checked
  6. Secure use reference: hardening for self-hosted runnersGitHub Docs · docs.github.com · checked
  7. REST API endpoints for self-hosted runnersGitHub Docs · docs.github.com · checked
  8. Actions runner pricingGitHub Docs · docs.github.com · checked
  9. GitHub Actions billingGitHub Docs · docs.github.com · checked
  10. actions/runner release v2.337.0GitHub · github.com · checked
All posts