Forgejo Actions: eigener CI/CD-Runner mit Docker
Einen Forgejo-Actions-Runner mit Docker-in-Docker aufsetzen und registrieren: eigene CI/CD-Pipelines auf dem selbstgehosteten Git-Server – Schritt für Schritt.
Inhaltsverzeichnis
Dein Forgejo-Git-Server läuft – aber Code liegt nur da herum, solange ihn niemand testet und ausrollt. Forgejo Actions bringt CI/CD direkt in deine Git-Plattform: Bei jedem Push laufen automatisch Tests, Builds oder Deployments. Die Arbeit erledigt ein Runner, den du selbst betreibst. Dieses Tutorial setzt einen solchen Runner mit Docker-in-Docker auf und lässt eine erste Pipeline grün durchlaufen.
Was bauen wir?
Am Ende hat dein Forgejo einen registrierten, aktiven Runner, der Workflows aus dem Verzeichnis
.forgejo/workflows/ ausführt. Wir setzen dabei auf Docker-in-Docker (DinD): Der Runner startet
jeden CI-Job in einem eigenen, wegwerfbaren Container, sauber isoliert vom Host. Konkret läuft am
Ende:
- der Forgejo-Runner (
code.forgejo.org/forgejo/runner:13.0.0), der bei Forgejo nach Jobs fragt, - ein Docker-in-Docker-Sidecar (
docker:29-dind), in dem die Jobs isoliert laufen, - ein Beispiel-Repository mit einem Workflow, der bei jedem Push
actions/checkoutausführt und eine kleine Aktion startet.
Forgejo Actions ist weitgehend kompatibel zu GitHub Actions – dieselbe Workflow-Syntax, viele Marketplace-Actions funktionieren unverändert. Du kannst also bestehendes Wissen direkt weiternutzen, nur eben auf deinem eigenen Server. Getestet mit Forgejo 16.0.1 und Runner v13.0.0 auf Debian 13 / Docker 29.
Voraussetzungen
- Ein laufender Forgejo-Server hinter einem Reverse Proxy,
erreichbar unter einer öffentlichen HTTPS-Domain (
DEINE_DOMAIN). Die öffentliche URL ist wichtig – dazu unten mehr. - Docker auf demselben Server (der Runner und sein DinD-Sidecar laufen als Container).
- Admin-Zugang zu Forgejo, um den Registrierungstoken zu erzeugen.
Actions ist seit Forgejo 1.21 standardmäßig aktiviert. Falls du es in deiner Forgejo-Compose explizit gesetzt hast (empfohlen), steht dort:
environment:
FORGEJO__actions__ENABLED: "true"CI-Jobs sind ressourcenhungriger als der reine Git-Server – Builds brauchen CPU und RAM. Für den Runner-Betrieb neben Forgejo empfehlen wir daher etwas mehr Reserve; wie viel dein konkretes Setup braucht, schätzt der Server-Rechner.
VPS 2000 G12
8 vCore · 16 GB RAM · 512 GB NVMe
ab 19,24 €/Monat
Forgejo plus Runner und Build-Jobs profitieren vom größeren Tarif.
💶 5 € Gutschein für netcup-Neukunden:36nc17844976032
(nur Neukunden, keine Domains)
Schritt für Schritt
Schritt 1: Registrierungstoken holen
Der Runner muss sich einmalig bei Forgejo anmelden. Dafür brauchst du einen Registrierungstoken. Am einfachsten holst du ihn über die Weboberfläche: Melde dich als Administrator an und geh auf Administrator-Einstellungen → Actions → Runner. Dort siehst du alle Runner und oben rechts den Knopf Registrierungstoken anzeigen.

