Self-hosted GitHub Actions runner with DooD and Actions Cache Server support
150
A containerized, multi-architecture GitHub Actions self-hosted runner with Docker workflow support (Docker-outside-of-Docker) and GitHub Actions Cache Server integration.
The easiest way to deploy single or multi-repository runners with an optional shared Actions Cache Server:
curl -fsSL https://raw.githubusercontent.com/bestony/self-hosted-action-runner/main/install.sh | bash
get.docker.com) and Homebrew setup on macOS.ghcr.io/falcondev-oss/github-actions-cache-server:9.8.0 with automatic DooD host IP detection.docker-compose.yml and .env with strict chmod 600 permissions. Tokens are never inlined into Compose files.--dir <path>: Target directory (default: /opt/github-runner for root, $HOME/github-runner for non-root).--non-interactive: Automate deployment without interactive prompts using GHR_* environment variables.--no-start: Generate configuration files and directory structure without starting containers.--skip-docker-install: Skip automatic Docker and Compose plugin installation attempts.--uninstall: Stop containers and prompt to delete persistent volumes.--debug: Enable verbose debug logging.-h, --help: Show help text and options.Run the command without any GHR_* variables or flags. The installer reads your answers from the terminal (/dev/tty), so this works through curl | bash. Press Enter to accept a default value. The token input is hidden.
$ curl -fsSL https://raw.githubusercontent.com/bestony/self-hosted-action-runner/main/install.sh | bash
==> [3/6] Collecting configuration
Enter installation directory [default: /home/me/github-runner]:
--- Configuring Runner #1 ---
GitHub Repository or Organization URL (e.g. https://github.com/org/repo): https://github.com/my-org/repo-a
GitHub Runner Registration Token:
Runner name prefix [default: repo-a-]:
Runner labels (comma-separated) [default: self-hosted,linux,docker]:
Add another repository/org runner? [y/N]: y
--- Configuring Runner #2 ---
GitHub Repository or Organization URL (e.g. https://github.com/org/repo): https://github.com/my-org
GitHub Runner Registration Token:
Runner name prefix [default: my-org-]:
Runner labels (comma-separated) [default: self-hosted,linux,docker]:
Add another repository/org runner? [y/N]:
--- Cache Server Configuration ---
Enable shared GitHub Actions cache server? [Y/n]:
Select cache URL mode: [1] Internal (http://cache-server:3000) or [2] Host IP (reachable by container jobs) [default: 1]:
Runner container image [default: ghcr.io/bestony/self-hosted-action-runner:latest]:
(configuration summary, tokens masked)
Write configuration and continue? [Y/n]:
Get a registration token from Settings > Actions > Runners > New self-hosted runner of the repository or organization. The token expires after 1 hour, but the runner needs it only for the first registration.
If you run the installer again with the same directory, it shows the configured runners and asks you to choose [A]dd runners, [R]econfigure from scratch or [Q]uit.
Use --non-interactive only when no person is at the terminal. In this mode the installer reads the GHR_* variables and does not ask questions. Without --non-interactive, the installer asks for all values and ignores the GHR_* variables (only GHR_DEBUG=1 applies in both modes).
curl -fsSL https://raw.githubusercontent.com/bestony/self-hosted-action-runner/main/install.sh | \
GHR_RUNNER_1_URL="https://github.com/my-org/repo-a" \
GHR_RUNNER_1_TOKEN="YOUR_REPO_A_TOKEN" \
GHR_RUNNER_2_URL="https://github.com/my-org/repo-b" \
GHR_RUNNER_2_TOKEN="YOUR_REPO_B_TOKEN" \
GHR_CACHE=1 \
bash -s -- --non-interactive --dir /opt/github-runner
| Topology | Docker Compose | CapRover | Kubernetes |
|---|---|---|---|
| Single Runner | docker-compose.yml | Single App Guide | Deployment + PVC |
| Multi-Runner (Independent) | docker-compose.multi.yml | Multi-App Setup | StatefulSet Cluster |
| Multi-Runner + Shared Cache | Multi-Repo Cache Compose | One-Click App Template | Kustomize Manifests |
To start a single runner container quickly, use docker run.
Generate a runner registration token from your GitHub repository or organization:
Settings > Actions > Runners > New runner.Organization Settings > Actions > Runners > New runner.Run this command on your host:
docker run -d \
--name github-runner \
--restart unless-stopped \
-e RUNNER_URL="https://github.com/your-org/your-repo" \
-e RUNNER_TOKEN="YOUR_REGISTRATION_TOKEN" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v runner_data:/runner \
bestony/self-hosted-runner:latest
The runner configures itself, registers with GitHub, and begins listening for jobs.
Runner credentials persist in the runner_data volume. Container restarts do not require re-registration.
This image supports an external GitHub Actions Cache Server (such as falcondev-oss/github-actions-cache-server).
The runner image includes a binary patch for ACTIONS_RESULTS_URL and bundles zstd for fast compression.
Set the ACTIONS_RESULTS_URL environment variable:
docker run -d \
--name github-runner \
--restart unless-stopped \
-e RUNNER_URL="https://github.com/your-org/your-repo" \
-e RUNNER_TOKEN="YOUR_REGISTRATION_TOKEN" \
-e ACTIONS_RESULTS_URL="http://cache-server:3000/" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v runner_data:/runner \
bestony/self-hosted-runner:latest
When ACTIONS_RESULTS_URL is configured:
actions/cache workflow steps save and restore cache directly with your cache server.--disableupdate) to prevent overwriting the internal cache patch.ACTIONS_RESULTS_URL uses an address reachable from the host (such as http://host.docker.internal:3000/ or your server IP).You can manage the runner and the cache server together with Docker Compose.
docker-compose.ymlservices:
runner:
image: bestony/self-hosted-runner:latest
restart: unless-stopped
environment:
- RUNNER_URL=${RUNNER_URL}
- RUNNER_TOKEN=${RUNNER_TOKEN}
- RUNNER_NAME=${RUNNER_NAME:-}
- RUNNER_LABELS=${RUNNER_LABELS:-self-hosted,docker,linux}
- RUNNER_WORKDIR=${RUNNER_WORKDIR:-${PWD}/work/runner}
- ACTIONS_RESULTS_URL=${ACTIONS_RESULTS_URL:-}
- LOG_LEVEL=${LOG_LEVEL:-info}
volumes:
- runner_data:/runner
- /var/run/docker.sock:/var/run/docker.sock
- ${RUNNER_WORKDIR:-${PWD}/work/runner}:${RUNNER_WORKDIR:-${PWD}/work/runner}
depends_on:
cache-server:
condition: service_started
required: false
cache-server:
image: ghcr.io/falcondev-oss/github-actions-cache-server:9.8.0
restart: unless-stopped
# ports:
# - "${CACHE_PORT:-3000}:3000"
environment:
API_BASE_URL: ${CACHE_API_BASE_URL:-http://cache-server:3000}
STORAGE_DRIVER: filesystem
STORAGE_FILESYSTEM_PATH: /data/cache
DB_DRIVER: sqlite
DB_SQLITE_PATH: /data/cache-server.db
volumes:
- cache_data:/data
profiles:
- cache
- full
volumes:
runner_data:
cache_data:
.env file:
cp .env.example .env
RUNNER_URL and RUNNER_TOKEN in .env.# Start runner only
docker compose up -d
# Start runner with cache server
docker compose --profile cache up -d
docker compose logs -f runner
You can run multiple independent runner stacks on the same host machine (for different teams, environments, or projects) without collisions:
COMPOSE_PROJECT_NAME in .env or top-level name: in docker-compose.yml). The installer automatically generates deterministic, collision-free project names (ghr-<dir>-<sha256:8>).container_name attributes are omitted. Containers are dynamically named <project>-<service>-<index> by Compose.runner_data, cache_data) are scoped per project, ensuring runner credentials and SQLite databases never conflict.${PWD}/work/<runner> or <install_dir>/work/runner-<n>), ensuring Docker-outside-of-Docker (DooD) host mounts do not collide.The runner process runs as the unprivileged runner user with HOME=/home/runner, so git config --global and actions/checkout work.
The image includes these tools for jobs:
/var/run/docker.sock.gh). Give it a token in the job, for example env: GH_TOKEN: ${{ github.token }}.git, curl, jq, zstd.Jobs that start containers through the host Docker socket often run them as root. Those containers can write root-owned files into the workspace, and the next actions/checkout then fails with EACCES: permission denied. To prevent this, the image enables a runner job hook (/opt/runner-hooks/fix-workspace-ownership.sh). Before and after each job, the hook gives the job workspace and RUNNER_TEMP back to the runner user.
| Variable | Default | Description |
|---|---|---|
FIX_WORKSPACE_OWNERSHIP | true | Set to false to disable the ownership hook. |
ACTIONS_RUNNER_HOOK_JOB_STARTED | ownership hook | Set your own script to replace the job-started hook. |
ACTIONS_RUNNER_HOOK_JOB_COMPLETED | ownership hook | Set your own script to replace the job-completed hook. |
To run matrix jobs in parallel, register more than one runner: run the installer again in the same directory and choose [A]dd runners. One runner executes one job at a time.
For comprehensive documentation, refer to:
Content type
Image
Digest
sha256:3a77acd42…
Size
369.6 MB
Last updated
2 minutes ago
docker pull bestony/self-hosted-runner