GitHub Actions Runner auf VPS selbst hosten
Installieren Sie einen selbst gehosteten Runner auf Ubuntu 24.04 mit eigenem Benutzer, SHA256-Pruefsumme, config.sh und systemd. Beachten Sie das Fork-PR-Risiko.
Was ein selbst gehosteter GitHub Actions Runner tut
Ein selbst gehosteter GitHub Actions Runner ist ein Programm, das Sie auf Ihrem eigenen VPS installieren. Es fragt GitHub nach Aufträgen und führt sie auf Ihrer Hardware aus. Sie registrieren ihn für ein Repository und installieren ihn als systemd-Dienst. Nach jedem Neustart wird er wieder gestartet. GitHub plant den Auftrag ein. Ihr Server führt ihn aus.
CI (Continuous Integration) auf einem eigenen System hat zwei Vorteile. Build-Minuten werden nicht mehr abgerechnet. Außerdem kann ein Auftrag auf Ressourcen zugreifen, die nur auf Ihrem System vorhanden sind, etwa einen vorgewärmten Build-Cache oder ein privates Netzwerk. Der Nachteil betrifft die Sicherheit. Der Runner führt alles aus, was in der Workflow-Datei angegeben ist, und verwendet dabei den Benutzer, den Sie ihm zugewiesen haben. Eine Workflow-Datei ermöglicht daher absichtlich die Ausführung von Code aus der Ferne. Bei einem privaten Repository ist das unproblematisch, weil nur vertrauenswürdige Personen eine solche Datei hinzufügen können. Bei einem öffentlichen Repository besteht ein echtes Risiko. Der Abschnitt zu Pull Requests aus Forks erklärt den Mechanismus.
Alle folgenden Schritte verwenden Ubuntu 24.04 mit Runner-Version 2.336.0, der im Juli 2026 aktuellen Version.
Was Sie vor dem Start benötigen
Beginnen Sie mit einem VPS mit einem normalen Administratorkonto und sudo, also mit dem Zustand, den Sie in den ersten zehn Minuten auf einem neuen VPS erreichen. Sie müssen keinen eingehenden Port öffnen. Der Runner stellt eine ausgehende HTTPS-Verbindung (Hypertext Transfer Protocol Secure) zu GitHub her und hält sie offen, während er auf Arbeit wartet. GitHub verbindet sich daher nie mit Ihrem Server. Ihre Firewall kann für die Außenwelt geschlossen bleiben, und Jobs treffen trotzdem ein.
Sie benötigen außerdem Administratorrechte für das Repository, weil das Registrierungstoken in den Repository-Einstellungen angezeigt wird.
Dedizierten Benutzer für den Runner erstellen
Führen Sie den Runner niemals als root oder mit Ihrem eigenen Administratorkonto aus. Jeder Job übernimmt die Berechtigungen des Runner-Benutzers. Daher ist ein Workflow, der sudo aufruft, erfolgreich, wenn der Runner-Benutzer sudo verwenden kann. Erstellen Sie einen nicht privilegierten Benutzer, dem ausschließlich sein eigenes Home-Verzeichnis gehört. Konten mit geringsten Berechtigungen auf einem VPS beschreibt das allgemeine Vorgehen. Hier ist die konkrete Variante.
sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runnerpasswd -l sperrt das Passwort. Dadurch kann sich niemand mit einem Passwort als gharunner anmelden. Der Modus 700 für das Runner-Verzeichnis ist wichtig, weil der Runner dort seine Anmeldedaten im Klartext speichert und ein Checkout privaten Quellcode enthalten kann.
Prüfen Sie beide Eigenschaften, bevor Sie fortfahren:
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -S gibt eine Zeile aus, die mit gharunner L beginnt. L bedeutet, dass das Passwort gesperrt ist. sudo -l -U gharunner sollte mit is not allowed to run sudo antworten. Wenn stattdessen eine Liste zulässiger Befehle ausgegeben wird, befindet sich das Konto in einer sudo-Gruppe. Die gerade eingerichtete Isolation ist dann nicht mehr gegeben.
Runner herunterladen und das tarball prüfen
Arbeiten Sie ab hier als Benutzer runner.
sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.336.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"Führen Sie zuerst uname -m aus, wenn Sie sich bei der Architektur nicht sicher sind. x86_64 verwendet die oben angegebene Datei linux-x64. aarch64 verwendet actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz.
Prüfen Sie nun den Download. Der folgende SHA256-Wert (Secure Hash Algorithm, 256 Bit) gehört zum tarball 2.336.0 für x64. GitHub zeigt den Wert für das aktuelle Release auf der Release-Seite und im Fenster „New self-hosted runner“ an. Der Wert ändert sich mit jeder Version. Übernehmen Sie ihn daher beim Installieren einer anderen Version von dort.
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -cBei einem fehlerfreien Download wird eine Zeile ausgegeben:
actions-runner-linux-x64-2.336.0.tar.gz: OKBei einer abgeschnittenen oder veränderten Datei werden der Fehler und eine Warnung ausgegeben:
actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT matchÜberspringen Sie diese Prüfung nicht, damit tar das Problem nicht erst später erkennt. Ein unvollständig geschriebenes Archiv führt zu gzip: stdin: unexpected end of file und tar: Unexpected EOF in archive. Das zeigt, dass die Datei beschädigt ist, aber nicht, ob sie zu früh abgeschnitten oder ersetzt wurde.
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lsWas das Tarball enthält und was nicht
Nach dem Entpacken enthält das Verzeichnis config.sh, run.sh, env.sh, safe_sleep.sh, bin/ und externals/. bin/ enthält die Runner-Binärdateien und bin/installdependencies.sh. externals/ enthält die gebündelte Node-Laufzeitumgebung, in der JavaScript-Aktionen ausgeführt werden.
svc.sh ist noch nicht vorhanden. In der GitHub-Dokumentation wird sie als das Skript beschrieben, „das nach dem erfolgreichen Hinzufügen des Runners erstellt wird“. Sie wird aus einer Vorlage erzeugt, in der Ihr Repository- und Runner-Name in den Servicenamen übernommen werden. Daher schlägt sudo ./svc.sh install vor ./config.sh mit sudo: ./svc.sh: command not found fehl. Registrieren Sie den Runner zuerst und installieren Sie anschließend den Service.
Abhängigkeiten für den Runner installieren
Der Runner ist eine .NET-Anwendung und benötigt daher einige gemeinsam genutzte Bibliotheken. Belassen Sie die Shell des Runner-Benutzers und installieren Sie die Bibliotheken mit sudo, da das Skript die systemweite Paketdatenbank ändert.
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shUnter Ubuntu 24.04 werden dadurch libkrb5-3, zlib1g, liblttng-ust1t64, libssl3t64 und libicu74 installiert. Das Skript versucht für jede Bibliothek mehrere Versionsnamen und verwendet den Namen, den Ihre Release-Version bereitstellt. Deshalb funktioniert dasselbe Skript auch mit älteren Ubuntu-Versionen und mit Debian.
Wenn Sie diesen Schritt überspringen, wird ./config.sh beendet, bevor es eine Aktion ausführt:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.Bei einem fehlenden libicu wird unter einer anderen ersten Zeile derselbe Hinweis ausgegeben: Libicu's dependencies is missing for Dotnet Core 6.0. Beide Meldungen haben dieselbe Ursache: config.sh führt vor dem Start ldd für die mitgelieferten Bibliotheken aus. Deshalb beendet ein nicht aufgelöster Link das Skript, anstatt später einen schwer verständlichen Absturz zu verursachen.
Runner bei Ihrem Repository registrieren
Rufen Sie ein Token aus dem Repository ab. Öffnen Sie Settings, dann Actions, dann Runners und anschließend New self-hosted runner. Die Seite zeigt ein Registrierungstoken an, das mit A beginnt. Es läuft eine Stunde nach der Erstellung ab. Erzeugen Sie es daher erst, wenn Sie bereit sind, es einzufügen.
Registrieren Sie den Runner als Benutzer, unter dem er ausgeführt werden soll. config.sh kann nicht mit sudo ausgeführt werden.
sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
--token PASTE_REGISTRATION_TOKEN_HERE \
--name vps-runner-1 \
--labels vps \
--work _work \
--unattended \
--replaceDiese Flags haben folgende Funktion: --name legt fest, wie der Runner im Repository angezeigt wird. Wählen Sie daher einen Namen, den Sie auch in sechs Monaten noch erkennen. --labels fügt eigene Labels hinzu. Der Runner verfügt bereits automatisch über self-hosted, Linux und X64. --work legt das Verzeichnis fest, in dem Checkouts innerhalb des Runner-Verzeichnisses abgelegt werden. --unattended beantwortet interaktive Eingabeaufforderungen mit den Standardwerten. Das ist für ein Kommando in einem Script erforderlich. --replace übernimmt eine vorhandene Registrierung mit demselben Namen, statt mit einem Fehler abzubrechen. Das ist beim erneuten Aufbau des Servers erforderlich.
Eine erfolgreiche Ausführung endet mit diesen Zeilen:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.Die Registrierung befindet sich jetzt im Runner-Verzeichnis als .runner, .credentials und .credentials_rsaparams. Die letzten beiden Dateien identifizieren diesen Runner gegenüber GitHub. Jeder, der sie lesen kann, kann den Runner daher imitieren. Deshalb hat das Verzeichnis den Modus 700 und der Benutzer verfügt über kein sudo.
Den Runner als systemd-Dienst installieren
./run.sh in einem Terminal ist für einen einzelnen Test geeignet, wird aber mit Ihrer SSH-Sitzung beendet. Installieren Sie den Dienst, damit der Runner beim Systemstart gestartet wird. systemd-Dienste und Timer auf einem VPS erklärt die Unit-Dateien selbst. Hier erstellt svc.sh eine Unit-Datei für Sie.
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh erfordert root, weil das Programm eine Unit-Datei in /etc/systemd/system schreibt und den Dienst aktiviert. Das Argument nach install gibt den Benutzer an, unter dem der Dienst ausgeführt wird. Übergeben Sie gharunner ausdrücklich. Ohne Argument verwendet das Skript $SUDO_USER. Das ist Ihr Administratorkonto. Dann wird jeder Job als Benutzer ausgeführt, der sudo verwenden kann.
Die Unit wird nach dem Repository und dem Runner im Format actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service benannt. Sie müssen diesen Namen nicht selbst eingeben:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerEin fehlerfrei laufender Runner protokolliert √ Connected to GitHub und anschließend eine Zeile, die mit Listening for Jobs endet. Auf der Runners-Seite des Repositorys wird er als Idle angezeigt. Ein Runner mit dem Status Offline wird entweder nicht ausgeführt oder kann GitHub über Port 443 nicht erreichen.
Einen Auftrag an den Runner senden
runs-on wählt einen Runner anhand seiner Bezeichnung aus. Fordern Sie self-hosted und zusätzlich Ihre eigene Bezeichnung an, damit ein Auftrag nicht auf einem Runner ausgeführt wird, den Sie nicht vorgesehen haben.
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aWenn der Auftrag bei Waiting for a runner to pick up this job wartet, stimmen die Bezeichnungen nicht überein. Jede Bezeichnung in runs-on muss auf dem Runner vorhanden sein. Ein zusätzliches Wort lässt den Auftrag ohne Fehlermeldung in der Warteschlange. Vergleichen Sie die Liste mit den Bezeichnungen, die in den Repository-Einstellungen neben dem Runner angezeigt werden.
Warum selbst gehostete Runner und öffentliche Repositorys nicht zusammenpassen
Das ist der Teil, den viele überspringen. Die Empfehlung von GitHub ist eindeutig: Selbst gehostete Runner „sollten fast nie für öffentliche Repositorys verwendet werden“ und „bieten keine Garantie dafür, dass sie in kurzlebigen, sauberen virtuellen Maschinen ausgeführt werden. Außerdem können sie durch nicht vertrauenswürdigen Code in einem Workflow dauerhaft kompromittiert werden“.
Der Ablauf ist einfach. Ein Pull Request aus einem Fork enthält eine eigene Kopie der Workflow-Datei. Wenn Ihr öffentliches Repository Pull-Request-Workflows auf Ihrem Runner ausführt, kann jeder, der das Repository forken kann, einen Workflow vorschlagen, der seine Befehle auf Ihrem VPS ausführt. Dafür ist kein Schreibzugriff erforderlich, weil genau der vorgeschlagene Inhalt ausgeführt wird.
Genehmigungseinstellungen entschärfen das Problem, beheben es aber nicht. Die Standardrichtlinie für ein öffentliches Repository fordert einen Maintainer auf, den Workflow eines Forks von einem erstmaligen Beitragenden zu genehmigen. Nachdem Sie diese Person einmal genehmigt haben, werden ihre späteren Pull Requests ohne eine neue Aufforderung ausgeführt. Die Schutzmaßnahme besteht daher jedes Mal darin, dass ein Mensch einen Diff liest. Eine in einem Build-Skript drei Ebenen tiefer verborgene Nutzlast wird leicht übersehen.
Ein Pull Request aus einem Fork erhält Ihre Secrets nicht, und sein GITHUB_TOKEN ist schreibgeschützt. Das begrenzt den Schaden innerhalb von GitHub. Für Ihren Server ändert es nichts. Der Angreifer verfügt über eine Shell als gharunner. Er kann daher jede Datei lesen, auf die dieser Benutzer zugreifen kann, alles erreichen, was der VPS in seinem privaten Netzwerk erreichen kann, und etwas in ~/.bashrc oder in einer Benutzer-systemd-Unit hinterlassen, die während des nächsten Jobs ausgeführt wird.
Die Registrierung mit --ephemeral sorgt dafür, dass der Runner einen Job annimmt und sich anschließend deregistriert. Dadurch kann ein Job nicht den Workspace des nächsten Jobs lesen. Das hilft nur, wenn die Maschine oder der Container für jeden Job neu erstellt wird, weil eine im Home-Verzeichnis des Runner-Benutzers abgelegte Hintertür eine neue Registrierung überlebt.
Die folgenden Regeln sind kurz. Verwenden Sie selbst gehostete Runner für private Repositorys. Wenn Sie einen Runner an ein öffentliches Repository anbinden müssen, führen Sie darauf keine Pull Requests aus Forks aus, lassen Sie auf diesem Server nichts anderes laufen und behandeln Sie die Maschine als jederzeit ersetzbar.
Docker-Jobs und die Gruppe, die tatsächlich root entspricht
Container-Jobs, Service-Container und jeder Workflow-Schritt, der docker build aufruft, benötigen einen Docker-Daemon auf dem Runner-Host. Installieren Sie Docker auf die übliche Weise. Docker und Docker Compose auf einem VPS beschreibt dieses Vorgehen. Fügen Sie anschließend den Runner-Benutzer zur Gruppe docker hinzu.
Beachten Sie die Auswirkungen, bevor Sie dies tun. Die Mitgliedschaft in der Gruppe docker entspricht root, weil ein Container / als Bind-Mount einbinden und darin als root ausgeführt werden kann. Ein Workflow, der mit dem Docker-Socket kommunizieren kann, kann daher jede Datei auf dem VPS lesen und schreiben, einschließlich /etc/shadow. Bei einem privaten Repository mit vertrauenswürdigen Mitwirkenden kann dieser Preis akzeptabel sein. In allen anderen Fällen entfällt dadurch der Zweck des unprivilegierten Benutzers. Rootless Docker beschränkt Container-Builds auf die Berechtigungen des Runner-Benutzers. Dafür sind der Storage-Treiber langsamer und privilegierte Container nicht möglich.
Updates und sauberes Entfernen des Runners
Ein selbst gehosteter Runner aktualisiert sich standardmäßig selbst. Er erkennt eine neue Version, ersetzt seine eigenen Dateien und startet den Dienst neu. Normalerweise müssen Sie daher nichts tun. ./config.sh --disableupdate deaktiviert die automatische Aktualisierung, wenn Sie eine feste Version benötigen. Danach müssen Sie die Aktualisierung selbst durchführen: In der GitHub-Dokumentation wird ausdrücklich darauf hingewiesen, dass ein mit --disableupdate konfigurierter Runner manuell aktualisiert werden muss.
Bei einer manuellen Aktualisierung bleibt die Registrierung erhalten, weil .runner und .credentials nicht im Tarball enthalten sind. Stoppen Sie den Dienst, laden Sie den neuen Tarball als gharunner herunter und prüfen Sie seine Prüfsumme. Entpacken Sie ihn mit tar xzf über dasselbe Verzeichnis. Starten Sie den Dienst anschließend erneut:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startUm den Runner zu entfernen, deinstallieren Sie zuerst den Dienst und heben Sie anschließend die Registrierung auf. Das Entfernungstoken finden Sie auf derselben Runners-Seite unter der Schaltfläche Remove des Runners.
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HEREWenn Sie das Verzeichnis löschen, ohne die Registrierung aufzuheben, bleibt der Runner im Repository als Offline aufgeführt. GitHub erfährt erst davon, dass er entfernt wurde, wenn der Runner dies meldet oder ein Administrator den Eintrag manuell löscht.
Fehlerbilder mit den angezeigten Meldungen
Must not run with sudo. config.sh gibt diese Meldung aus und wird beendet, wenn es als root ausgeführt wird. Die Prüfung ist beabsichtigt, weil Dateien im Besitz von root in _work alle späteren Jobs beschädigen, die als Servicebenutzer ausgeführt werden. Führen Sie ./config.sh als gharunner aus. Die Variable RUNNER_ALLOW_RUNASROOT setzt die Prüfung außer Kraft. Dadurch wird der Fehler jedoch nur auf einen späteren Zeitpunkt verschoben.
sudo: ./svc.sh: command not found. Sie befinden sich im richtigen Verzeichnis. svc.sh ist noch nicht vorhanden, weil config.sh noch keine Registrierung abgeschlossen hat. Registrieren Sie den Runner und installieren Sie anschließend den Service.
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'. Das Token ist kein gültiges Registrierungstoken. Es ist entweder abgelaufen, da es nur eine Stunde gültig ist, oder anstelle des Registrierungstokens von der Seite „Runners“ wurde ein persönliches Zugriffstoken eingefügt. Erstellen Sie ein neues Token und fügen Sie es erneut ein.
Dependencies is missing for Dotnet Core 6.0. Führen Sie sudo ./bin/installdependencies.sh aus dem Runner-Verzeichnis als root aus und registrieren Sie den Runner anschließend erneut.
Runner nach einem Neustart offline. Führen Sie systemctl is-enabled 'actions.runner.*' aus. Wenn keine Ausgabe vorhanden ist, wurde ./svc.sh install nie ausgeführt. Der Runner war dann nur innerhalb Ihrer Terminalsitzung vorhanden. Wenn die Unit aktiviert ist und der Runner weiterhin offline ist, lesen Sie journalctl -u 'actions.runner.*' und prüfen Sie ausgehendes HTTPS.
Die Festplatte läuft voll. Checkouts, Build-Caches und Docker-Images sammeln sich unter _work und im Home-Verzeichnis des Runner-Benutzers an. Sie werden nicht automatisch bereinigt. Überwachen Sie du -sh /home/gharunner/actions-runner/_work und richten Sie eine geplante Bereinigung ein, bevor die Festplatte selbst die Entscheidung trifft.
FAQ
Warum meldet sudo ./svc.sh install command not found?
svc.sh befindet sich nicht im Runner-Tarball. Das Skript wird im Runner-Verzeichnis erzeugt, sobald ./config.sh die Registrierung abgeschlossen hat. Dabei werden der Name Ihres Repositorys und der Runner-Name zum Dienstnamen kombiniert. Führen Sie zuerst ./config.sh als Runner-Benutzer aus. Danach findet sudo ./svc.sh install gharunner das Skript und schreibt eine Unit mit dem Namen actions.runner.OWNER-REPO.RUNNER-NAME.service in /etc/systemd/system.
Muss ich einen Firewall-Port für einen selbst gehosteten Runner öffnen?
Nein. Der Runner öffnet eine ausgehende HTTPS-Verbindung zu GitHub und hält sie offen, während er auf Aufträge wartet. GitHub muss daher keine Verbindung zu Ihrem VPS initiieren. Erlauben Sie ausgehenden Datenverkehr über 443 und lassen Sie eingehende Regeln geschlossen. Wenn der Runner bei laufendem Dienst als Offline angezeigt wird, prüfen Sie die Filterung ausgehenden Datenverkehrs und DNS statt der eingehenden Regeln.
Kann ich einen selbst gehosteten Runner für ein öffentliches Repository verwenden?
Das ist möglich, wird von GitHub jedoch nicht empfohlen. Ein Pull Request aus einem Fork enthält seine eigene Workflow-Datei. Jeder Benutzer, der Ihr Repository forken kann, kann daher Befehle vorschlagen, die auf Ihrem Rechner ausgeführt werden. Die Eingabeaufforderung zur Genehmigung gilt nur für den ersten Lauf eines Contributors. Wenn Sie einen Runner an ein öffentliches Repository anbinden, deaktivieren Sie dort Workflows für Pull Requests aus Forks. Bewahren Sie keine anderen Daten auf diesem Server auf und erstellen Sie die Maschine regelmäßig neu.
Warum schlägt die Registrierung mit Http response code: NotFound fehl?
Der Registrierungsaufruf antwortet mit NotFound, wenn die Zugangsdaten falsch sind, und nicht nur, wenn die URL falsch ist. Dadurch ist die Meldung irreführend. Registrierungstoken laufen eine Stunde nach ihrer Anzeige ab. Ein Personal Access Token wird für diesen Aufruf nicht akzeptiert. Öffnen Sie erneut Settings, Actions, Runners, New self-hosted runner, kopieren Sie das neue Token und prüfen Sie, ob der Wert --url auf ein Repository verweist, für das Sie Administratorrechte haben.