Last active
December 22, 2025 12:00
-
-
Save grenade/128986996dc588c34ee6c3cbdd1b155a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ARG UBUNTU_VERSION=24.04 | |
| FROM ubuntu:${UBUNTU_VERSION} | |
| ARG RUNNER_VERSION=2.330.0 | |
| ENV DEBIAN_FRONTEND=noninteractive | |
| ENV UBUNTU_VERSION=${UBUNTU_VERSION} | |
| ENV RUNNER_VERSION=${RUNNER_VERSION} | |
| # 1. Install base dependencies | |
| RUN apt-get update && apt-get install -yqq --no-install-recommends \ | |
| curl \ | |
| jq \ | |
| git \ | |
| unzip \ | |
| tar \ | |
| build-essential \ | |
| ca-certificates \ | |
| gnupg \ | |
| lsb-release \ | |
| sudo \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # 2. Install Docker CLI | |
| RUN mkdir -p /etc/apt/keyrings && \ | |
| curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ | |
| $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \ | |
| apt-get update && apt-get install -yqq docker-ce-cli && \ | |
| rm -rf /var/lib/apt/lists/* | |
| # 3. Install GitHub CLI (gh) | |
| RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/etc/apt/keyrings/githubcli-archive-keyring.gpg && \ | |
| chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && \ | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \ | |
| apt-get update && apt-get install -yqq gh && \ | |
| rm -rf /var/lib/apt/lists/* | |
| # 4. Create runner user | |
| RUN groupadd -g 1001 podman && \ | |
| useradd -m runner -s /bin/bash && \ | |
| usermod -aG podman runner && \ | |
| echo "runner ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers | |
| USER runner | |
| WORKDIR /home/runner | |
| # 5. Download Runner | |
| RUN curl -o runner.tar.gz -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz && \ | |
| tar xzf ./runner.tar.gz && \ | |
| rm runner.tar.gz | |
| # 6. Install runner dependencies | |
| USER root | |
| RUN ./bin/installdependencies.sh && \ | |
| rm -rf /var/lib/apt/lists/* | |
| # 7. Setup Entrypoint | |
| COPY --chown=runner:runner entrypoint.sh /entrypoint.sh | |
| RUN chmod +x /entrypoint.sh | |
| USER runner | |
| ENTRYPOINT ["/entrypoint.sh"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # usage: curl -sL https://gist.github.com/grenade/128986996dc588c34ee6c3cbdd1b155a/raw/create-github-runner-quadlet.sh | bash -s ${GITHUB_ACCESS_TOKEN} ${GITHUB_ORG} ${GITHUB_REPO} | |
| # | |
| # Arguments: | |
| # $1 - GITHUB_ACCESS_TOKEN (PAT used by the runner) | |
| # $2 - GITHUB_ORG (GitHub organization, e.g. "helexa-ai") | |
| # $3 - GITHUB_REPO (optional, GitHub repo name for repo-level runner) | |
| # | |
| # Optional env: | |
| # GITHUB_API_TOKEN (optional PAT for calling GitHub Gist API; falls back to anonymous if unset) | |
| # RUNNER_API_TOKEN (optional PAT for calling GitHub Actions Runner releases API; falls back to anonymous if unset) | |
| # UBUNTU_VERSION (optional Ubuntu base image version, defaults to 24.04) | |
| # | |
| # This script: | |
| # - Resolves the latest revision of this gist and downloads the quadlet artifacts | |
| # - Queries the GitHub API for the latest Actions Runner release tag | |
| # - Strips the "v" prefix and passes the version as a build-arg RUNNER_VERSION to podman build | |
| # - Passes a configurable Ubuntu version as build-arg UBUNTU_VERSION (default 24.04) | |
| # - Sets up podman.socket and the gh-runner systemd unit | |
| set -euo pipefail | |
| ############################################################################### | |
| # Basic argument validation | |
| ############################################################################### | |
| GITHUB_ACCESS_TOKEN="${1:-}" | |
| GITHUB_ORG="${2:-}" | |
| GITHUB_REPO="${3:-}" | |
| if [ -z "${GITHUB_ACCESS_TOKEN}" ]; then | |
| echo "Error: GITHUB_ACCESS_TOKEN (arg 1) is required." >&2 | |
| echo "Usage: bash -s <GITHUB_ACCESS_TOKEN> <GITHUB_ORG> [GITHUB_REPO]" >&2 | |
| exit 1 | |
| fi | |
| if [ -z "${GITHUB_ORG}" ]; then | |
| echo "Error: GITHUB_ORG (arg 2) is required." >&2 | |
| echo "Usage: bash -s <GITHUB_ACCESS_TOKEN> <GITHUB_ORG> [GITHUB_REPO]" >&2 | |
| exit 1 | |
| fi | |
| ############################################################################### | |
| # Resolve latest gist revision SHA | |
| ############################################################################### | |
| gist_id="128986996dc588c34ee6c3cbdd1b155a" | |
| gist_api_url="https://api.github.com/gists/${gist_id}" | |
| if [ -n "${GITHUB_API_TOKEN:-}" ] && [ "${GITHUB_API_TOKEN}" != "null" ]; then | |
| latest_git_sha="$( | |
| curl \ | |
| --fail \ | |
| --location \ | |
| --silent \ | |
| --header "Authorization: Bearer ${GITHUB_API_TOKEN}" \ | |
| --header 'X-GitHub-Api-Version: 2022-11-28' \ | |
| --url "${gist_api_url}" \ | |
| | jq --raw-output '.history[0].version' | |
| )" || { | |
| echo "Error: Failed to fetch gist metadata with authenticated request from ${gist_api_url}" >&2 | |
| exit 1 | |
| } | |
| else | |
| latest_git_sha="$( | |
| curl \ | |
| --fail \ | |
| --location \ | |
| --silent \ | |
| --header 'X-GitHub-Api-Version: 2022-11-28' \ | |
| --url "${gist_api_url}" \ | |
| | jq --raw-output '.history[0].version' | |
| )" || { | |
| echo "Error: Failed to fetch gist metadata (anonymous) from ${gist_api_url}" >&2 | |
| exit 1 | |
| } | |
| fi | |
| if [ -z "${latest_git_sha}" ] || [ "${latest_git_sha}" = "null" ]; then | |
| echo "Error: Failed to determine latest gist revision SHA from ${gist_api_url}" >&2 | |
| exit 1 | |
| fi | |
| echo "Using gist ${gist_id} revision ${latest_git_sha}" | |
| ############################################################################### | |
| # Download artifacts from the resolved gist revision | |
| ############################################################################### | |
| base_gist_raw_url="https://gist.github.com/grenade/${gist_id}/raw/${latest_git_sha}" | |
| sudo mkdir -p /opt/github/runner || { | |
| echo "Error: Failed to create /opt/github/runner" >&2 | |
| exit 1 | |
| } | |
| for file in Containerfile entrypoint.sh template.env; do | |
| target="/opt/github/runner/${file}" | |
| url="${base_gist_raw_url}/${file}" | |
| if ! sudo curl \ | |
| --fail \ | |
| --location \ | |
| --silent \ | |
| --output "${target}" \ | |
| --url "${url}" | |
| then | |
| echo "Error: Failed to download ${file} from ${url}" >&2 | |
| exit 1 | |
| fi | |
| done | |
| quadlet_target="/etc/containers/systemd/gh-runner.container" | |
| quadlet_url="${base_gist_raw_url}/gh-runner.container" | |
| if ! sudo curl \ | |
| --fail \ | |
| --location \ | |
| --silent \ | |
| --output "${quadlet_target}" \ | |
| --url "${quadlet_url}" | |
| then | |
| echo "Error: Failed to download gh-runner.container from ${quadlet_url}" >&2 | |
| exit 1 | |
| fi | |
| sudo groupadd -f podman || { | |
| echo "Error: Failed to ensure podman group exists" >&2 | |
| exit 1 | |
| } | |
| sudo mkdir -p /etc/systemd/system/podman.socket.d || { | |
| echo "Error: Failed to create /etc/systemd/system/podman.socket.d" >&2 | |
| exit 1 | |
| } | |
| override_target="/etc/systemd/system/podman.socket.d/override.conf" | |
| override_url="${base_gist_raw_url}/override.conf" | |
| if ! sudo curl \ | |
| --fail \ | |
| --location \ | |
| --silent \ | |
| --output "${override_target}" \ | |
| --url "${override_url}" | |
| then | |
| echo "Error: Failed to download override.conf from ${override_url}" >&2 | |
| exit 1 | |
| fi | |
| ############################################################################### | |
| # Ensure podman.socket is enabled and active | |
| ############################################################################### | |
| if ! systemctl is-enabled podman.socket >/dev/null 2>&1; then | |
| if ! sudo systemctl enable podman.socket; then | |
| echo "Error: Failed to enable podman.socket" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if ! systemctl is-active podman.socket >/dev/null 2>&1; then | |
| if ! sudo systemctl start podman.socket; then | |
| echo "Error: Failed to start podman.socket" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| ############################################################################### | |
| # Generate runner env file | |
| ############################################################################### | |
| RUNNER_NAME="$(hostname -s)" | |
| if [ -n "${GITHUB_REPO:-}" ] && [ "${GITHUB_REPO}" != "null" ]; then | |
| GITHUB_URL="https://github.com/${GITHUB_ORG}/${GITHUB_REPO}" | |
| else | |
| GITHUB_URL="https://github.com/${GITHUB_ORG}" | |
| fi | |
| GITHUB_ACCESS_TOKEN="${GITHUB_ACCESS_TOKEN}" \ | |
| GITHUB_URL="${GITHUB_URL}" \ | |
| RUNNER_NAME="${RUNNER_NAME}" \ | |
| envsubst < /opt/github/runner/template.env | sudo tee /opt/github/runner/.env >/dev/null | |
| ############################################################################### | |
| # Fetch latest GitHub Actions Runner version | |
| ############################################################################### | |
| # We call the public GitHub API: | |
| # GET https://api.github.com/repos/actions/runner/releases/latest | |
| # and extract .tag_name (e.g. "v2.330.0"), then strip the "v" prefix. | |
| runner_releases_api="https://api.github.com/repos/actions/runner/releases/latest" | |
| runner_tag="" | |
| if [ -n "${RUNNER_API_TOKEN:-}" ] && [ "${RUNNER_API_TOKEN}" != "null" ]; then | |
| runner_tag="$( | |
| curl \ | |
| --fail \ | |
| --location \ | |
| --silent \ | |
| --header "Authorization: Bearer ${RUNNER_API_TOKEN}" \ | |
| --header 'X-GitHub-Api-Version: 2022-11-28' \ | |
| --url "${runner_releases_api}" \ | |
| | jq --raw-output '.tag_name' | |
| )" || { | |
| echo "Error: Failed to fetch latest runner release (authenticated) from ${runner_releases_api}" >&2 | |
| exit 1 | |
| } | |
| else | |
| runner_tag="$( | |
| curl \ | |
| --fail \ | |
| --location \ | |
| --silent \ | |
| --header 'X-GitHub-Api-Version: 2022-11-28' \ | |
| --url "${runner_releases_api}" \ | |
| | jq --raw-output '.tag_name' | |
| )" || { | |
| echo "Error: Failed to fetch latest runner release (anonymous) from ${runner_releases_api}" >&2 | |
| exit 1 | |
| } | |
| fi | |
| if [ -z "${runner_tag}" ] || [ "${runner_tag}" = "null" ]; then | |
| echo "Error: Failed to determine latest runner tag from ${runner_releases_api}" >&2 | |
| exit 1 | |
| fi | |
| # Strip leading "v" if present, e.g. "v2.330.0" -> "2.330.0" | |
| RUNNER_VERSION="${runner_tag#v}" | |
| if [ -z "${RUNNER_VERSION}" ]; then | |
| echo "Error: Derived empty RUNNER_VERSION from tag '${runner_tag}'" >&2 | |
| exit 1 | |
| fi | |
| echo "Using GitHub Actions Runner version ${RUNNER_VERSION} (tag ${runner_tag})" | |
| ############################################################################### | |
| # Determine Ubuntu version to use for base image | |
| ############################################################################### | |
| # Default to 24.04 if UBUNTU_VERSION is not set. | |
| UBUNTU_VERSION_DEFAULT="24.04" | |
| UBUNTU_VERSION="${UBUNTU_VERSION:-${UBUNTU_VERSION_DEFAULT}}" | |
| if [ -z "${UBUNTU_VERSION}" ]; then | |
| echo "Error: UBUNTU_VERSION resolved to empty value" >&2 | |
| exit 1 | |
| fi | |
| echo "Using Ubuntu base image version ${UBUNTU_VERSION}" | |
| ############################################################################### | |
| # Build the runner image (pass RUNNER_VERSION and UBUNTU_VERSION as build-args) | |
| ############################################################################### | |
| if ! sudo podman build \ | |
| --build-arg "RUNNER_VERSION=${RUNNER_VERSION}" \ | |
| --build-arg "UBUNTU_VERSION=${UBUNTU_VERSION}" \ | |
| -t localhost/gh-runner:latest \ | |
| /opt/github/runner | |
| then | |
| echo "Error: podman build of localhost/gh-runner:latest failed" >&2 | |
| exit 1 | |
| fi | |
| ############################################################################### | |
| # Reload systemd and start/restart gh-runner service | |
| ############################################################################### | |
| if ! sudo systemctl daemon-reload; then | |
| echo "Error: systemctl daemon-reload failed" >&2 | |
| exit 1 | |
| fi | |
| if ! sudo systemctl restart gh-runner 2>/dev/null; then | |
| # If restart fails because the service doesn't exist yet, try start | |
| if ! sudo systemctl start gh-runner; then | |
| echo "Error: Failed to start gh-runner systemd service" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| # At this point, things are working as expected; no further noisy output. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -e | |
| if [ -z "${GITHUB_URL}" ] || [ -z "${GITHUB_ACCESS_TOKEN}" ]; then | |
| echo "Error: GITHUB_URL and GITHUB_ACCESS_TOKEN must be set." | |
| exit 1 | |
| fi | |
| # Derive the path part after github.com/, stripping any trailing .git | |
| REPO_PATH=$(echo "${GITHUB_URL}" | awk -F'github.com/' '{print $2}' | sed 's/\.git$//') | |
| # Decide whether this is a repo-level or org-level runner | |
| # - If the path contains a slash ("org/repo"), treat it as a repo runner | |
| # - Otherwise, treat it as an org-level runner | |
| if echo "${REPO_PATH}" | grep -q "/"; then | |
| API_URL_BASE="https://api.github.com/repos/${REPO_PATH}" | |
| TARGET_TYPE="repository" | |
| else | |
| API_URL_BASE="https://api.github.com/orgs/${REPO_PATH}" | |
| TARGET_TYPE="organization" | |
| fi | |
| echo "Configuring runner for ${TARGET_TYPE} '${REPO_PATH}'..." | |
| echo "Generating registration token for ${REPO_PATH}..." | |
| REG_TOKEN=$(curl -sX POST -H "Accept: application/vnd.github.v3+json" \ | |
| -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" \ | |
| "${API_URL_BASE}/actions/runners/registration-token" | jq -r .token) | |
| if [ "$REG_TOKEN" == "null" ] || [ -z "$REG_TOKEN" ]; then | |
| echo "Error: Failed to generate registration token. Check your PAT scopes and GITHUB_URL." | |
| exit 1 | |
| fi | |
| cleanup() { | |
| echo "Removing runner from ${TARGET_TYPE} '${REPO_PATH}'..." | |
| REM_TOKEN=$(curl -sX POST -H "Accept: application/vnd.github.v3+json" \ | |
| -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" \ | |
| "${API_URL_BASE}/actions/runners/remove-token" | jq -r .token) | |
| if [ "$REM_TOKEN" == "null" ] || [ -z "$REM_TOKEN" ]; then | |
| echo "Warning: Failed to generate removal token. Runner may not be deregistered cleanly." | |
| else | |
| ./config.sh remove --token "${REM_TOKEN}" | |
| fi | |
| } | |
| trap 'cleanup; exit 130' SIGINT | |
| trap 'cleanup; exit 143' SIGTERM | |
| echo "Configuring runner..." | |
| ./config.sh --unattended \ | |
| --url "${GITHUB_URL}" \ | |
| --token "${REG_TOKEN}" \ | |
| --name "${RUNNER_NAME:-$(hostname)}" \ | |
| --work _work \ | |
| --labels "podman" \ | |
| --replace | |
| echo "Starting runner..." | |
| ./run.sh & wait $! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [Unit] | |
| Description=GitHub Actions Self-Hosted Runner | |
| After=network-online.target podman.socket | |
| Wants=network-online.target | |
| [Container] | |
| Image=localhost/gh-runner:latest | |
| EnvironmentFile=/opt/github/runner/.env | |
| Volume=/run/podman/podman.sock:/var/run/docker.sock | |
| SecurityLabelDisable=true | |
| [Service] | |
| User=root | |
| Group=podman | |
| Restart=always | |
| TimeoutStartSec=300 | |
| [Install] | |
| WantedBy=multi-user.target |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [Socket] | |
| SocketMode=0660 | |
| SocketGroup=podman |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| GITHUB_URL=${GITHUB_URL} | |
| GITHUB_ACCESS_TOKEN=${GITHUB_ACCESS_TOKEN} | |
| RUNNER_NAME=${RUNNER_NAME} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment