GitHub Actions Runner auf VPS selbst hosten
So registrieren Sie einen Runner auf Ubuntu 24.04: eigener Benutzer, SHA256-Pruefung, config.sh und systemd-Dienst. Beachten Sie das Risiko bei Fork-Pull-Requests.
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, installieren ihn als systemd-Dienst und er startet nach jedem Reboot wieder. GitHub plant den Auftrag ein. Ihr Server führt ihn aus.
CI (Continuous Integration) auf einem eigenen System lohnt sich aus zwei Gründen. Build-Minuten werden nicht mehr abgerechnet. Außerdem kann ein Auftrag auf Ressourcen zugreifen, die nur auf Ihrem System vorhanden sind, etwa auf einen bereits gefüllten Build-Cache oder ein privates Netzwerk. Der Preis dafür ist die Sicherheit. Der Runner führt alles aus, was die Workflow-Datei vorgibt, und zwar als der 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 vertretbar, weil nur Personen, denen Sie vertrauen, 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 beziehen sich auf Ubuntu 24.04 mit Runner-Version 2.336.0, der aktuellen Version im Juli 2026.
Was Sie vor dem Start benötigen
Beginnen Sie mit einem VPS mit einem normalen Administratorkonto und sudo, also mit dem Zustand aus den ersten zehn Minuten auf einem neuen VPS. Sie müssen keinen eingehenden Port öffnen. Der Runner baut eine ausgehende HTTPS-Verbindung (Hypertext Transfer Protocol Secure) zu GitHub auf und hält sie offen, während er auf Aufgaben 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.
Einen dedizierten Benutzer für den Runner erstellen
Führen Sie den Runner niemals als root oder als Ihren eigenen Administratorkonto aus. Jeder Job übernimmt die Berechtigungen des Runner-Benutzers. Daher ist ein Workflow erfolgreich, der sudo aufruft, wenn der Runner-Benutzer sudo verwenden darf. Erstellen Sie einen nicht privilegierten Benutzer, dem außer seinem eigenen Home-Verzeichnis nichts gehört. Benutzerkonten mit minimalen Berechtigungen auf einem VPS beschreibt das allgemeine Vorgehen. Hier folgt die konkrete Umsetzung.
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 diesem Passwort als gharunner anmelden. Der Modus 700 für das Runner-Verzeichnis ist wichtig, weil der Runner seine Zugangsdaten dort im Klartext speichert und ein Checkout private Quelldateien 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, gehört das Konto zu einer sudo-Gruppe. Die gerade eingerichtete Isolation ist dann nicht mehr gegeben.
Runner herunterladen und das tar-Archiv 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 jetzt den Download. Der folgende SHA256-Wert (Secure Hash Algorithm, 256 Bit) gehört zum x64-tar-Archiv für 2.336.0. GitHub zeigt den Wert für das aktuelle Release auf der Release-Seite und im Dialog „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 unvollständigen 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 feststellt. Ein unvollständig geschriebenes Archiv schlägt bei gzip: stdin: unexpected end of file und tar: Unexpected EOF in archive fehl. Dadurch ist zwar erkennbar, dass die Datei beschädigt ist, aber nicht, ob sie zu kurz ist 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, auf der JavaScript-Actions ausgeführt werden.
svc.sh ist zu diesem Zeitpunkt noch nicht vorhanden. In der Dokumentation von GitHub 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 der Name des Runners in den Dienstnamen ü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 Dienst.
Installieren Sie die Runner-Abhängigkeiten
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 verä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 probiert für jede Bibliothek mehrere Versionsnamen aus und verwendet den Namen, den Ihre Version bereitstellt. Deshalb funktioniert dasselbe Skript auch mit älteren Ubuntu-Versionen und mit Debian.
Überspringen Sie diesen Schritt, beendet sich ./config.sh, bevor es irgendetwas ausführt:
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.Eine fehlende libicu führt unter einer anderen ersten Zeile zu derselben Empfehlung: 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. Wenn eine Verknüpfung nicht aufgelöst werden kann, beendet sich das Skript, statt 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 seiner Erstellung ab. Erzeugen Sie es daher erst, wenn Sie bereit sind, es einzufügen.
Registrieren Sie den Runner als Benutzer des Runners. config.sh kann nicht unter 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 Bedeutung. Mit --name wird festgelegt, unter welchem Namen 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 ohne weitere Angaben über self-hosted, Linux und X64. --work legt das Verzeichnis fest, in dem Checkouts innerhalb des Runner-Verzeichnisses abgelegt werden. --unattended beantwortet die interaktiven Eingabeaufforderungen mit den Standardwerten. Das ist sinnvoll, wenn der Befehl in einem Skript ausgeführt wird. --replace übernimmt eine vorhandene Registrierung mit demselben Namen, anstatt mit einem Fehler abzubrechen. Das ist beim Neuaufbau des Servers erwünscht.
Ein erfolgreicher Lauf endet mit diesen Zeilen:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.Die Registrierung befindet sich nun 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 sich daher als dieser Runner ausgeben. Deshalb hat das Verzeichnis die Berechtigungen 700 und der Benutzer keinen Zugriff auf sudo.
Installieren Sie den Runner als systemd-Dienst
./run.sh in einem Terminal eignet sich für einen einzelnen Test, wird jedoch mit Ihrer SSH-Sitzung beendet. Installieren Sie den Dienst, damit der Runner beim Booten startet. systemd-Dienste und Timer auf einem VPS erläutert die Unit-Dateien selbst. Hier erstellt svc.sh eine solche 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 Skript eine Unit in /etc/systemd/system schreibt und sie aktiviert. Das Argument nach install gibt den Benutzer an, unter dem der Dienst ausgeführt wird. Übergeben Sie gharunner ausdrücklich. Ohne dieses Argument verwendet das Skript $SUDO_USER. Das ist Ihr Administratorkonto. Danach wird jeder Job mit einem Benutzer ausgeführt, der sudo verwenden kann.
Die Unit wird nach dem Repository und dem Runner benannt, und zwar im Format actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.service. Sie müssen diesen Namen nicht selbst eingeben:
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pagerEin funktionsfähiger Runner protokolliert √ Connected to GitHub und anschließend eine Zeile, die mit Listening for Jobs endet. Auf der Seite Runners des Repositorys wird er dann 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 seines Labels aus. Fordern Sie zusätzlich self-hosted und Ihr eigenes Label 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 Labels nicht überein. Jedes Label in runs-on muss auf dem Runner vorhanden sein. Bereits ein zusätzliches Wort lässt den Auftrag ohne Fehlermeldung in der Warteschlange bleiben. Vergleichen Sie die Liste mit den Labels, die in den Repository-Einstellungen neben dem Runner angezeigt werden.
Warum selbst gehostete Runner und öffentliche Repositorys nicht zusammenpassen
Dies 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 GitHub „gibt keine Garantien dafür, dass sie in kurzlebigen, sauberen virtuellen Maschinen ausgeführt werden, und sie können 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 der vorgeschlagene Inhalt selbst ausgeführt wird.
Genehmigungseinstellungen reduzieren dieses Risiko, 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 erneute Aufforderung ausgeführt. Die Schutzmaßnahme besteht daher jedes Mal aus einer manuellen Prüfung des Diffs. Eine in einem Build-Skript drei Ebenen tiefer verborgene Payload 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 sich dadurch nichts. Der Angreifer verfügt über eine Shell als gharunner. Damit kann er 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 systemd-Benutzereinheit hinterlassen, die während des nächsten Jobs ausgeführt wird.
Die Registrierung mit --ephemeral veranlasst den Runner, einen Job anzunehmen und sich anschließend abzumelden. 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. Eine in das Home-Verzeichnis des Runner-Benutzers geschriebene Backdoor bleibt bei einer erneuten Registrierung erhalten.
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, betreiben Sie auf diesem Server nichts anderes und behandeln Sie die Maschine als austauschbar.
Docker-Jobs und die Gruppe, die tatsächlich root bedeutet
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. Eine Anleitung dazu finden Sie unter Docker und Docker Compose auf einem VPS. Fügen Sie anschließend den Runner-Benutzer der Gruppe docker hinzu.
Machen Sie sich vorab mit dem damit verbundenen Risiko vertraut. 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. In einem privaten Repository mit vertrauenswürdigen Mitwirkenden kann dieser Preis akzeptabel sein. In allen anderen Fällen wird der Zweck des unprivilegierten Benutzers dadurch aufgehoben. Rootless Docker hält Container-Builds innerhalb der Berechtigungen des Runner-Benutzers. Dafür sind die Speicherzugriffe langsamer, und privilegierte Container werden nicht unterstützt.
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.
Eine manuelle Aktualisierung behält die Registrierung bei, weil sich .runner und .credentials nicht im Tarball befinden. 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 wieder:
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 betreffenden 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 von seiner Entfernung, wenn der Runner dies meldet oder ein Administrator den Eintrag manuell löscht.
Fehlerbilder und die angezeigten Meldungen
Must not run with sudo. config.sh gibt diese Meldung aus und beendet sich, wenn es als root ausgeführt wird. Diese Prüfung ist beabsichtigt, weil root gehörende Dateien in _work jeden späteren Job beschädigen, der als Dienstbenutzer ausgeführt wird. Führen Sie ./config.sh als gharunner aus. Die Variable RUNNER_ALLOW_RUNASROOT deaktiviert die Prüfung. Dadurch wird das Problem lediglich 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 Dienst.
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 Registrierungstokens nur eine Stunde gültig sind, oder anstelle des Registrierungstokens von der Seite „Runners“ wurde ein persönliches Zugriffstoken eingefügt. Erzeugen 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 als root aus dem Runner-Verzeichnis aus und registrieren Sie den Runner anschließend erneut.
Runner nach einem Reboot offline. Führen Sie systemctl is-enabled 'actions.runner.*' aus. Wenn nichts aufgelistet wird, wurde ./svc.sh install nie ausgeführt. Der Runner existierte dann nur innerhalb Ihrer Terminalsitzung. Wenn die Unit aktiviert ist und der Runner weiterhin offline ist, lesen Sie journalctl -u 'actions.runner.*' und prüfen Sie ausgehende HTTPS-Verbindungen.
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 voll ist.
FAQ
Warum meldet sudo ./svc.sh install „command not found“?
Weil svc.sh nicht im Runner-Tarball enthalten ist. Das Skript wird im Runner-Verzeichnis erzeugt, wenn ./config.sh die Registrierung abgeschlossen hat. Dabei werden der Name Ihres Repositorys und der Runner-Name verwendet, um den Dienstnamen zu bilden. Führen Sie zuerst ./config.sh als Runner-Benutzer aus. Danach findet sudo ./svc.sh install gharunner das Skript und schreibt eine Unit namens actions.runner.OWNER-REPO.RUNNER-NAME.service nach /etc/systemd/system.
Muss ich einen Firewall-Port für einen Self-Hosted Runner öffnen?
Nein. Der Runner öffnet eine ausgehende HTTPS-Verbindung zu GitHub und hält sie offen, während er auf Jobs wartet. GitHub initiiert daher keine Verbindung zu Ihrem VPS. Erlauben Sie ausgehenden Verkehr auf Port 443 und lassen Sie eingehende Verbindungen blockiert. Wenn der Runner trotz laufendem Dienst als Offline angezeigt wird, prüfen Sie die Filterung ausgehender Verbindungen und DNS statt der eingehenden Regeln.
Kann ich einen Self-Hosted Runner für ein öffentliches Repository verwenden?
Das ist möglich, wird von GitHub aber 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 Bestätigungsabfrage gilt nur für den ersten Lauf eines Mitwirkenden. Wenn Sie einen Runner an ein öffentliches Repository anbinden, deaktivieren Sie dort Workflows für Pull Requests aus Forks, betreiben Sie auf diesem Server keine anderen Dienste und setzen Sie den Rechner regelmäßig neu auf.
Warum schlägt die Registrierung mit Http response code: NotFound fehl?
Der Registrierungsvorgang antwortet mit NotFound, wenn die Zugangsdaten falsch sind, nicht nur wenn die URL falsch ist. Dadurch ist die Meldung irreführend. Registrierungstokens 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.