Skip to content

About

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

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

docker-github-actions-runner-android

A self-hosted GitHub Actions runner image, plus a Docker Compose setup for running it at scale. The image builds on myoung34/github-runner and adds:

  • a Temurin JDK, the Android SDK and Node.js LTS, so jobs can skip setup-java, setup-android and setup-node;
  • multi-arch images (linux/amd64, linux/arm64).

The Compose setup adds:

  • ephemeral runners with persistent cache volumes;
  • a pruner service that does the maintenance;
  • a local action archive cache that avoids GitHub's download rate limits.

Quick start

  1. Create a GitHub App for runner registration. See the base image docs.
  2. Copy .env.example to .env and set APP_ID, APP_PRIVATE_KEY and APP_LOGIN.
  3. Start the runners:
    docker compose up -d

docker-compose.yml starts three runners and a pruner. Each runner is ephemeral: it takes one job, then restarts and registers again with a clean container. Only the named volumes survive between jobs.

To update a host, run docker compose pull && docker compose up -d.

Fit it to your setup

One host or many. Run the same Compose file on every host and change only .env. To run more or fewer runners per host, add or remove runner-N services, along with their volumes.

Different kinds of machines. Give each host the capability labels your workflows select on, through LABELS in its .env. For example, build for general jobs and heavy for memory-hungry ones. Workflows then ask for runs-on: [self-hosted, Linux, build]. Label by what a machine can do, not by its name. A host can carry several labels, so a large machine labelled build,heavy also picks up general jobs when the others are busy.

Small hosts. Boards such as a Raspberry Pi have little disk. Layer hosts/no-caches.compose.yml by setting COMPOSE_FILE in .env. Those runners skip the per-runner cache volumes.

Repository or enterprise runners. The Compose file registers runners for an organization (RUNNER_SCOPE: org). For other scopes, change the runner environment to what the base image supports.

Your own image. Set RUNNER_IMAGE to use a fork or a pinned tag. See Image tags.

Caches

Each runner gets its own volume for these caches:

  • bun, npm and pnpm
  • Gradle
  • Cargo and rustup
  • Kotlin/Native
  • mise and hk

The image sets MISE_DATA_DIR, MISE_CACHE_DIR and HK_CACHE_DIR so those tools write there.

  • Never share a cache volume between runners. Concurrent jobs would corrupt it.
  • Keep the default cache locations in workflows. If a job moves GRADLE_USER_HOME or BUN_INSTALL_CACHE_DIR into $RUNNER_TEMP, the volumes go unused.
  • Turn off the GitHub cache where a volume replaces it. For example, use cache: false on jdx/mise-action.
  • Don't put /tmp on tmpfs. The job workspace is /tmp/runner/work.

Action archive cache

Ephemeral runners download every uses: action on every job. Several runners behind one IP address soon hit 429 Too Many Requests in "Set up job".

The runner can read action archives from a local directory before it downloads anything. Set ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE to that directory. Here it is /opt/actionarchivecache, mounted read-only into every runner. The runner never writes this directory itself, so the pruner fills it every hour. It uses the GitHub App's token, so the downloads count against the App's rate limit rather than your IP address. It fetches:

  • every uses: found in the repositories listed in PREFETCH_SOURCES, such as the repo that holds your reusable workflows;
  • the actions listed in PREFETCH_EXTRA.

With neither variable set, the cache stays empty and jobs download as usual. A cache miss always falls back to a normal download.

To fill the cache straight away:

docker compose run --rm pruner prefetch

To confirm it works, re-run a job with debug logging. "Set up job" then shows Copied action archive '/opt/actionarchivecache/...'.

Maintenance

The pruner service (pruner.sh) keeps slow work off the job path:

  • Nightly: trims the Gradle, bun, mise and hk caches to CACHE_CAP_BYTES each (default 20 GiB), dropping the least recently used files first. It then runs docker system prune --filter until=24h and docker builder prune --filter until=72h. Named volumes are never pruned.
  • Hourly: fills the action archive cache, capped at 2 GiB.

After each job, cleanup.sh does only a quick tidy-up: /tmp, Gradle daemons and locks, and runner logs.

To run the nightly pass once by hand:

docker compose run --rm -e PRUNE_RUN_ONCE=true pruner

Image tags

Images are published to ghcr.io/dodi-smart/github-runner.

Tag Pins
latest Nothing
jdk21 JDK major
node22 Node major
jdk21-node22 JDK and Node majors
2.338.0-jdk21-sdk36-node24 Everything: runner, JDK, SDK and Node. Never overwritten

matrix.json lists the available JDK and Node versions and the defaults. Each image sets RUNNER_IMAGE_TAG and RUNNER_IMAGE_REVISION, so a job can tell which image it ran on.

To use another Android platform, install it in the job: sdkmanager "platforms;android-28".

Notes

  • EPHEMERAL and DISABLE_AUTO_UPDATE count as set for any non-empty value, "false" included. The upstream entrypoint only checks whether they are empty.
  • preflight-entrypoint.sh wipes a stored runner registration only after GitHub confirms that runner is gone (404). A removed runner then registers again instead of crash-looping.

Development

build.yml builds every pull request. Its All builds passed check is the one to require.

Renovate keeps the base image and the actions up to date; forkProcessing is on because this repo is a fork. A weekly workflow opens pull requests for new JDK, Node LTS, Android SDK, build-tools and NDK versions.

To build locally:

docker build --build-arg JAVA_VERSION=17 --build-arg NODE_VERSION=22 .

The build args and their defaults are at the top of the Dockerfile.

About

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

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages