Skip to content
 
 

Repository files navigation

docker-github-actions-runner-android

Based off https://github.com/myoung34/docker-github-actions-runner but adds the android sdk into the container so we don't need to run the setup-android action every time from: https://github.com/android-actions/setup-android

The Android SDK requires Java, so the container includes a Temurin JDK as well, which means you can also skip the setup-java action: https://github.com/actions/setup-java

Node.js LTS is also baked in (see matrix.json for available versions), so actions/setup-node is unnecessary for LTS workflows.

Images are published as multi-arch manifests for both linux/amd64 and linux/arm64, so Apple Silicon Macs (via Docker Desktop / OrbStack) and ARM Linux hosts run natively without emulation.

To run the container, check out the environment variables from the base image: https://github.com/myoung34/docker-github-actions-runner#environment-variables

docker-compose.yml is the X64 beelink layout — three persistent runners, each with its own work and cache volumes — not a side file. After pulling a new image on the host:

docker compose pull && docker compose up -d

Existing named runner-work* volumes keep their data; new cache volumes start empty and warm on first jobs. Do not share one bun/gradle volume across the three containers: concurrent jobs would corrupt the same tree.

X64 cache volumes

This compose is for the X64 class (beelink, docker, large). System labels (Linux, X64) are still applied by the runner binary; set LABELS in .env to beelink,docker,large (do not add a build label). EPHEMERAL stays false so the volumes survive between jobs.

Each runner gets its own:

Mount Volume (runner / runner-2 / runner-3) Why
/tmp/runner/work runner-work* Job workspace. Cleanup spares $RUNNER_WORKDIR.
/root/.bun runner-bun* Bun install cache
/root/.npm runner-npm* npm cache
/root/.cache/pnpm runner-pnpm* pnpm store
/root/.gradle runner-gradle* Gradle modules, build-cache, transforms
/root/.cargo runner-cargo* Cargo registry

Budget ~20 GB per runner (enforced by the post-job hook with LRU, not a wipe), ~60 GB per beelink. ARM64 / Pi hosts should not mount these cache volumes — those runners isolate to RUNNER_TEMP.

Do not tmpfs /tmp. RUNNER_WORKDIR is /tmp/runner/work; a tmpfs or a blanket find /tmp -delete hides or wipes the work volume.

Workflows must use the volume homes

Named volumes are unused if the job relocates the caches. On these X64 self-hosted runners, workflows must not isolate GRADLE_USER_HOME or BUN_INSTALL_CACHE_DIR (or npm_config_cache / CARGO_HOME) to $RUNNER_TEMP. That is the setup-stack local-home mode in dodi-smart/.githubdeps-verify stays isolated; pr-checks uses the default homes so the volumes are actually read.

The other half of cache poison is GitHub actions/cache with restore-keys re-importing a partial bun tarball. Use an exact key, or do not upload the bun install cache at all.

Image tags

Images are published to GitHub Container Registry (GHCR):

  • ghcr.io/dodi-smart/github-runner

Tag scheme

Each image is built with a specific combination of base runner version, JDK version, Node major version, and Android SDK level. Tags let you choose how tightly to pin:

Tag Example What it pins What floats
latest latest Nothing Runner, JDK, Node, and SDK all track latest
jdk<VERSION> jdk21 JDK major version Runner, Node (default), and SDK track latest
node<VERSION> node22 Node major version Runner, JDK (default), and SDK track latest
jdk<JDK>-node<NODE> jdk21-node22 JDK + Node Runner and SDK track latest
<RUNNER>-jdk<JDK>-sdk<SDK>-node<NODE> 2.332.0-jdk21-sdk36-node22 Everything Nothing — fully pinned

Which tag should I use?

  • latest — always the newest runner, default JDK, default Node LTS, and latest Android SDK. Good for staying current, but builds may break if a new SDK introduces incompatibilities.
  • jdk21 / jdk17 — pin the JDK version but still get runner, Node, and SDK updates automatically. A good balance for most users.
  • jdk21-node22 — pin both JDK and Node majors; runner and SDK still track latest. Useful when an LTS-pinned Node is part of your build's contract.
  • 2.332.0-jdk21-sdk36-node22 — fully pinned. Use this for reproducible builds where you want complete control over when to upgrade. Old pinned tags are never overwritten; they remain in the registry from previous builds.

What's in each image?

Every image includes:

  • The myoung34/github-runner base image
  • Temurin JDK (the version in the tag)
  • Node.js LTS (the version in the tag) — installed from NodeSource, replacing the base image's older system Node
  • Android SDK platform matching the SDK level in the tag (e.g., sdk36 = platforms;android-36)
  • Latest Android build-tools and NDK for that SDK level
  • Android SDK command-line tools, platform-tools, and cmake
  • Multi-arch manifest covering linux/amd64 and linux/arm64

Projects that need additional SDK platforms (e.g., an older compileSdk) can install them at build time:

- run: sdkmanager "platforms;android-28"

Currently available JDK versions

The build matrix is defined in matrix.json. Check that file for the current list of JDK versions and which one is the default.

Currently available Node versions

Also defined in matrix.json — see node_versions for the current LTS majors and default_node for the version applied to short tags like latest and jdk<VERSION>.

Automated updates

Versions are kept up to date automatically:

Component Mechanism
Base runner image Renovate tracks myoung34/github-runner Docker tags
GitHub Actions versions Renovate (config:recommended)
New JDK major versions Weekly workflow queries the Adoptium API and opens a PR
New Node LTS majors Weekly workflow queries the Node Release schedule and opens a PR
Android SDK platform level Weekly workflow queries sdkmanager and opens a PR
Android build-tools Weekly workflow queries sdkmanager and opens a PR
Android NDK Weekly workflow queries sdkmanager and opens a PR

Every pull request is validated by build.yml, which builds each JDK against the default Node, each Node against the default JDK, and one linux/arm64 build of the default pair. Its All builds passed job is the single check to require in branch protection.

Because this repository is a fork, renovate.json sets "forkProcessing": "enabled". The Mend-hosted Renovate App leaves fork processing disabled for org-wide ("All repositories") installations, so without that flag Renovate silently skips the repository and the base image is never bumped.

The weekly SDK/JDK/Node workflow opens its PR with GITHUB_TOKEN, which means GitHub does not run build.yml on it. Upstream solves this with an AUTOMERGE_PAT secret and gh pr merge --auto; this fork does not, so those PRs are merged manually and the builds run on merge.

Runner registration preflight

The image wraps the base entrypoint with preflight-entrypoint.sh. When CONFIGURED_ACTIONS_RUNNER_FILES_DIR is set, the upstream entrypoint reuses persisted registration credentials without checking them, so a runner that was removed on GitHub's side crash-loops forever instead of re-registering. The preflight asks GitHub whether the persisted runner still exists and wipes the stored registration only on a definitive 404, letting a fresh ACCESS_TOKEN registration take over. Timeouts, missing variables, and unparseable state all leave the configuration untouched.

Building locally

docker build -f Dockerfile .

Build args can be used to customize the image. See the top of the Dockerfile for the current defaults and available args (VERSION, JAVA_VERSION, NODE_VERSION, COMPILE_SDK, BUILD_TOOLS, NDK_VERSION, SDK_TOOLS).

Example:

docker build --build-arg JAVA_VERSION=17 --build-arg NODE_VERSION=22 --build-arg COMPILE_SDK=35 -f Dockerfile .

Inspiration

About

Based off myoung34's container but adds java + android sdk

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages