Skip to content
prostractionPublic

About

CLI utility for hosting multiple Github Runners on a single host (using Docker)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Runners

Runners is a lightweight, Go-based CLI utility designed to manage multiple GitHub Actions self-hosted runners on a single host using Docker or Podman. Every runner is isolated with Sysbox and has its own private Docker daemon, resource limits, and an unprivileged job user by default.

Features

  • Sysbox Isolation: Each runner runs under the sysbox-runc runtime in its own user namespace (root inside maps to an unprivileged host UID), without --privileged and without the host Docker socket. Jobs cannot see or control other runners, their containers, or the host.
  • Resource Constraints: Define hard limits for CPU (cores) and Memory (MB) per runner.
  • Dynamic Updates: Modify CPU and Memory limits for running containers using the Docker Container Update API without downtime.
  • Private Docker per Runner: docker run, docker build, container jobs and service containers in a workflow use a dockerd running inside that runner's container. Images and containers are not shared between runners.
  • Unprivileged Jobs: jobs run as the runner user and sudo is disabled (no-new-privileges). Add a runner with --root to run jobs as root with working sudo.
  • Monitoring: Real-time tracking of runner status, uptime, and cumulative error counts.
  • Bulk Operations: start, stop, reboot, update, and remove accept any list of runner names or --all for the whole fleet.
  • Persistent Configuration: Local JSON-based configuration management.

