Skip to content
myoung34 edited this page Oct 28, 2024 · 15 revisions

Usage

Notes

Token Scope

Creating GitHub personal access token (PAT) for using by self-hosted runner make sure the following scopes are selected:

  • repo (all)
  • admin:org (all) (mandatory for organization-wide runner)
  • admin:enterprise (all) (mandatory for enterprise-wide runner)
  • admin:public_key - read:public_key
  • admin:repo_hook - read:repo_hook
  • admin:org_hook
  • notifications
  • workflow

To use the runner you will need to enable the organization_self_hosted_runners permission with read and write

Back to top

Systemd

Here's an example service definition for systemd:

# Install with:
#   sudo install -m 644 ephemeral-github-actions-runner.service /etc/systemd/system/
#   sudo systemctl daemon-reload
#   sudo systemctl enable ephemeral-github-actions-runner
# Run with:
#   sudo systemctl start ephemeral-github-actions-runner
# Stop with:
#   sudo systemctl stop ephemeral-github-actions-runner
# See live logs with:
#   journalctl -f -u ephemeral-github-actions-runner.service --no-hostname --no-tail
[Unit]
Description=Ephemeral GitHub Actions Runner Container
After=docker.service
Requires=docker.service
[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop %N
ExecStartPre=-/usr/bin/docker rm %N
ExecStartPre=-/usr/bin/docker pull myoung34/github-runner:latest
ExecStart=/usr/bin/docker run --rm \
                              --env-file /etc/ephemeral-github-actions-runner.env \
                              -e RUNNER_NAME=%H \
                              -v /var/run/docker.sock:/var/run/docker.sock \
                              --name %N \
                              myoung34/github-runner:latest
[Install]
WantedBy=multi-user.target

And an example of the corresponding env file that the service reads from:

# Install with:
#   sudo install -m 600 ephemeral-github-actions-runner.env /etc/
RUNNER_SCOPE=repo
REPO_URL=https://github.com/your-org/your-repo
# Alternate for org scope:
#RUNNER_SCOPE=org
#ORG_NAME=your-org
LABELS=any-custom-labels-go-here
ACCESS_TOKEN=foo-access-token
RUNNER_WORKDIR=/tmp/runner/work
DISABLE_AUTO_UPDATE=1
EPHEMERAL=1

Back to top

Ephemeral Runners

GitHub's hosted runners are completely ephemeral. You can remove all its data without breaking all future jobs.

To achieve the same resilience in a self-hosted runner:

  1. set EPHEMERAL=1 in the container's environment
  2. don't mount a local folder into RUNNER_WORKDIR (to ensure no filesystem persistence)
  3. run the container with --rm (to delete it after termination)
  4. wrap the container execution in a system service that restarts (to start a fresh container after each job)

Back to top

Non-Root Runners

This project runs the container as root by default.

Running as non-root is non-default behavior that is supported via an environment variable RUN_AS_ROOT. Default value is true.

  • If true: preserve old behavior and run as root
  • If true and user is provided with -u (or any orchestrator equiv): error and exit
  • if false: run container as root and assume runner user via gosu
  • if false and user is provided with -u (or any orchestrator equiv): run entire container as runner user

The runner user is runner with uid 1001 and gid 121

If you'd like to run the whole container as non-root:

  • Set the environment variable RUN_AS_ROOT to false
  • Ensure RUNNER_WORKDIR is either not provided (/_work by default) or permissions are correct. the runner user cannot change a directories permissions in entrypoint.sh that it does not have access to
  • add -u runner or -u 1001 to the docker command. In k8s this would be securityContext.runAsUser. Nomad, etc would all do this differently.

Back to top

Modifications (Software, configuration, etc)

Customizing the Image

The runner image can be customized by modifying the config.json file.

User and Group IDs

If a specific user and group ID are required to align with a user on the host, they can be set by modifying the user-id and group-id fields:

{
  "user": {
    "user-id": 1001,
    "group-id": 121
  }
}
Installed Software

Packages can be added or removed from the image by changing the install field. An example is the following:

{
  "install": [
    {
      "category": "sdks",
      "source": "apt",
      "packages": [
        "nodejs",
        "python3",
        "openjdk-21-jdk"
      ]
    },
    {
      "category": "clis",
      "source": "script",
      "packages": [
        "aws-cli",
        "github-cli"
      ]
    }
]
}

The category name is arbitrary and is used only to document related packages together. Any number of packages can be specified in each category.

Sources:

  • For apt-sourced packages, any package can be installed for the sources configured in sources.sh.
  • For script-sourced packages, a function must exist in tools.sh with the name install_<package>.

Back to top

Actions Workflow

name: Package
on:
  release:
    types: [created]
jobs:
  build:
    runs-on: self-hosted
    steps:
    - uses: actions/checkout@v1
    - name: build packages
      run: make all

Back to top

Docker-Compose

version: '2.3'
services:
  worker:
    image: myoung34/github-runner:latest
    environment:
      REPO_URL: https://github.com/example/repo
      RUNNER_NAME: example-name
      RUNNER_TOKEN: someGithubTokenHere
      RUNNER_WORKDIR: /tmp/runner/work
      CONFIGURED_ACTIONS_RUNNER_FILES_DIR: /runner/data # Required for persistence
      RUNNER_GROUP: my-group # github enterprise only
      RUNNER_SCOPE: 'repo'
      LABELS: linux,x64,gpu
    security_opt:
      # needed on SELinux systems to allow docker container to manage other docker containers
      - label:disable
    volumes:
      - '/var/run/docker.sock:/var/run/docker.sock'
      - '/runner/data:/runner/data' # required for persistence
      - '/tmp/runner:/tmp/runner'
      # note: a quirk of docker-in-docker is that this path
      # needs to be the same path on host and inside the container,
      # docker mgmt cmds run outside of docker but expect the paths from within

Back to top

Nomad

job "github_runner" {
  datacenters = ["home"]
  type = "system"
  task "runner" {
    driver = "docker"
    env {
      ACCESS_TOKEN       = "footoken"
      RUNNER_NAME_PREFIX = "myrunner"
      RUNNER_WORKDIR     = "/tmp/github-runner-your-repo"
      RUNNER_GROUP       = "my-group"
      RUNNER_SCOPE       = "org"
      ORG_NAME           = "octokode"
      LABELS             = "my-label,other-label"
    }
    config {
      image = "myoung34/github-runner:latest"
      
      privileged  = true
      userns_mode = "host"
      volumes = [
        "/var/run/docker.sock:/var/run/docker.sock",
        "/tmp/github-runner-your-repo:/tmp/github-runner-your-repo",
      ]
    }
  }
}

Back to top

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: actions-runner
  namespace: runners
spec:
  replicas: 1
  selector:
    matchLabels:
      app: actions-runner
  template:
    metadata:
      labels:
        app: actions-runner
    spec:
      volumes:
      - name: containerdsock
        hostPath:
          path: /run/containerd/containerd.sock
      - name: workdir
        hostPath:
          path: /tmp/github-runner-your-repo
      containers:
      - name: runner
        image: myoung34/github-runner:latest
        env:
        - name: START_DOCKER_SERVICE
          value: "true"
        - name: ACCESS_TOKEN
          valueFrom:
            secretKeyRef:
              name: actions-runner
              key: ACCESS_TOKEN
        - name: RUNNER_SCOPE
          value: "org"
        - name: ORG_NAME
          value: octokode
        - name: LABELS
          value: my-label,other-label
        - name: RUNNER_NAME_PREFIX
          value: foo
        - name: RUNNER_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: RUNNER_WORKDIR
          value: /tmp/github-runner-your-repo
        - name: RUNNER_GROUP
          value: my-group
        volumeMounts:
        - name: containerdsock
          mountPath: /run/containerd/containerd.sock
        - name: workdir
          mountPath: /tmp/github-runner-your-repo
        securityContext:
          privileged: true # Required if you're enabling docker
        resources:
          limits:
            cpu: 2
            memory: 512Mi
          requests:
            cpu: 2
            memory: 256Mi

Bash

Automatically Getting A Token

A runner token can be automatically acquired at runtime if ACCESS_TOKEN (a GitHub personal access token) is a supplied. This uses the GitHub Actions API. e.g.:

docker run -d --restart always --name github-runner \
  -e ACCESS_TOKEN="footoken" \
  -e RUNNER_NAME="foo-runner" \
  -e RUNNER_WORKDIR="/tmp/github-runner-your-repo" \
  -e RUNNER_GROUP="my-group" \
  -e RUNNER_SCOPE="org" \
  -e ORG_NAME="octokode" \
  -e LABELS="my-label,other-label" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /tmp/github-runner-your-repo:/tmp/github-runner-your-repo \
  myoung34/github-runner:latest

Back to top

Enterprise Scope

docker run -d --restart always --name github-runner \
  -e ACCESS_TOKEN="footoken" \
  -e RUNNER_NAME="foo-runner" \
  -e RUNNER_WORKDIR="/tmp/github-runner-your-repo" \
  -e RUNNER_GROUP="my-group" \
  -e RUNNER_SCOPE="enterprise" \
  -e ENTERPRISE_NAME="my-enterprise" \
  -e LABELS="my-label,other-label" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /tmp/github-runner-your-repo:/tmp/github-runner-your-repo \
  myoung34/github-runner:latest

Back to top

Org Runner

docker run -d --restart always --name github-runner \
  -e RUNNER_NAME_PREFIX="myrunner" \
  -e ACCESS_TOKEN="footoken" \
  -e RUNNER_WORKDIR="/tmp/github-runner-your-repo" \
  -e RUNNER_GROUP="my-group" \
  -e RUNNER_SCOPE="org" \
  -e DISABLE_AUTO_UPDATE="true" \
  -e ORG_NAME="octokode" \
  -e LABELS="my-label,other-label" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /tmp/github-runner-your-repo:/tmp/github-runner-your-repo \
  myoung34/github-runner:latest

Back to top

Per-Repo Runner

docker run -d --restart always --name github-runner \
  -e REPO_URL="https://github.com/myoung34/repo" \
  -e RUNNER_NAME="foo-runner" \
  -e RUNNER_TOKEN="footoken" \
  -e RUNNER_WORKDIR="/tmp/github-runner-your-repo" \
  -e RUNNER_GROUP="my-group" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /tmp/github-runner-your-repo:/tmp/github-runner-your-repo \
  myoung34/github-runner:latest

Back to top

Shell Wrapper

function github-runner {
    name=github-runner-${1//\//-}
    org=$(dirname $1)
    repo=$(basename $1)
    tag=${3:-latest}
    docker rm -f $name
    docker run -d --restart=always \
        -e REPO_URL="https://github.com/${org}/${repo}" \
        -e RUNNER_TOKEN="$2" \
        -e RUNNER_NAME="linux-${repo}" \
        -e RUNNER_WORKDIR="/tmp/github-runner-${repo}" \
        -e RUNNER_GROUP="my-group" \
        -e LABELS="my-label,other-label" \
        -v /var/run/docker.sock:/var/run/docker.sock \
        -v /tmp/github-runner-${repo}:/tmp/github-runner-${repo} \
        --name $name myoung34/github-runner:latest
}
github-runner your-account/your-repo       AARGHTHISISYOURGHACTIONSTOKEN
github-runner your-account/some-other-repo ARGHANOTHERGITHUBACTIONSTOKEN ubuntu-focal

Back to top

Reusage

This can be propogated to all other approaches

# per repo
docker run -d --restart always --name github-runner \
  -e REPO_URL="https://github.com/myoung34/repo" \
  -e RUNNER_NAME="foo-runner" \
  -e RUNNER_TOKEN="footoken" \
  -e RUNNER_WORKDIR="/tmp/github-runner-your-repo" \
  -e RUNNER_GROUP="my-group" \
  -e CONFIGURED_ACTIONS_RUNNER_FILES_DIR="/actions-runner-files" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /tmp/github-runner-your-repo:/tmp/github-runner-your-repo \
  -v /tmp/foo:/actions-runner-files \
  myoung34/github-runner:latest

Back to top

Proxy Support

To run the github runners behind a proxy, you need to pass the proxy parameters required for the GitHub Runner as environment variables. Note: The http:// as prefix is required by the GitHub Runner.

docker run -d --restart always --name github-runner \
  -e https_proxy="http://myproxy:3128" \
  -e http_proxy="http://myproxy:3128" \
  -e RUNNER_NAME_PREFIX="myrunner" \
  # ...
  myoung34/github-runner:latest

Back to top