A self-hosted GitHub Actions runner image based on the official
ghcr.io/actions/actions-runner
image, extended with build tooling: a JDK, common command-line tools, and on-demand Android SDK
setup.
ghcr.io/blafry/actions-runner:latest
| Component | Details |
|---|---|
| Base | The official runner image as published by GitHub (user runner, Docker CLI, runner container hooks) |
| JDK | Eclipse Temurin 21 from the Adoptium APT repository (JAVA_HOME=/usr/lib/jvm/temurin-21-jdk-amd64) |
| Tools | python3, zip, unzip, curl, jq, git |
| Android SDK | Installed on first use by ensure-android-sdk (see below) into ANDROID_HOME=/home/runner/android-sdk |
PATH includes the JDK, cmdline-tools/latest/bin and platform-tools (adb).
The Android SDK License Agreement does not allow redistributing the SDK, so the published image
doesn't contain it. Instead, the runner runs /usr/local/bin/ensure-android-sdk before every job
(ACTIONS_RUNNER_HOOK_JOB_STARTED):
- On the first job it downloads the command-line tools (SHA-1 verified) and installs
ANDROID_SDK_PACKAGESwith the Android CLI. That takes about 20-60 seconds. - On later jobs it returns immediately, as long as
ANDROID_HOMEis on a persistent volume.
Installing the SDK this way accepts the
Android SDK Terms of Service on behalf of whoever
runs the runner. To bake the SDK into a private image instead, build it yourself with
--build-arg BAKE_ANDROID_SDK=true.
RUNNER_VERSION is required (there is deliberately no default):
docker build --build-arg RUNNER_VERSION=2.x.y -t actions-runner .| Variable | Default | Meaning |
|---|---|---|
ANDROID_HOME / ANDROID_SDK_ROOT |
/home/runner/android-sdk |
SDK location; mount a volume here |
ANDROID_SDK_PACKAGES |
platform-tools platforms/android-37.0 build-tools/36.0.0 |
Packages to keep installed (Android CLI syntax) |
ANDROID_SDK_SKIP |
unset | 1 skips the SDK setup (runners that never build Android) |
CMDLINE_TOOLS_ZIP, CMDLINE_TOOLS_SHA1 |
pinned | Command-line tools archive and checksum |
docker volume create android-sdk
docker run -d --name runner \
-v android-sdk:/home/runner/android-sdk \
-v gradle-cache:/home/runner/.gradle \
ghcr.io/blafry/actions-runner:latest \
bash -c './config.sh --url https://github.com/<owner>/<repo> --token <registration-token> --unattended --ephemeral && ./run.sh'Runner scale set values:
template:
spec:
securityContext:
fsGroup: 1001 # lets the runner write to the mounted volumes
containers:
- name: runner
image: ghcr.io/blafry/actions-runner:latest
command: ["/home/runner/run.sh"]
resources:
requests: { cpu: "2", memory: 6Gi }
limits: { cpu: "4", memory: 8Gi }
volumeMounts:
- { name: android-sdk, mountPath: /home/runner/android-sdk }
- { name: gradle, mountPath: /home/runner/.gradle }
volumes:
- name: android-sdk
persistentVolumeClaim: { claimName: android-sdk }
- name: gradle
persistentVolumeClaim: { claimName: gradle-cache }Instrumented tests need a device:
- adb over Wi-Fi (simplest in containers):
adb connect <device-ip>:5555at the start of the job, after enabling wireless debugging on the device once. - USB: pass the device into the container (
--device /dev/bus/usbwith Docker, a hostPath mount and a node selector on Kubernetes) and accept the debugging prompt on the device. Persist/home/runner/.androidso the adb key survives restarts.
Emulators need /dev/kvm and a lot more resources; they are not the focus of this image.
Typical Gradle Android builds with unit tests:
| vCPU | RAM | Disk | |
|---|---|---|---|
| Minimum | 2 | 6 GB | 20 GB |
| Recommended | 4 | 8 GB | 30 GB |
| Large projects | 8 | 16 GB | 50 GB |
On small runners, limit Gradle in ~/.gradle/gradle.properties, e.g.
org.gradle.jvmargs=-Xmx2g, kotlin.daemon.jvmargs=-Xmx1536m, org.gradle.workers.max=2.
| Tag | Meaning |
|---|---|
latest |
Newest build |
<runner version> (e.g. 2.x.y) |
Newest build on that upstream runner version |
<runner version>-<YYYYMMDD> |
A specific build |
A build happens only when it is needed (.github/scripts/resolve-versions.sh):
- Weekly (Fridays 04:20 UTC), the workflow compares the newest version of the official runner image (its newest
x.y.ztag) with the newest version built here, and builds only if upstream is newer. - Pushes to
masterthat change the image (Dockerfile,rootfs/, scripts, the workflow) always build, on the newest upstream version. - On demand (
workflow_dispatch) it behaves like the weekly run;forcebuilds anyway.
Every build is smoke-tested before it is published (runner user and version, JDK, SDK setup, adb).
The files in this repository are under the MIT License. The base image and the
software installed into containers keep their own licenses (GitHub Actions runner: MIT; Eclipse
Temurin: GPLv2 with Classpath Exception; Android SDK: Android SDK License Agreement, accepted by
whoever runs ensure-android-sdk).