A self-hosted, Docker-based pool of ephemeral GitHub Actions runners.
🍴 Dogfooded — this repo's own CI was running on a gharp pool during development!
- 🔐 Self-hosted — no external service dependency
- ♻️ Ephemeral runners — one job per runner, clean environment every time
- ⚡ Autoscaling — runners are created on-demand from webhook events
- 📊 Built-in dashboard — inspect jobs and runners (with prometheus metrics), retry/cancel controls, and in-place credential rotation (webhook secret / pem / client secret) gated behind a
ALLOW_ADMIN_EDITkill-switch - 📦 Multi-repository, personal-account support — share compute across repos (not supported natively by GitHub)
👉 Try the dashboard live — same UI, mock data, no backend. Click Cancel / Retry to see how the dashboard reacts.
Pre-built multi-arch image: muhac/gharp.
Minimal docker run
docker run -d --name gharp \
-p 8080:8080 \
-e BASE_URL=https://gharp.example.com \
-v /var/run/docker.sock:/var/run/docker.sock \
-v gharp-data:/data \
muhac/gharp:1BASE_URL must be a public HTTPS URL GitHub can reach, terminating at
the container's port 8080 (above mapped to the host's 8080). See
docs/configuration.md for the full env-var reference.
Recommended — Docker Compose
# copy docker-compose.yml from this repo, then:
BASE_URL=https://gharp.example.com docker compose up -dSee docker-compose.yml for the full reference configuration
(workdir cleanup, host Docker socket forwarding for ephemeral runners,
runner-container log caps, and ADMIN_TOKEN passthrough — set the
token in .env).
Open ${BASE_URL}/setup and click Create GitHub App. gharp drives
the GitHub App Manifest flow and persists the credentials locally.
⚠️ Don't rename the auto-generated App name on GitHub. gharp creates the App asgharp-<hash>; renaming it changes the slug, which breaks the install link gharp renders on/setup. The webhook keeps working — only the install link goes stale. To fix, delete the App on GitHub and re-run/setup.
⚠️ BASE_URLis sticky. It's baked into the GitHub App's webhook and OAuth-callback URLs at/setuptime. Changing it later won't reconfigure the App — gharp will log aBASE_URL driftwarning at startup. To migrate, re-run/setup(creating a fresh App) or revertBASE_URLto the original value.
Pick the repos (or "All repositories") you want runners for and submit.
⚠️ Self-hosted runners + public repos = remote code execution. GitHub explicitly recommends against using self-hosted runners with public repositories: any contributor who can open a PR can run arbitrary code on your machine. Only install the App on private repos you trust, and run gharp on a dedicated VM / cloud instance / homelab node. gharp drops public-repoworkflow_jobwebhooks by default. To opt in, setALLOW_PUBLIC_REPOS=truefor all public repos, or useREPO_ALLOWLIST=owner/repoto bypass the guard for selected public repos.
jobs:
build:
runs-on:
- self-hosted
steps:
- uses: actions/checkout@v4
- run: echo "hello from $(hostname)"
test:
runs-on:
- self-hosted
- "gharp-test-${{ github.run_id }}-${{ github.run_attempt }}"
steps:
- run: echo "tests from $(hostname)"Every workflow_job whose runs-on set is fully covered by
RUNNER_LABELS (default self-hosted; self-hosted is implicit on
every self-hosted runner so you don't need to list it) or a configured
dynamic label prefix (default gharp-) will get a fresh runner. Jobs
requiring a label this pool doesn't advertise are dropped — see
docs/configuration.md.
For the full deployment guide (from-source build, docker compose,
volumes, upgrades, ops APIs, troubleshooting), see docs/deploy.md.
GitHub does not support "user-level" runners.
- Runners are scoped to: repository, organization, or enterprise
This makes it hard to:
- share a runner across multiple repositories
- use self-hosted runners efficiently in personal accounts
- scale runners dynamically
💡 This project solves that
- Uses GitHub App + webhook (
workflow_job) - Dynamically creates ephemeral runners per job
- Automatically cleans up after execution
👉 You get GitHub-hosted-like behavior on your own machine
flowchart TB
subgraph setup["One-time setup"]
direction LR
U[User] -- POST manifest --> GH1[GitHub]
GH1 -- code + slug --> G1[gharp]
G1 -- App credentials --> DB[(sqlite)]
U -- install App --> GH1
end
subgraph runtime["Per-job runtime"]
direction LR
GH2[GitHub] -- workflow_job webhook --> G2[gharp]
G2 -- record job --> DB2[(sqlite)]
G2 -- registration token --> GH2
G2 -- docker run --> R[ephemeral runner]
R -- runs the job --> GH2
R -- one job, then exit --> X((removed))
end
setup --> runtime
See docs/architecture.md for the full design,
including the GitHub App Manifest flow, sqlite job durability, and
permission scopes.
- Go (server)
- Docker (runner execution)
- GitHub App (auth + webhook)
Apache License 2.0 — Copyright 2026 Muhan Li.