Self-Hosted CI/CD Runner on a Raspberry Pi: GitHub Actions and Gitea Actions
If you write any amount of code for your projects — firmware for an ESP32 project, a Klipper macro repo, a personal website, scripts that manage your homelab — a CI/CD runner gives you automatic testing, linting, and builds on every commit without paying for cloud minutes or trusting a third party with build secrets. A Raspberry Pi is a genuinely good fit for this: it's low power enough to leave running 24/7, quiet enough to sit on a shelf, and more than capable of running lint checks, unit tests, and small builds for most personal-scale projects. This guide covers wiring up a self-hosted runner for both GitHub Actions and Gitea Actions (if you're also self-hosting your git server on a Pi, per our Raspberry Pi home server guide).
Why Self-Host a Runner
- No usage limits: GitHub's free tier caps Actions minutes for private repos; a self-hosted runner has no such cap since it's your own hardware.
- Access to local/private resources: a runner on your network can reach your homelab services, private package registries, or hardware-in-the-loop test rigs that a cloud runner never could.
- No secrets leave your network for builds that touch sensitive credentials or internal-only resources.
- Works with fully self-hosted git (Gitea, Forgejo) where there's no cloud CI offering at all without a runner of your own.
The tradeoff: you're responsible for keeping the runner patched and isolated, since a CI runner executes arbitrary code from your repos (and, if misconfigured, potentially from pull requests submitted by others) — see the security section below before exposing this to anything beyond your own private repos.
Hardware Recommendation
A Raspberry Pi 4 or 5 with at least 4GB RAM and a fast USB SSD (not a microSD card — CI workloads do enough small file I/O that SD cards become both a bottleneck and a wear-out risk) handles most personal-project CI comfortably: linting, unit tests, small Docker builds, and static site builds. Heavier workloads (compiling large C++ projects, building multi-arch Docker images) will be noticeably slower than a cloud runner's dedicated hardware, but for the kind of firmware and script-level projects typical of a maker's repos, a Pi keeps up fine.
Setting Up a GitHub Actions Self-Hosted Runner
GitHub's official runner binary runs natively on Raspberry Pi OS (64-bit is required — confirm you're running the 64-bit image before starting):
# On the Pi, create a runner directory mkdir actions-runner && cd actions-runner # Download the ARM64 runner package (get the current version/URL from # your repo's Settings -> Actions -> Runners -> New self-hosted runner page) curl -o actions-runner.tar.gz -L https://github.com/actions/runner/releases/download/vX.X.X/actions-runner-linux-arm64-X.X.X.tar.gz tar xzf actions-runner.tar.gz # Configure, using the token shown on the same GitHub settings page ./config.sh --url https://github.com/YOUR_USER/YOUR_REPO --token YOUR_TOKEN # Install and start as a systemd service so it survives reboots sudo ./svc.sh install sudo ./svc.sh startOnce registered, the runner shows up under the repo's (or organization's) Settings > Actions > Runners page, and any workflow file specifying runs-on: self-hosted will queue on your Pi instead of GitHub's cloud runners. Add custom labels during config.sh (e.g. --labels rpi,arm64) so workflows can target this specific runner deliberately with runs-on: [self-hosted, rpi] rather than any self-hosted runner you might add later.
Setting Up a Gitea Actions Runner
If you're running Gitea (or Forgejo) on your network, Gitea Actions uses a compatible workflow syntax to GitHub Actions and its own runner binary, act_runner:
# Download act_runner (ARM64 build) from Gitea's releases page wget https://gitea.com/gitea/act_runner/releases/download/vX.X.X/act_runner-X.X.X-linux-arm64 chmod +x act_runner-X.X.X-linux-arm64 sudo mv act_runner-X.X.X-linux-arm64 /usr/local/bin/act_runner # Register against your Gitea instance (token from Gitea's admin/repo Actions settings) act_runner register --instance https://your-gitea-url --token YOUR_TOKEN # Run it (wrap in a systemd service for persistence) act_runner daemonact_runner supports both a Docker-backed execution mode (isolates each job in a container, recommended) and a native/host execution mode. On a Pi, Docker mode adds some overhead but is worth the isolation, especially if more than one person or repo will trigger jobs on the runner.
Running Jobs in Docker for Isolation
Whichever platform you use, running CI jobs inside Docker containers rather than directly on the Pi's host OS is the safer default — it keeps a broken or malicious build script from touching the rest of the Pi's filesystem or other services you're running on it. Install Docker on the Pi (the official convenience script works fine on Raspberry Pi OS 64-bit) and, for GitHub Actions specifically, note that the vanilla self-hosted runner doesn't auto-containerize jobs the way GitHub-hosted runners do — jobs run directly on the host unless your workflow explicitly uses a container: directive, so add one for anything you don't fully trust.
Multi-Architecture Considerations
A Pi runner is ARM64 (aarch64), and if your project also needs to build or test for x86_64, you'll need either a second runner on x86 hardware, GitHub's hosted runners for that architecture in the same workflow (mixing self-hosted and hosted runners across jobs is fully supported), or Docker Buildx with QEMU emulation for cross-building images — emulated cross-builds work but are noticeably slower than native. For firmware projects (ESP32, Arduino, Flipper Zero apps) this usually isn't an issue since those toolchains cross-compile from ARM64 just as well as x86_64.
Security Notes
- Never attach a self-hosted runner to a public repository that accepts pull requests from strangers unless you've locked down workflow permissions carefully — a malicious PR can submit a workflow that runs arbitrary code on your runner (and, by extension, your home network) the moment it's triggered. This is GitHub's own documented warning, not a hypothetical.
- Keep the runner on private repos only, or use required-approval settings for workflow runs from outside collaborators if the repo must stay public.
- Run jobs inside Docker containers rather than directly on the host, and don't reuse the same Pi for CI and for anything storing sensitive credentials or acting as a critical piece of home infrastructure (a Pi-hole DNS server, a security camera NVR) — isolate the CI runner to its own device or at minimum its own container/VM boundary.
- Keep the runner binary and the Pi's OS patched; both GitHub and Gitea periodically ship security fixes for their runner software.
- Consider firewalling the Pi so it can only reach the specific git host it needs to talk to, rather than sitting fully open on your LAN.
What This Is Good For
Linting and unit tests on every commit, building and validating Klipper/firmware configs before flashing, building a static site (like a personal maker log) and deploying it on push, running scheduled jobs (nightly backups, link checkers, dependency update checks) — all of it running for free, on hardware you already understand, without a monthly CI minutes bill.
Related Guides
- Managing a Fleet of Raspberry Pis with Ansible: Automated Provisioning, Updates, and Config Management
- Self-Hosting a Git Server on Raspberry Pi with Gitea
- Build a Raspberry Pi Kubernetes Cluster with K3s: A Hands-On Way to Learn Distributed Systems
- Automated Plant Watering System with Raspberry Pi
- Running Home Assistant on a Raspberry Pi 4
- Auto-Backup Your Raspberry Pi SD Card to a Network Share
- Kiosk Mode on Raspberry Pi: Boot Straight to a Webpage
- MQTT and Node-RED on Raspberry Pi: Visual Automation for ESP32 Sensor Networks