RunZero Documentation
Everything you need to know about configuring, running, and scaling your local GitHub Actions runner fleet with Docker containers, dedicated Linux Virtual Machines, and intelligent proxy caching.
Quick Start in 60 Seconds
RunZero deploys an autoscaler daemon alongside local caching proxies (apt-cacher-ng for Debian/Ubuntu, Verdaccio for npm, Athens for Go, and Docker Hub mirror).
# 1. Clone the repository
git clone git@github.com:el-j/run-zero.git
cd run-zero
# 2. Launch the interactive configuration wizard
make env
# 3. Build runner images (ARM64 + AMD64)
make build
# 4. (Optional for VM speeds) Build golden VM base template
make build-vm-base
# 5. Start the autoscaler and caching proxies in the background
make start
# 6. Stream live runner logs
make logsPrerequisites
RunZero runs on macOS, Linux, and Windows with minimal system requirements:
- Container Runtime: OrbStack (recommended for macOS), Docker Desktop, or native Linux Docker.
- GitHub Personal Access Token (PAT): Classic PAT with
reposcope (andadmin:orgif managing an Organization fleet). - Hardware: Apple Silicon (M1/M2/M3/M4) or Intel/AMD x86_64 CPU, 4GB+ RAM.
Interactive Setup Wizard (make env)
Running make env invokes an interactive terminal wizard that asks for your token, account username, desired engine, and concurrency limit:
If run non-interactively (e.g. in CI with NON_INTERACTIVE=true), make env automatically copies .env.example to .env without blocking.
Real-Time Web Dashboard & Host VM Bridge
RunZero features a built-in, lightweight real-time Web UI Dashboard and Host VM Bridge. Monitor active ephemeral runners, GitHub API quotas, hybrid routing distributions, and cache storage with zero external SaaS dependencies.
1. Real-Time Web Dashboard (Port 49505)
The Web Dashboard starts automatically alongside the autoscaler daemon on http://localhost:49505.
# Quick launch in your default web browser
make dashboard
# Or start the full fleet and navigate directly to:
open http://localhost:49505Key Dashboard Capabilities
- Live Ephemeral Runner Fleet: Real-time status halos, active duration stopwatches, target repo/workflow name, and runtime engine (Docker vs OrbStack / WSL2 / Multipass VM).
- Server-Sent Events (SSE) Stream: Instant UI updates delivered via
/api/eventswith zero page polling and zero socket bloat. - GitHub API Quota Tracker: Visual gauge tracking live GitHub REST API rate-limit headroom.
- Hybrid Routing Breakdown: Real-time counts of jobs routed to Docker vs VM backends (Services/DIND, Browser E2E, Systemd).
- Proxy Cache Analytics: Disk usage breakdown across NPM (Verdaccio), Go (Athens), APT, Docker, and Toolcache directories, with one-click purge actions.
- Streaming Terminal Log Viewer: Live tailing ring buffer of autoscaler daemon logs with auto-scroll and pause controls.
Dashboard Configuration Options
| Variable | Default | Description |
|---|---|---|
DASHBOARD_ENABLED | true | Enables the built-in HTTP and SSE dashboard server. |
DASHBOARD_HOST | 0.0.0.0 | Host address the dashboard binds to. |
DASHBOARD_PORT | 49505 | Web UI dashboard listening port. |
GitHub Actions Minutes Panel Setup
The dashboard's GitHub Actions Minutes panel uses GitHub billing endpoints and does not reuse API rate-limit values. Configure account scope and token permissions correctly to populate this panel with real data.
# Personal scope
OWNER=your-username
# Organization scope (optional)
# ORG=your-org-name
# Token used by autoscaler API + billing lookups
ACCESS_TOKEN=ghp_xxx
# Optional polling cadence for billing panel
ACTIONS_BILLING_REFRESH_INTERVAL=300- Primary runner operations: Token needs repository access for queue discovery and runner registration.
- Minutes panel: Token also needs billing-read access for the target owner/org scope.
- Validation: If permissions are insufficient, panel status shows a clear error instead of fake numbers.
Full HOWTO: ACTIONS_BILLING_HOWTO.md
2. Host VM Bridge (Port 49504)
When running RunZero's autoscaler inside a Docker container (make start-container), native virtualization CLI tools like orbctl, wsl.exe, or multipass are not directly executable within the container. The Host VM Bridge bridges this gap by exposing a zero-dependency HTTP control daemon on the host.
# Start the Host VM Bridge daemon on the host
make bridge-start
# Check bridge daemon status and health
make bridge-status
# Stream bridge logs
make bridge-logs
# Stop the bridge daemon
make bridge-stopBridge Configuration Options
| Variable | Default | Description |
|---|---|---|
HOST_VM_BRIDGE_PORT | 49504 | Port the host bridge HTTP daemon listens on. |
HOST_VM_BRIDGE_URL | http://host.docker.internal:49504 | URL the containerized autoscaler uses to reach the host bridge. |
Dual-Engine Fleet Architecture
Unlike traditional runner solutions that force you into a single execution model, RunZero offers a pluggable driver architecture:
🪐 OrbStack Linux VM Driver (RUNNER_BACKEND=orbstack-vm)
Spawns dedicated lightweight Linux machines via OrbStack and Apple Virtualization.framework. Provides full systemd support, unconfined Google Chrome sandboxing, background daemons, and internal Docker-in-VM capabilities. Automatically connects to host Verdaccio, Athens, and apt-cacher proxies via host.orb.internal.
🏗️ Golden VM Base Image (make build-vm-base)
To make VM boot times near-instant, make build-vm-base pre-bakes the shared toolchain into a golden base image (runzero-vm-base-<arch>). Subsequent VM jobs spin up in seconds by cloning the base image!
🐳 Docker Container Driver (RUNNER_BACKEND=docker)
Launches ultra-fast ephemeral containers powered by Docker Engine. Starts in ~0.3 seconds and consumes zero RAM when idle. Ideal for unit tests, linting, build pipelines, and JavaScript/Python workflows.
🪟 Windows WSL2 Driver (RUNNER_BACKEND=wsl2)
Runs native Linux runners directly inside Windows Subsystem for Linux 2 (Windows 10/11 and Windows Server) without requiring Docker Desktop.
🚀 Canonical Multipass Driver (RUNNER_BACKEND=multipass)
Universal cross-platform virtual machine backend using Canonical Multipass via QEMU, Hyper-V, or VirtualBox.
Hybrid Auto-Routing
With RUNNER_BACKEND=auto and AUTO_ROUTE_VM=true, you don't need to choose a single engine. The autoscaler inspects workflow labels and job names:
# Standard unit test runs in ultra-fast Docker container (~0.3s)
jobs:
test:
runs-on: [ self-hosted, local ]
steps:
- run: npm test
# Lighthouse / Browser E2E automatically routes to dedicated Linux VM!
jobs:
audit:
runs-on: [ self-hosted, local, browser ] # Triggers: 'browser', 'e2e', 'lighthouse', 'systemd', 'vm'
steps:
- run: npx lhci autorunAPT & Package Proxy Caching
RunZero runs lightweight background caching proxy services so that neither your container image builds nor your CI runner executions waste bandwidth re-downloading packages from public registries:
| Proxy Service | Target Packages | Host Port | Web UI / Dashboard |
|---|---|---|---|
| APT Cacher NG | Ubuntu/Debian .deb packages | :49503 | http://localhost:49503/acng-report.html |
| Verdaccio | NPM, Yarn, PNPM JavaScript packages | :49501 | http://localhost:49501 |
| Athens | Go language modules | :49500 | http://localhost:49500 |
| Docker Registry Mirror | Docker Hub container layers | :49502 | http://localhost:49502 |
| devpi | Python pip & uv packages (PyPI) | :49507 | http://localhost:49507/root/pypi/+simple/ |
| kellnr | Cargo / Rust crates (crates.io) | :49506 | http://localhost:49506 |
Cargo has no single "index URL" environment variable the way pip/uv (PIP_INDEX_URL/UV_INDEX_URL) and Go (GOPROXY) do -- pointing it at kellnr requires a real ~/.cargo/config.tomlsource-replacement block, written automatically for both the Docker and OrbStack VM engines. pip also refuses a plain-HTTP index on any host other than localhost/127.0.0.1 unless it's explicitly trusted via PIP_TRUSTED_HOST, which both engines set automatically alongside PIP_INDEX_URLwherever the index isn't reached via localhost.
OrbStack VM Local Disk Caching
The proxies above cache package downloads over the network. Package managers also keep a local, already-extracted disk cache (~/.npm, ~/.cache/pip, ~/go/pkg,~/.cargo/registry, etc.), which for the Docker engine is a plain -v host:containerbind mount. An OrbStack VM is a real, separate guest filesystem with no such flag, but every non-isolated OrbStack VM automatically virtiofs-shares the entire host macOS filesystem into the guest at a fixed path,/mnt/mac<absolute-macOS-path>. RunZero's OrbStack driver uses this to bind-mount each host-side cache directory onto its container-style destination path inside the VM before the job runs, so cache data written by one ephemeral VM is really on host disk and visible to the next VM cloned for the same architecture.
Job Environment & the RUNZERO Marker
Every runner, on every backend, exports the same variables beforerun.sh, so every job step inherits them, the way GitHub's own images set theirs. Each one fixes a cache miss that was measured on real jobs:
| Variable | Value | Why |
|---|---|---|
RUNNER_TOOL_CACHE, AGENT_TOOLSDIRECTORY | /opt/hostedtoolcache | Without them actions/setup-node and setup-godownload into the throwaway _work/_tool instead of the host-backed tool cache. |
npm_config_registry, pnpm_config_registry,YARN_REGISTRY, YARN_NPM_REGISTRY_SERVER | Verdaccio | pnpm 11 ignores every npm_config_* variable and pnpm 10 reads only those, so both spellings are set. |
pnpm_config_store_dir, npm_config_store_dir | ~/.local/share/pnpm/store | The pnpm store is a host cache mount. |
PLAYWRIGHT_BROWSERS_PATH | ~/.cache/ms-playwright | Browsers are cached on the host, per architecture. |
RUNZERO, RUNZERO_CACHES | 1, list of mounted caches | Lets a workflow tell it is running on RunZero. |
On RunZero, actions/cache steps for node_modules, the pnpm store or Playwright browsers only duplicate, slowly and over the network, what the host already caches. Skip them with the marker; the same workflow keeps caching normally on GitHub-hosted runners:
- uses: actions/cache@v4
if: env.RUNZERO != '1'
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('pnpm-lock.yaml') }}
# The runner images already contain Playwright's system packages, so
# --with-deps only adds an apt-get update on RunZero.
- run: pnpm exec playwright install ${{ env.RUNZERO == '1' && '' || '--with-deps' }} chromiumIf a job still needs apt-get, it goes through apt-cacher-ng. The runner images keep their package indices and skip translation files, soapt-get update fetches only what changed since the image was built. Repositories served over HTTPS (Microsoft, Docker) bypass the proxy.
Run make doctor to check all of this from inside a fresh runner. For each backend it reports the registry and store that npm, yarn and pnpm 10/11 resolve, the tool cache and Playwright mounts, proxy latency, cache sizes, host load, runner sizing, and whether the bridge and autoscaler run the code you have checked out.
Native-Arch Override (Apple Silicon)
A workflow that targets runs-on: [self-hosted, local, amd64]has to keep working on GitHub-hosted ubuntu-latest. On Apple Silicon, every such job runs under Rosetta emulation, which is several times slower for Node and browsers, even though most jobs (Node, pnpm, Playwright, Go) do not depend on the architecture.
With NATIVE_ARCH_OVERRIDE=all (or a list ofowner/repo names) RunZero serves those repositories'amd64 jobs with native arm64 runners, with no change to the workflow. The runner still carries the job's amd64 label, butRUNNER_ARCH inside it is ARM64, sosetup-node and Playwright fetch native binaries. x86-64 binaries and --platform linux/amd64 containers still run through Rosetta. The startup log, the job log line and the dashboard's routing card show when an override was applied.
Trade-off: a job that checks uname -m, or builds amd64 artifacts without --platform, gets arm64. Leave such repositories off the list. Jobs labelled rosetta orx86_64 are never moved, and the setting does nothing on amd64 hosts.
First-Level Dependencies & Upstream Tools
RunZero builds upon best-of-breed open-source virtualization and runtime platforms:
- OrbStack: Superfast, lightweight container engine and Linux Virtual Machine hypervisor for macOS.
- Docker Engine & Compose: Industry standard container runtime and service orchestration.
- apt-cacher-ng: High-performance caching proxy for Debian and Ubuntu APT package downloads.
- Verdaccio: Lightweight zero-config private npm proxy registry with web interface.
- Athens: Enterprise-grade Go module caching proxy server.
- NVM (Node Version Manager): Multi-version Node.js environment pre-baking Node 20, 22, and 24.
- Microsoft .NET SDK: Cross-platform .NET 8.0 SDK runtime for C# and F# builds.
- Playwright: Reliable end-to-end testing for modern web apps with pre-installed OS dependencies.
Zombie Runner Healing & Auto-Reconciliation
If an ephemeral runner machine is stopped abruptly mid-job (e.g. during workstation sleep or make stop), GitHub might mark it as offline but busy: true.
RunZero's autoscaler automatically reconciles zombie runners by cancelling stranded runs and unsticking GitHub's runner queue so jobs never hang.
Environment Variables Reference (.env)
| Variable | Default | Description |
|---|---|---|
ACCESS_TOKEN | Required | GitHub Personal Access Token (classic with repo scope). For Actions minutes panel, token must also have billing-read access for target owner/org. |
OWNER | Required | GitHub username to monitor for auto-discovery. |
RUNNER_BACKEND | auto | Execution engine: auto, docker, orbstack-vm, wsl2, multipass. |
AUTO_ROUTE_VM | true | Automatically route jobs with browser/e2e/systemd labels to VMs. |
RUNNER_ARCH | both | Target architecture: both (ARM64 + AMD64), arm64, amd64. |
ACTIVE_REPO_DAYS | 60 | Only monitor repositories with commits in the last N days. |
MAX_RUNNERS | 4 | Maximum concurrent runner instances allowed on this workstation. |
RUNNER_CPUS / RUNNER_MEMORY | derived | Per-runner CPU and memory limit for Docker containers, OrbStack job VMs (set on every clone) and Multipass VMs. Unset, each runner gets an equal share of the host (OrbStack: its VM pool) after keeping 1 CPU and 2 GiB back: (cpus - 1) // MAX_RUNNERS. The startup log and the dashboard show the sizing and warn when MAX_RUNNERS × the limit exceeds the host. |
RUNNER_SIZING | auto | unlimited restores the old behavior: no limits, every runner may claim every host core. |
CLEANUP_RUNNERS_ON_SHUTDOWN | false | Destroy runners when the autoscaler stops. Off by default, so restarting or redeploying the autoscaler no longer cancels jobs that are running; runners are ephemeral and exit after their job. |
NATIVE_ARCH_OVERRIDE | off | off, all, or comma-separated owner/repo names. On Apple Silicon, serves those repositories' amd64-labelled jobs with native arm64 runners, without changing the workflow. See Native-arch override. |
PROXIES_ENABLED | true | Start apt-cacher, Verdaccio, Athens, and Docker Hub caching proxies. |
RATE_LIMIT_REFRESH_INTERVAL | 60 | Polling interval (seconds) for GitHub API quota telemetry updates. |
ACTIONS_BILLING_REFRESH_INTERVAL | 300 | Polling interval (seconds) for GitHub Actions minutes billing updates. |
Makefile Command Reference
make start # Start Autoscaler and Proxy registries (apt-cacher, Verdaccio, Athens)
make stop # Stop Autoscaler, Proxies, and cleanup active runners
make status # Show status of services and active runner instances
make doctor # Verify cache wiring from inside throwaway runners (no GitHub token needed)
make logs # Stream live autoscaler logs
make test # Run 85 unit tests directly (0.04s)
make test-suite # Run Flake8 linter, Mypy type-checker, and Pytest coverage
make install-hooks # Install pre-commit quality guard into .git/hooks/pre-commit
make pre-commit # Run pre-commit quality guard manually with auto-fixes
make lint # Run Flake8 linter and Mypy static type checker
make lint-fix # Auto-fix Python formatting and strip trailing whitespace
make build-vm-base # Build golden OrbStack VM base image for instant cloning
make website-dev # Start Astro documentation website development server
make website-build # Build Astro static website and synchronize to docs/
make verdaccio-ui # Open Verdaccio NPM registry dashboard (http://localhost:49501)
make apt-cacher-ui # Open apt-cacher-ng statistics report (http://localhost:49503/acng-report.html)
make clean-cache # Clear shared package caches to reclaim disk spaceTroubleshooting & FAQ
How do I test a workflow on Apple Silicon and AMD64?
Set RUNNER_ARCH=both in .env and specify runs-on: [self-hosted, local, arm64] or runs-on: [self-hosted, local, amd64] in your GitHub workflow YAML.
How does package caching work across containers and VMs?
RunZero exposes apt-cacher-ng on port 49503, Verdaccio on port 49501, and Athens on port 49500. Containers on host networking and VMs on virtualized bridges automatically route package downloads through these local proxies.