Skip to content
GantryCD

Runner groups

A runner group starts a fresh runner for each run. Set one up with GitHub Actions or run one in your Kubernetes cluster.

GitHub Actions

GantryCD can dispatch a workflow in a GitHub repository. GitHub then provides the runner.

This runner group needs its own GitHub App. A source control integration is a different purpose with different access, and GantryCD can dispatch runners from a GitHub repository even when your stacks come from somewhere else.

  1. Create a GitHub App with these repository permissions:

    PermissionAccessWhy
    MetadataReadAlways. GitHub adds this permission.
    ActionsRead and writeWrite to dispatch the workflow, read to follow the run it created.

    Nothing else is needed: no event subscriptions, no webhook, no callback URL, and no user authorization. Without Actions: read and write, every dispatch fails with 403 Resource not accessible by integration.

  2. Note the App ID, generate a private key, and install the App on the repository that will hold the workflow.

  3. Expand the workflow template below and copy it to .github/workflows/gantrycd-runner.yaml in that repository.

  4. In GantryCD, open Runner groups → Create Runner Group and choose GitHub Actions.

  5. Enter:

    • the repository as owner/name;
    • the workflow file and ref;
    • the App ID and private key;
    • the maximum number of concurrent runs.
Workflow template
name: GantryCD Runner
run-name: gantrycd-${{ inputs.correlation-id }}

on:
  workflow_dispatch:
    inputs:
      assignment-token:
        required: true
      gantrycd-url:
        required: true
      correlation-id:
        required: true
      runner-version:
        required: true

env:
  # renovate: datasource=github-releases depName=tofuutils/tenv
  TENV_VERSION: v4.12.2

jobs:
  run:
    # Change this label to choose another GitHub runner.
    runs-on: ubuntu-slim
    timeout-minutes: 75

    steps:
      - name: Resolve architecture and tool paths
        env:
          HOST_ARCH: ${{ runner.arch }}
        run: |
          set -euo pipefail
          case "${HOST_ARCH}" in
            X64)   arch=amd64; tenv_arch=x86_64 ;;
            ARM64) arch=arm64; tenv_arch=arm64  ;;
            *) echo "unsupported runner.arch: ${HOST_ARCH}" >&2; exit 1 ;;
          esac
          TOOLS_DIR="${HOME}/.cache/gantrycd-tools"
          mkdir -p "${TOOLS_DIR}/tenv"
          printf '%s\n' "${TOOLS_DIR}/tenv" >> "${GITHUB_PATH}"
          {
            echo "ARCH=${arch}"
            # Not TENV_ARCH: tenv reads that name as its own arch override and
            # would then look for a tofu asset called linux_x86_64.
            echo "TENV_PKG_ARCH=${tenv_arch}"
            echo "TOOLS_DIR=${TOOLS_DIR}"
            echo "TENV_ROOT=${HOME}/.tenv"
          } >> "${GITHUB_ENV}"

      - name: Install cosign
        run: |
          set -euo pipefail
          curl -fsSL -o cosign \
            "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-${ARCH}"
          sudo install -m 0755 cosign /usr/local/bin/cosign
          rm cosign

      - name: Download runner binary
        env:
          RUNNER_VERSION: ${{ inputs.runner-version }}
        run: |
          set -euo pipefail
          curl -fsSL -o runner.tar.gz \
            "https://artifacts-axj4rmt8.gantrycd.io/releases/${RUNNER_VERSION}/runner-${RUNNER_VERSION}-linux-${ARCH}.tar.gz"
          tar -xzf runner.tar.gz
          chmod +x runner

      - name: Cache the OpenTofu runtime
        uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
        with:
          path: |
            ~/.cache/gantrycd-tools/tenv
            ~/.tenv
          key: ${{ runner.os }}-${{ runner.arch }}-tofu-runtime-${{ env.TENV_VERSION }}-${{ github.run_id }}
          restore-keys: |
            ${{ runner.os }}-${{ runner.arch }}-tofu-runtime-${{ env.TENV_VERSION }}-

      - name: Install tenv
        run: |
          set -euo pipefail
          if [ -x "${TOOLS_DIR}/tenv/tenv" ]; then
            echo "tenv ${TENV_VERSION} restored from cache"
          else
            curl -fsSL "https://github.com/tofuutils/tenv/releases/download/${TENV_VERSION}/tenv_${TENV_VERSION}_Linux_${TENV_PKG_ARCH}.tar.gz" \
              | tar -xz -C "${TOOLS_DIR}/tenv"
          fi
          command -v tenv

      - name: Run the ephemeral runner
        env:
          BACKEND_URL: ${{ inputs.gantrycd-url }}
          EPHEMERAL_RUNNER_JWT: ${{ inputs.assignment-token }}
        run: ./runner

The template accepts the assignment from GantryCD, downloads the matching runner version, and starts it. Keep its four workflow inputs and its run-name unchanged.

Keep the template current

The workflow pins the tools it installs, so those pins go stale. Let Renovate raise the pull requests:

  • The actions/cache digest and its # v6.1.0 comment are updated by Renovate’s built-in GitHub Actions support, with no extra configuration.

  • TENV_VERSION carries a # renovate: datasource=github-releases depName=tofuutils/tenv annotation. Comments like it are read by the customManagers:githubActionsVersions preset, which config:recommended does not include. Extend it in the repository’s renovate.json and every *_VERSION pin you annotate the same way is covered:

    {
      "extends": ["config:recommended", "customManagers:githubActionsVersions"]
    }

The runner version is not a pin: GantryCD passes it as a workflow input so the runner always matches the backend release.

GitHub Actions receives no runner spec overrides from a stack. The runner label (runs-on), container image, and other compute options are set in the workflow.

Self-hosted with Helm

The runner group Helm chart runs a launcher in your cluster. The launcher creates one short-lived Kubernetes Pod for each run.

First, open Runner groups → Create Runner Group, choose Self-hosted, and save the group ID and private key. The private key is shown only once.

Create a namespace and store the key:

kubectl create namespace acme
kubectl -n acme create secret generic acme-runners-key \
  --from-file=private-key=/path/to/private-key.pem

Install one runner group in that namespace:

helm install acme-runners \
  oci://ghcr.io/gantrycd/helm/gantrycd-runner-group-kubernetes \
  --version vX.Y.Z \
  --namespace acme \
  --set groupID=<group-id> \
  --set backendURL=http://gantrycd.gantrycd.svc:8080 \
  --set privateKey.existingSecret=acme-runners-key

Use a values file for shared settings:

maxConcurrent: 4

ephemeralRunner:
  serviceAccount: gantrycd-runner
  serviceAccountCreate: true
  cpu: "1"
  memory: 1Gi

cpu and memory set both the request and limit for every runner Pod. A stack can override each value separately.

The backend URL must be reachable from the cluster. The service account is the identity used by the short-lived runner Pods, so attach the cloud access they need to it.

View all chart values for your version:

helm show values \
  oci://ghcr.io/gantrycd/helm/gantrycd-runner-group-kubernetes \
  --version vX.Y.Z

Use runner group selectors to route stacks and pass runner spec overrides supported by the selected group.