Skip to content

DevOps: Self-Hosted GitHub Actions Runner with Docker on Pi 5

Automating software compilation, testing, and deployment (CI/CD) is essential for modern development. While GitHub provides free cloud hosting minutes for shared runners, these runners are limited to x86_64 architecture and can be slow for compiling ARM64 binaries (which requires QEMU emulation).

By setting up a self-hosted GitHub Actions runner on a Raspberry Pi 5, you get: 1. Native ARM64 Compilation: Drastically accelerates compilation speeds for Docker images and binary releases targetting Raspberry Pi or edge devices. 2. Access to Local Hardware: Workflows can deploy directly to local homelab storage, Docker networks, or interact with physical GPIO pins. 3. Unlimited Build Minutes: Run high-frequency, complex pipelines entirely free of charge.

This guide walks you through running an isolated, containerized GitHub Actions runner on Raspberry Pi 5.


Architecture Overview

graph TD A["Developer (Git Push)"] -->|Triggers Workflow| B["GitHub Actions (Cloud)"] B -->|Poll / Assign Jobs| C["Raspberry Pi 5 (Self-Hosted Runner Container)"] C -->|Docker-in-Docker| D["Build & Run Jobs (Native ARM64)"] D -->|Optional| E["GPIO / Local Network Deployments"] style C fill:#7209b7,stroke:#560bad,color:#fff style D fill:#f72585,stroke:#b5179e,color:#fff

Step 1: Generate the Runner Token in GitHub

First, generate the necessary authorization token from your target GitHub repository:

  1. Navigate to your GitHub repository (or organization).
  2. Go to Settings -> Actions -> Runners in the left sidebar.
  3. Click New self-hosted runner.
  4. Select Linux as the runner image, and ARM64 as the architecture.
  5. In the shown configuration instructions, copy the registration token and repository URL. (The token looks like: AN735B0x...).

Step 2: Build the Containerized Runner (Dockerfile)

For security and reproducibility, we will run the runner inside an isolated Docker container rather than directly on the host OS.

Create a workspace directory:

mkdir -p ~/github-runner && cd ~/github-runner

Create a Dockerfile defining the runner environment, including basic tools (git, curl, docker-cli):

nano Dockerfile

Add the following Dockerfile configuration:

FROM ubuntu:22.04

# Avoid timezone prompt during package install
ENV DEBIAN_FRONTEND=noninteractive

# Install core dependencies
RUN apt-get update && apt-get install -y \
    curl \
    sudo \
    git \
    jq \
    build-essential \
    ca-certificates \
    docker.io \
    && rm -rf /var/lib/apt/lists/*

# Create runner user
RUN useradd -m runner && usermod -aG sudo runner && echo "runner ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
USER runner
WORKDIR /home/runner

# Download the GitHub Actions runner package (ARM64 version)
# Be sure to verify the latest runner version in GitHub settings
RUN curl -o actions-runner-linux-arm64-2.311.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-arm64-2.311.0.tar.gz \
    && tar xzf ./actions-runner-linux-arm64-2.311.0.tar.gz \
    && rm actions-runner-linux-arm64-2.311.0.tar.gz

# Inject entrypoint script to automatically register on startup
COPY --chown=runner:runner entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh

ENTRYPOINT ["./entrypoint.sh"]

Step 3: Create the Entrypoint Registration Script

The entrypoint script registers the runner with GitHub using the token on container boot, and gracefully de-registers it when the container stops.

Create the file:

nano entrypoint.sh

Add the script:

#!/bin/bash

# Configuration checks
if [ -z "$REPOSITORY_URL" ] || [ -z "$REGISTRATION_TOKEN" ]; then
  echo "Error: REPOSITORY_URL or REGISTRATION_TOKEN is not set."
  exit 1
fi

RUNNER_NAME="pi5-runner-$(hostname)"

# Register the runner
./config.sh --url ${REPOSITORY_URL} --token ${REGISTRATION_TOKEN} --name ${RUNNER_NAME} --unattended --replace

# De-register the runner on exit
cleanup() {
    echo "De-registering runner..."
    ./config.sh remove --token ${REGISTRATION_TOKEN}
}
trap 'cleanup' SIGINT SIGQUIT SIGTERM

# Start the runner listening loop
./run.sh &
wait $!

Step 4: Write the Docker Compose Configuration

This compose configuration links the runner to the host's Docker daemon (/var/run/docker.sock), allowing the runner to spin up Docker-in-Docker workflows (e.g. docker build) directly.

Create the file:

nano docker-compose.yml

Add the configuration (replace <YOUR_REPO_URL> and <YOUR_REGISTRATION_TOKEN>):

version: '3.8'

services:
  github-runner:
    build: .
    container_name: github-runner
    environment:
      - REPOSITORY_URL=https://github.com/<YOUR_USER_OR_ORG>/<YOUR_REPO>
      - REGISTRATION_TOKEN=<YOUR_REGISTRATION_TOKEN>
    volumes:
      # Map host docker daemon for Docker-in-Docker actions
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped

Build the image and launch the container:

docker compose up -d --build

Verify that the runner is successfully connected to GitHub:

docker compose logs -f
(In your GitHub settings browser panel, the status of the runner should change from Offline to Idle / Online).

Step 5: Test the Runner with a Workflow

Let's test if our Raspberry Pi 5 runs native ARM64 jobs correctly.

In your GitHub repository, create a workflow file: .github/workflows/test-pi.yml:

name: Test Raspberry Pi Runner

on: [push]

jobs:
  build-on-pi:
    runs-on: self-hosted # Tells GitHub to schedule the job on your Pi runner

    steps:
      - name: Checkout Code
        uses: actions/checkout@v3

      - name: Verify Architecture
        run: |
          echo "Current Host OS Architecture:"
          uname -a

      - name: Test Docker Access
        run: |
          docker run --rm alpine uname -a

Push the commit to GitHub. In the Actions tab of your repository, select the triggered workflow. You will see that the job instantly schedules onto your local Raspberry Pi 5 container, executing the steps natively and showing aarch64 in the logs!


Security Best Practices

[!WARNING] Never run self-hosted runners on Public repositories. Anyone can fork a public repository, submit a Pull Request with a modified workflow file (e.g., executing malicious bash commands), and trigger it to run on your local Raspberry Pi, compromising your home network. Use self-hosted runners only for Private repositories.

By integrating this containerized GitHub Actions runner, your Raspberry Pi 5 becomes an efficient, ARM64-native automation server, supercharging your local software deployment pipelines.