Prerequisites

  • Go 1.25 or higher
  • Linux host with a rootful container engine:
    • Docker Engine, or
    • Podman: the local podman command plus its API service (systemctl enable --now podman.socket), with runners run as root. Sysbox does not officially support Podman (nestybox/sysbox#128); see Podman for how the two are made to work together.
  • Sysbox (sysbox-runc runtime). install.sh installs it on Debian/Ubuntu; on other distributions build it from source first. Manual setup:
    • Docker: the Sysbox package registers the runtime in /etc/docker/daemon.json itself.
    • Podman: register it in /etc/containers/containers.conf (or a file in /etc/containers/containers.conf.d/):
      [engine.runtimes]
      sysbox-runc = ["/usr/bin/sysbox-runc"]
  • Permission to use the engine socket (e.g. membership in the docker group). Rootful Podman's socket is root-only, so run runners as root (config then lives in /root/.runners).

runners add checks the engine before doing anything and refuses rootless engines or a missing sysbox-runc runtime with a hint.

The engine socket is taken from DOCKER_HOST, then CONTAINER_HOST, then the first one that answers of /var/run/docker.sock, /run/podman/podman.sock, $XDG_RUNTIME_DIR/podman/podman.sock. A socket file with no listener behind it (e.g. left over from a removed Docker) is skipped.

Podman has no daemon to bring containers back after a reboot: enable podman-restart.service (install.sh does). Runner containers use the always restart policy on Podman (unless-stopped on Docker), so a runner stopped with runners stop also comes back after a host reboot.

Installation

Linux

  1. Clone the repository.
  2. Execute the installation script as your regular user (it calls sudo where needed):
    chmod +x install.sh
    ./install.sh

The script is safe to re-run and:

  • compiles the binary and installs it to /usr/local/bin/runners;
  • picks the engine: a running Docker, otherwise Podman (override with --engine docker|podman);
  • installs the latest Sysbox release if sysbox-runc is missing, verifying its checksum (pin a version with SYSBOX_VERSION=0.7.1, skip with --skip-sysbox);
  • on Podman: enables podman.socket and podman-restart.service (ordered after Sysbox), registers sysbox-runc in /etc/containers/containers.conf.d/50-sysbox.conf, and starts a throwaway test container under Sysbox to check that it runs and has network access.

On a Docker host with existing containers the script asks before installing Sysbox, because the package restarts Docker (--yes skips the question).

Windows

  1. Ensure Go is installed.
  2. Build the executable:
    go build -o runners.exe

Usage

Commands Overview

Command Description Multi-target
add Register and start a new GitHub runner No
list List all runners with status, uptime, and limits N/A
log Print a log from runner's container (Docker syntax) N/A
start Start one, several, or all stopped runners name... or --all
stop Stop one, several, or all running runners name... or --all
reboot Restart one, several, or all runners (alias: restart) name... or --all
update Update resource limits (CPU/RAM) dynamically name... or --all
remove Stop, remove container, and delete configuration name... or --all

Multi-target commands accept any number of runner names. Unknown names are skipped with a warning and the remaining runners are still processed. --all (-a) ignores positional args and applies to every configured runner in alphabetical order.

Examples

Registering a new Runner

Requires a registration token from GitHub (Settings -> Actions -> Runners). Tokens expire after 1 hour.

runners add --name runner-01 --url https://github.com/org/repo --token TOKEN --cpu 1.0 --memory 2048

Quotes are optional. Values are cleaned up before use: surrounding whitespace, CR from CRLF scripts, invisible characters and leftover quotes ('...' in cmd.exe, typographic quotes) are stripped. The URL may omit https:// and may be any page of the repo/org copied from the browser (e.g. .../settings/actions/runners/new?arch=x64) — it is reduced to https://github.com/org/repo (or /org, /enterprises/name). Labels may be written as --labels gpu, linux, x64.

Jobs run as the unprivileged runner user with sudo disabled. If workflows need sudo apt-get install ..., add the runner with --root:

runners add --name runner-02 --url https://github.com/org/repo --token TOKEN --root

The runner user can still use the runner's private Docker, and through it become root inside that runner's container (e.g. docker run -v /:/host ...). Sysbox keeps that root away from the host and from other runners. Non-root mode keeps ordinary job steps from changing the runner itself; it is not a barrier against a job that sets out to.

Error state

When the runner hits an error that retrying cannot fix (invalid or expired token, wrong URL, runner deleted on GitHub), it is halted instead of being restarted in a loop: list shows the Error status and runners logs runner-01 shows the reason. Fix the cause, then runners reboot runner-01 (or remove and add again with a fresh token).

Monitoring

runners list

Resource Management

Update limits for one, several, or all runners. Changes apply immediately to running containers.

# Single runner
runners update "runner-01" --cpu 2.0 --memory 4096

# Several runners at once
runners update "runner-01" "runner-02" --cpu 1.0 --memory 2048

# All runners
runners update --all --cpu 0.5 --memory 1024

Lifecycle Control

Pass any number of runner names, or use --all (alphabetical order). Unknown names are skipped without aborting the rest.

# Start a few specific runners
runners start "runner-01" "runner-02"

# Stop several runners
runners stop "runner-01" "runner-03" "runner-05"

# Reboot all runners
runners reboot -a

# `restart` is an alias for `reboot`
runners restart "runner-01" "runner-02"

# Wipe everything
runners remove --all

Technical Architecture

  • Runner Image: Utilizes myoung34/github-runner:latest.
  • Isolation: containers run with --runtime=sysbox-runc and START_DOCKER_SERVICE=true (dockerd inside the container). The only host path mounted is the runner's own ~/.runners/data/<name>. Non-root runners also get --security-opt no-new-privileges. The image entrypoint stays root, and the supervisor drops the listener to runner with gosu.
  • Configuration: State is persisted in ~/.runners/config.json.
  • Process Ordering: --all operations follow the same lexicographical sorting as the list command for deterministic behavior. Multi-name invocations preserve the order given on the command line.
  • Supervisor: the container command is a small bash supervisor around ./run.sh. When the listener reports a non-retryable "terminated error" (or the runner never got configured), it writes ~/.runners/data/<name>/runners-error and parks the container, so Docker's restart policy does not loop. Session conflicts are retried.
  • Graceful Shutdown: containers stop with SIGINT and a 60s timeout. With the default SIGTERM the image's entrypoint died at once, the listener was killed without deleting its GitHub session, and after a restart the runner looped on A session for this runner already exists. Containers created by older versions (including all pre-Sysbox ones) are re-created (registration kept) on reboot, or on start if they are stopped. After upgrading, run runners reboot --all.
  • Auto-Update Resilience: Containers are launched with ./run.sh as the entrypoint command, so the upstream actions/runner update loop (exit code 3 → apply staged update → relaunch listener) runs inside a single container instead of crashing it. start / reboot automatically recreate containers that exited with a non-zero code, so a single reboot --all heals runners broken by a botched self-update.

Podman

Podman selects the OCI runtime per process (podman --runtime ...), not per container: a runtime requested through its Docker-compatible or native API is ignored and the container ends up under the default runtime without Sysbox. On Podman, runner containers are therefore created with the podman command line and managed through the API afterwards:

  • podman --runtime sysbox-runc create --uidmap 0:<start>:65536 --gidmap 0:<start>:65536 ..., where <start> is the range the Sysbox service reserves for the sysbox user in /etc/subuid and /etc/subgid (the same range Sysbox gives containers under Docker). With Podman setting up the user namespace, the container's network namespace is created inside it; without that Sysbox cannot start the container (write sysctl key net.ipv4.ping_group_range: permission denied).
  • --ulimit nofile=1048576:1048576 --ulimit nproc=-1:-1 (Docker's defaults): the image's init script for the inner dockerd raises these limits and aborts when it cannot.
  • Resource limits are changed with podman update; the restart policy is fixed at creation.
  • runners must run as root, so that the command line and the rootful API service share one container store.

Uninstallation (Linux)

To remove the binary and stop all associated containers:

chmod +x uninstall.sh
./uninstall.sh

About

CLI utility for hosting multiple Github Runners on a single host (using Docker)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages