skip to content

How do you register a self-hosted GitHub Actions runner, and how does it receive jobs?

level: middleimportance: should knowfreq 62%

answer

  1. Two scripts: one configures, one runs
  2. The registration credential is deliberately short-lived
  3. Which direction does the connection go?
  4. Registration also assigns routing labels
  5. One runner, one job at a time

basics

~20 s

You download GitHub's runner package onto the machine and run config.sh with the target URL and a short-lived registration token, then run.sh or the service installer. The runner then polls GitHub over outbound HTTPS for jobs; GitHub never connects inbound.

solid answer

~40 s

Registration is: unpack the runner release on the machine, run `./config.sh --url https://github.com/OWNER/REPO --token <registration token>` (the token is generated in the repository, organization or enterprise Actions settings, or via the REST API, and expires within about an hour), then start it with `./run.sh` in the foreground or install it as a service with `./svc.sh install` so it survives reboot. Once configured the runner **long-polls GitHub over outbound HTTPS 443** and pulls work; you do not open any inbound firewall port, which is what makes self-hosted runners viable inside a private network. Registration assigns default labels — `self-hosted`, the OS (`Linux`, `Windows`, `macOS`) and the architecture (`X64`, `ARM64`) — plus any custom labels you pass with `--labels`, and those labels are what `runs-on` matches.

code

bash · 8 lines
bash
mkdir actions-runner && cd actions-runner
# download and extract the runner release for this OS/arch, then:
./config.sh --url https://github.com/acme/api \
            --token AXXXXXXXXXXXXXXXXXXXXXXXXX \
            --labels gpu,build-fleet \
            --unattended
sudo ./svc.sh install
sudo ./svc.sh start

go deeper

for a junior

Know that a self-hosted runner is an agent you install and register against a repository or organization, and that labels are how a job finds it.

for a middle

Explain the configure-then-run split, the short-lived registration token, the outbound-only long poll, and the default plus custom labels registration assigns.

for a senior

Demonstrate operating a fleet: services that survive reboot, deregistration of dead machines, auto-update behaviour, and diagnosing state left behind on a shared workspace.

for a principal

Own provisioning strategy — token minting at boot versus just-in-time configuration, fleet scoping into runner groups, and sizing by queue depth rather than by CPU utilisation.

## The registration handshake A self-hosted runner is a small agent you install on your own machine. The lifecycle has three distinct steps that candidates routinely blur together. **1. Configure.** You download the runner release, then run the configuration script with the scope URL and a **registration token**: ``` ./config.sh --url https://github.com/acme/api --token AXXXXXXXXXXXXXXXXXXXXXXXXX --labels gpu,fast ``` The registration token is deliberately short-lived (roughly an hour) and is not a credential the runner keeps. `config.sh` exchanges it for durable runner credentials stored on disk in `.credentials`, writes `.runner`, and creates the `_work` directory where jobs will be checked out. Because the token is short-lived, automated provisioning has to mint one at boot — from the REST API or `gh` — rather than baking one into an image. **2. Run.** `./run.sh` starts the agent in the foreground; `./svc.sh install && ./svc.sh start` registers it with the OS service manager so it restarts after reboot. On Linux the service runs as the configured user, and *that user's* privileges are the job's privileges. **3. Deregister.** `./config.sh remove --token <removal token>` cleanly unregisters. A machine that is destroyed without this leaves an offline runner entry that GitHub eventually reaps, but which can keep matching-label jobs looking busy in the meantime. ## How work arrives The runner opens an outbound HTTPS connection to GitHub and long-polls for assigned jobs. There is **no inbound connection**: GitHub never dials your machine, so no port forwarding, no public IP, no inbound firewall rule. Egress to GitHub's endpoints is the only network requirement, which is exactly why self-hosted runners are the standard way to reach a private database, an internal artifact registry, or on-prem hardware from CI. ## Scope and labels A runner is registered at repository, organization, or enterprise scope. Repository-scoped runners serve one repository; org-scoped runners are shared and are placed into **runner groups** that control which repositories may use them. Labels are the routing mechanism: registration always adds `self-hosted`, an OS label and an architecture label, and `--labels` adds your own (`gpu`, `macos-m2`, `build-fleet`). A job asking for `runs-on: [self-hosted, gpu]` will only land on a runner carrying both. ## Operational realities The runner application **auto-updates itself** by default; `--disableupdate` opts out but then you own patching, and a too-old runner is refused work. Jobs execute directly on the host: the workspace under `_work` is reused, and anything a job leaves behind — installed tools, Docker images, credential files, a poisoned `~/.gradle` — is visible to the next job on that machine. That persistence is simultaneously the performance advantage (warm caches, no re-provisioning) and the security and flakiness hazard that pushes teams toward ephemeral runners. ## Capacity One runner executes **one job at a time**. Concurrency comes from running more runner processes or more machines, not from a setting. Sizing a self-hosted fleet is therefore a queue-depth exercise: measure jobs waiting versus runners idle, not CPU.

  • Do you need to open an inbound firewall port for a self-hosted GitHub Actions runner?
    No. The runner establishes an outbound HTTPS connection to GitHub and long-polls for work, so only egress to GitHub's endpoints is required. This is precisely why self-hosted runners are used to reach private networks: the runner sits inside the perimeter and reaches out, rather than GitHub reaching in.
  • How do you get two jobs running concurrently on one self-hosted machine?
    Run two runner instances. A single runner process executes exactly one job at a time; there is no parallelism setting. Configure each instance in its own directory with its own credentials and service, and size the machine for the combined load — two runners on a two-core box mostly buys you contention.
  • Why does automated provisioning fail if you bake the registration token into a machine image?
    Registration tokens expire in about an hour, so an image built yesterday holds a dead token. Provisioning must mint one at boot through the REST API or gh, or use just-in-time runner configuration, which issues a single-use config for a runner that is created at launch.

saying these in an interview costs you the question

  • Claiming GitHub connects inbound to the runner
  • Thinking the registration token is a long-lived credential
  • Expecting one runner to run several jobs at once
  • Assuming self-hosted runners are wiped between jobs
  • Registering runners by hand and never deregistering dead ones

context