Skip to content

Repository files navigation

gh-runner-android

A self-hosted GitHub Actions runner in a container, built from the official actions/runner release tarball with an Android/Gradle toolchain layered on top.

Standalone by design: point it at a different repo by editing REPO_URL in .env. Registration is scoped by an API call, not by where these files live, so target repo and location on disk are independent.

  • Current target: utilityvault (Android, Gradle 9.3.1, AGP 9.1.1, compileSdk 36.1)
  • Host: macOS / Apple Silicon, 8 GB, running the image under x86_64 emulation
  • Scope: single repo, PAT auth, github.com, persistent (not ephemeral)

Registration, polling and clean deregistration are verified working. The known limitation is memory, not mechanism: ci.yml's two-flavour lint asks for a 4 GB Gradle heap and the Docker VM has 3.83 GB, so that particular job gets OOM-killed — see docs/decisions.md.

Quickstart

cp .env.example .env      # paste a fine-grained PAT, set REPO_URL
./test_token.sh           # sanity check, no network needed
docker compose up -d --build
docker compose logs -f    # expect "Listening for Jobs"

Then confirm the runner appears under the target repo's Settings → Actions → Runners, and that docker compose down makes it disappear rather than go offline.

Full walkthrough, including how to create the PAT: docs/setup.md.

How it works, briefly

The Dockerfile downloads the runner tarball — Runner.Listener (long-polls GitHub for jobs), Runner.Worker (spawned per job), and a bundled Node.js that every uses: action runs on. At container start, entrypoint.sh trades the PAT for a short-lived registration token, registers with config.sh, runs the listener, and deregisters on shutdown.

Details: docs/architecture.md.

Layout

Dockerfile            JDK 21 + Android SDK + gh + git + runner tarball (all pinned)
entrypoint.sh         register / run / deregister — deliberately toolchain-free
token.sh              PAT -> 1-hour registration token
docker-compose.yml    cache volumes, linux/amd64, shutdown grace period
hooks/post-job.sh     prunes build output past a size threshold
test_token.sh         asserts REPO_URL -> API path parsing
.env                  PAT, REPO_URL, labels (gitignored)
docs/                 reference documentation — start at docs/README.md
reference/            myoung34's image, for reading only (gitignored)

Documentation

Page Covers
docs/architecture.md Registration, polling and shutdown mechanics
docs/setup.md PAT creation, first run, retargeting, version bumps
docs/workflows.md Required .github/workflows/ edits in the target repo
docs/decisions.md Why each choice, and what was deliberately cut
docs/reference.md myoung34's implementation, mapped
docs/troubleshooting.md Symptoms and fixes

Credits and prior art

Built by reading myoung34/docker-github-actions-runner (MIT) rather than depending on it — the goal was to learn the actual mechanism instead of a wrapper's env-var interface over it. Its trap-and-deregister shutdown pattern and registration-token call are the parts most directly borrowed, and it remains the recommendation for anyone who needs org/enterprise scope or GitHub App auth.

A clone lives in reference/ (gitignored) while this project is being built. See docs/reference.md for a file-by-file map of what it does and which parts were used.

Also relevant, and not used:

  • actions/runner — the runner agent itself. Its prebuilt release tarball is what this image installs; compiling the C# source ourselves would add nothing.
  • actions/runner-images — Packer definitions for GitHub's hosted runner VMs. Not Docker, not runnable here, and would bring a 20GB+ image of tools this project does not need.

Security

On a persistent self-hosted runner, a PR from a fork would execute untrusted code on this machine with access to the cache volumes. ci.yml is wired so that pull_request always resolves to a hosted runner and only a manual dispatch can select self-hosted — fork PRs cannot reach it. Keep it that way: pointing any automatic trigger at this runner makes the risk live again. See docs/workflows.md.