Alternativ per Kommandozeile direkt im Forgejo-Container:
docker exec -u git forgejo forgejo actions generate-runner-tokenDas gibt einen langen Token aus – kopiere ihn, du brauchst ihn gleich einmal.
Tipp
Schritt 2: Die Runner-Compose schreiben
Leg einen eigenen Ordner an und wechsle hinein:
mkdir -p /opt/forgejo-runner && cd /opt/forgejo-runnerErstelle die compose.yaml:
services:
docker:
image: docker:29-dind
container_name: fjr-docker
privileged: true
restart: unless-stopped
environment:
DOCKER_TLS_CERTDIR: /certs
volumes:
- dind_certs:/certs
- runner_data:/data
runner:
image: code.forgejo.org/forgejo/runner:13.0.0
container_name: fjr-runner
restart: unless-stopped
depends_on: [docker]
environment:
DOCKER_HOST: tcp://docker:2376
DOCKER_CERT_PATH: /certs/client
DOCKER_TLS_VERIFY: "1"
volumes:
- dind_certs:/certs:ro
- runner_data:/data
working_dir: /data
command: forgejo-runner daemon
volumes:
dind_certs:
runner_data:Die wichtigsten Punkte:
- Der DinD-Dienst heißt bewusst
docker. Sein automatisch erzeugtes TLS-Zertifikat ist auf genau diesen Namen ausgestellt – heißt der Dienst anders, scheitert der Runner mit „certificate is valid for docker, not …" (siehe „Wenn es nicht funktioniert"). privileged: truebraucht DinD, um seine eigene Docker-Engine zu betreiben. Das ist der Preis der Isolation; halte den Runner-Server entsprechend abgesichert.- Der Runner spricht den DinD über
DOCKER_HOST: tcp://docker:2376mit TLS an; die Client-Zertifikate teilt er sich über das Volumedind_certs. - Die Registrierung landet als
.runner-Datei im Volumerunner_dataund übersteht so Neustarts und Updates.
Warum Docker-in-Docker?
/var/run/docker.sock)
hineinzureichen. Das ist einfacher, gibt den CI-Jobs aber faktisch Root auf dem Host – ein
manipulierter Workflow könnte den ganzen Server übernehmen. DinD kapselt die Jobs in einer eigenen
Docker-Instanz und ist die deutlich sicherere Wahl.Schritt 3: DinD starten und den Runner registrieren
Starte zuerst nur den DinD-Sidecar, damit er seine Zertifikate erzeugt:
docker compose up -d dockerJetzt registrierst du den Runner einmalig. Ersetze DEIN_TOKEN durch den Token aus Schritt 1:
docker compose run --rm runner forgejo-runner register \
--no-interactive \
--instance https://DEINE_DOMAIN \
--token DEIN_TOKEN \
--name mein-runner \
--labels "docker:docker://node:24-bookworm"Bei Erfolg endet die Ausgabe mit Runner registered successfully. (eine Warnung, dass register
„deprecated" sei, kannst du ignorieren – es funktioniert).
Unbedingt die öffentliche URL verwenden
https://DEINE_DOMAIN) – nicht mit
einer internen wie http://forgejo:3000. Grund: Die CI-Jobs laufen im DinD in eigenen Containern
mit eigenem Netzwerk und können interne Docker-Namen nicht auflösen. Beim Auschecken müssen sie
den Git-Server aber erreichen. Mit der öffentlichen URL klappt das von überall – mit einem internen
Namen scheitert jeder Job beim checkout.Das Label docker:docker://node:24-bookworm bedeutet: Jobs mit runs-on: docker werden in einem
node:24-bookworm-Container ausgeführt (bringt Node.js und die üblichen Build-Tools mit). Node 24
ist die aktuell aktive LTS-Linie – Node 20 ist seit April 2026 aus dem Support.
Schritt 4: Den Runner starten und Status prüfen
Jetzt startest du den ganzen Stack:
docker compose up -dPrüfe, dass beide Container laufen:
docker compose psWirf einen Blick ins Runner-Log – hier siehst du, ob die Anmeldung geklappt hat:
docker compose logs runnerDu solltest eine Zeile wie declared successfully und [poller] launched sehen – der Runner fragt
Forgejo jetzt aktiv nach Jobs. In der Weboberfläche unter Administrator-Einstellungen → Actions →
Runner taucht mein-runner mit einem grünen Statuspunkt und dem Label docker auf (siehe
Screenshot oben). Steht er auf Inaktiv mit grünem Punkt, ist alles gut: Er ist verbunden und
wartet nur auf Arbeit.
Schritt 5: Den ersten Workflow anlegen
Workflows liegen im Repository unter .forgejo/workflows/. Lege in einem beliebigen Repo die Datei
.forgejo/workflows/ci.yml an:
name: CI
on: [push]
jobs:
test:
runs-on: docker
steps:
- uses: actions/checkout@v7
- run: echo "Commit $GITHUB_SHA wird getestet"
- run: node --versionZerlegt:
on: [push]– der Workflow startet bei jedem Push.runs-on: docker– wählt unseren Runner über das Labeldockeraus.actions/checkout@v7– checkt den Code aus (dieselbe Action wie bei GitHub; Forgejo lädt sie automatisch aus seinem Action-Register).- Die beiden
run-Schritte geben den Commit und die Node-Version aus – ein minimales, aber echtes Beispiel, das du später durch deine echten Build-/Test-Befehle ersetzt.
Committe und pushe die Datei. Der Push löst den Workflow sofort aus.
Schritt 6: Den Lauf ansehen
Öffne im Repository den Reiter Actions. Dort erscheint dein Lauf – nach wenigen Sekunden mit einem grünen Haken:

Ein Klick auf den Lauf öffnet die Job-Ansicht mit den einzelnen Schritten und ihren Logs. Hier
siehst du, wie actions/checkout das Repository klont und die Befehle nacheinander laufen:

Der node --version-Schritt gibt bei uns v24.20.0 aus – der Beweis, dass der Job wirklich im
node:24-bookworm-Container gelaufen ist. Damit steht deine CI/CD: Ab jetzt kannst du in den
run-Schritten testen, bauen und deployen, was du brauchst.
Wenn es nicht funktioniert
Der Runner startet neu und meldet „cannot ping the docker daemon … certificate is valid for
docker, …, not fjr-docker". Der DinD-Dienst heißt anders als docker, aber sein TLS-Zertifikat
ist auf docker ausgestellt. Nenne den DinD-Service exakt docker (wie oben) und sprich ihn
über DOCKER_HOST: tcp://docker:2376 an – dann passt der Name zum Zertifikat.
Der Job startet, scheitert aber beim actions/checkout mit einem Verbindungsfehler. Der Runner
wurde mit einer internen Instanz-URL (http://forgejo:3000) registriert. Die Job-Container im
DinD können diesen Namen nicht auflösen. Neu registrieren mit der öffentlichen URL
https://DEINE_DOMAIN (.runner-Datei im Volume vorher löschen oder das Volume neu anlegen).
Der Runner erscheint gar nicht in der Übersicht / die Registrierung schlägt fehl. Falscher oder
bereits verbrauchter Token, oder der Runner erreicht Forgejo nicht. Frischen Token holen (Schritt 1)
und prüfen, dass der Runner-Container https://DEINE_DOMAIN erreicht (docker compose run --rm runner wget -qO- https://DEINE_DOMAIN/api/healthz).
Ein Job bleibt ewig „wartend" (pending). Kein Runner hat ein passendes Label. Der Workflow
nutzt runs-on: docker, der Runner muss also das Label docker tragen. Labels beim Registrieren
prüfen; in der Runner-Übersicht werden die Labels je Runner angezeigt.
actions/checkout findet die Action nicht. Forgejo lädt Actions aus einem konfigurierten
Register (standardmäßig data.forgejo.org). Ist der Server komplett vom Internet abgeschnitten,
schlägt das fehl. Ausgehenden HTTPS-Zugriff erlauben oder Actions in einem internen Register
spiegeln.
Wartung & Backups
Updates. Runner und DinD aktualisierst du wie jeden Compose-Stack:
cd /opt/forgejo-runner
docker compose pull && docker compose up -dHalte den Runner grob auf Augenhöhe mit deiner Forgejo-Version – eine stark veraltete
Runner-Version kann mit neuen Forgejo-Features Probleme bekommen. Pinne wie oben eine konkrete
Version statt latest, damit Updates bewusst passieren.
Umstieg von Runner v12 auf v13
set-output, set-env und
add-path sind ersatzlos entfernt – schreib stattdessen in die Dateien $FORGEJO_OUTPUT,
$FORGEJO_ENV und $FORGEJO_PATH. Außerdem lassen fehlerhafte Ausdrücke einen Job jetzt hart
scheitern (statt nur zu warnen), und in der Runner-Konfiguration heißt container.network_mode
nur noch container.network. Ein frisch aufgesetzter Runner wie hier ist davon nicht betroffen;
wer bestehende Workflows mitnimmt, liest vorher die
Release-Notes zu v13.Backups. Sicherungswürdig ist vor allem die .runner-Datei im Volume runner_data – sie
enthält die Registrierung. Geht sie verloren, registriert sich der Runner beim nächsten Start als
neuer Runner (der alte bleibt als „offline" in der Übersicht stehen und kann dort gelöscht
werden). Ein Totalverlust ist kein Drama: Du holst einen neuen Token und registrierst neu. Die
DinD-Daten (dind_certs, Job-Caches) sind flüchtig und müssen nicht gesichert werden.
Aufräumen. Die CI-Jobs erzeugen im DinD mit der Zeit ungenutzte Images und Layer. Räum sie gelegentlich auf, damit die Platte nicht vollläuft:
docker compose exec docker docker system prune -afSicherheit. Der DinD läuft privileged – behandle den Runner-Host wie ein sicherheitskritisches
System: nur nötige Ports offen, keine anderen sensiblen Dienste daneben, und CI nur für Repositories,
deren Workflows du kontrollierst. Wer Workflows aus fremden Forks zulässt, sollte sich vorher intensiv
mit deren Risiken beschäftigen.
Zuletzt aktualisiert: 28.08.2026
Feedback per E-Mail: feedback@serverkueche.de
Das könnte dir auch schmecken

Die eigene Website mit Hugo hosten – wie die Serverküche selbst
Statische Website mit Hugo bauen und im Docker-Container hinter Traefik ausliefern – schnell, sicher, ohne Datenbank. …

Immich optimieren: Handy-Backup, deutsche KI-Suche & Familien-Betrieb (Teil 2)
Immich Teil 2: Handy-Backup zuverlässig machen, die KI-Suche auf Deutsch umstellen, Bestandsfotos als externe Bibliothek …

AdGuard Home: netzwerkweiter Werbe- und Tracking-Blocker
AdGuard Home mit Docker aufsetzen: ein eigener DNS-Server, der Werbung, Tracker und Schadseiten für alle Geräte blockt – …