Forgejo mit Actions-Runner: Website per git push ausrollen
Teil 15 der Reihe Server im Netz: Forgejo und forgejo-runner auf dem VPS bauen die Hugo-Website bei jedem git push und rollen sie aus. Mit drei echten Läufen, gemessener Zeit bis online und den Fallen bei Rechten und Datum.
Geprüft am 05.11.2026 · für Forgejo 17.0.1, forgejo-runner 13.2.1, Hugo 0.169.0
WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Die Hugo-Website aus Teil 5 haben wir bisher von Hand gebaut und auf den Server kopiert. Das geht, wird aber lästig und fehleranfällig, sobald öfter etwas Neues erscheint. Mit einem eigenen Git-Server und einer Pipeline reicht ein git push: Der Server baut die Seite und rollt sie aus. Diese Anleitung richtet dafür Forgejo mit einem Actions-Runner auf dem VPS ein. Die Workflows sehen aus wie bei GitHub Actions. Dazu kommen drei echte Läufe aus dem Test, zwei davon mit einer Überraschung.
Dies ist Teil 15 der Reihe „Server im Netz“. Als Beispiel dient ein netcup-VPS mit Ubuntu und Docker, auf dem Caddy die Domain sedv-lab.de ausliefert. Die Hugo-Seite aus Teil 5 liegt hinter Anubis aus Teil 6.
Voraussetzungen
- Der VPS aus Teil 4 mit Caddy im Host-Netz und Docker
- Die Hugo-Seite aus Teil 5, deren Dateien Caddy aus einem Verzeichnis ausliefert
- Ein Name für Forgejo, im Beispiel
git.sedv-lab.de - Git auf dem Rechner, von dem aus Sie pushen
Schritt 1: Forgejo starten
Forgejo konfigurieren wir komplett über Umgebungsvariablen, der Einrichtungsassistent entfällt. Die compose.yaml enthält gleich auch den Runner, den wir in Schritt 4 einrichten:
services:
forgejo:
image: codeberg.org/forgejo/forgejo:17.0.1
container_name: forgejo
restart: unless-stopped
network_mode: host
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__server__HTTP_ADDR: "127.0.0.1"
FORGEJO__server__HTTP_PORT: "3100"
FORGEJO__server__ROOT_URL: "https://git.sedv-lab.de/"
FORGEJO__server__DISABLE_SSH: "true"
FORGEJO__database__DB_TYPE: "sqlite3"
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__actions__ENABLED: "true"
volumes:
- ./forgejo:/data
runner:
image: data.forgejo.org/forgejo/runner:13.2.1
container_name: forgejo-runner
restart: unless-stopped
network_mode: host
user: "0:0"
command: ["forgejo-runner", "daemon", "--config", "/data/config.yml"]
volumes:
- ./runner:/data
- /var/run/docker.sock:/var/run/docker.sock
depends_on: [forgejo]
Forgejo lauscht nur lokal, hier auf Port 3100, weil der übliche Port 3000 auf dem Server schon belegt war. INSTALL_LOCK überspringt den Assistenten, DISABLE_REGISTRATION verhindert, dass Fremde Konten anlegen. SSH für Git schalten wir ab, gepusht wird über HTTPS mit Token. Der Datenordner muss dem Benutzer 1000 gehören:
mkdir -p forgejo runner && chown 1000:1000 forgejo
docker compose up -d forgejo
Dazu kommt ein Block im Caddyfile:
git.sedv-lab.de {
import sicher
reverse_proxy 127.0.0.1:3100
}
Verifizieren: Das Log zeigt Listen: http://127.0.0.1:3100 und AppURL(ROOT_URL): https://git.sedv-lab.de/, die Startseite liefert 200.
Schritt 2: Benutzer und Token
Den ersten Benutzer legen Sie auf der Kommandozeile an, ebenso ein Token für den Zugriff per Git und API:
docker exec -u 1000 forgejo forgejo admin user create --admin \
--username lab --email admin@sedv-lab.de --password "$(openssl rand -hex 16)" --must-change-password=false
docker exec -u 1000 forgejo forgejo admin user generate-access-token \
--username lab --token-name ausrollen --scopes write:repository,write:user --raw
Das Passwort brauchen Sie nur für die Oberfläche. Wollen Sie sich dort anmelden, speichern Sie es vorher in einer Variablen und geben es aus. Das Token heben Sie auf, es erscheint nur einmal. In der Remote-URL steht es danach im Klartext in .git/config, ein Credential-Helper ist die sauberere Lösung. Das Repository legen wir über die API an:
curl -s -H "Authorization: token TOKEN" -H "Content-Type: application/json" \
-d '{"name":"website","private":false,"default_branch":"main"}' \
https://git.sedv-lab.de/api/v1/user/repos
Verifizieren: Die Antwort enthält "full_name":"lab/website".
Schritt 3: Der Workflow
Ins Hugo-Projekt kommt die Datei .forgejo/workflows/ausrollen.yaml:
on:
push:
branches: [main]
jobs:
ausrollen:
runs-on: docker
container:
image: ghcr.io/gohugoio/hugo:v0.169.0
volumes:
- /opt/caddy/site:/ziel
steps:
- name: Quelltext holen
run: git clone --depth 1 "${{ forge.server_url }}/${{ forge.repository }}.git" src
- name: Bauen
run: cd src && hugo --minify
- name: Ausrollen
run: |
cd src/public
find /ziel -mindepth 1 -delete
cp -a . /ziel/
ls /ziel
Der Job läuft im offiziellen Hugo-Image, das Git schon mitbringt. Statt der Aktion actions/checkout, die Node.js im Container braucht, holt ein einfaches git clone den Quelltext. Das klappt hier ohne Anmeldung, weil das Repository öffentlich ist. Der Pfad /opt/caddy/site ist das Verzeichnis auf dem VPS, das Caddy ausliefert. Passen Sie ihn an Ihren Aufbau an. Dateien, die nicht aus Hugo kommen, etwa .well-known/security.txt aus Teil 4, gehören in den Ordner static/ des Projekts, sonst löscht der Workflow sie.
git init -b main && git add -A && git commit -m "Website mit Workflow"
git remote add origin "https://lab:TOKEN@git.sedv-lab.de/lab/website.git"
git push origin main
Verifizieren: Im Repository unter Aktionen erscheint ein Lauf mit dem Namen des Commits.
Schritt 4: Den Runner einrichten
Der Runner holt Aufträge von Forgejo ab und startet für jeden Job einen Container. Er braucht ein Token zur Registrierung und eine Konfiguration:
RT=$(docker exec -u 1000 forgejo forgejo forgejo-cli actions generate-runner-token | tail -1)
docker run --rm -v "$PWD/runner:/data" data.forgejo.org/forgejo/runner:13.2.1 \
forgejo-runner generate-config > runner/config.yml
In der config.yml tragen Sie unter container ein, welche Verzeichnisse Jobs einbinden dürfen. Laut Kommentar in der Datei kann ohne diesen Eintrag kein Job ein Volume einbinden:
valid_volumes:
- /opt/caddy/site
Dann die Registrierung. Sie muss als root laufen, denn das Image arbeitet sonst als eigener Benutzer und darf die Datei .runner im Ordner nicht anlegen. Im Test meldete es genau das: failed to save runner config: open .runner: permission denied.
docker run --rm --user 0:0 --network host -v "$PWD/runner:/data" -w /data \
data.forgejo.org/forgejo/runner:13.2.1 forgejo-runner register --no-interactive \
--instance https://git.sedv-lab.de --token "$RT" --name vps-runner \
--labels docker:docker://data.forgejo.org/oci/node:22-bookworm --config /data/config.yml
docker compose up -d runner
Startet der Runner vor der Registrierung, beendet er sich mit runner: 0 server connections configured, terminating und versucht es dank restart immer wieder. Nach der Registrierung meldet er declared successfully.
Ein wichtiger Punkt zur Sicherheit: Der Runner bekommt über den Docker-Socket vollen Zugriff auf Docker. In der erzeugten config.yml steht docker_host: "-", der Socket wird also nicht an die Jobs weitergereicht. Reicht man ihn weiter, erlaubt privileged oder großzügige valid_volumes, kann ein Workflow praktisch root auf dem VPS werden. Und in jedem Fall darf jeder Workflow die Website überschreiben. Lassen Sie deshalb nur eigene Repositorys und vertrauenswürdige Personen zu. Die Doku von Forgejo beschreibt als Alternative einen eigenen Docker-Daemon nur für den Runner.
Verifizieren: docker logs forgejo-runner zeigt with labels: [docker], ephemeral: false, declared successfully und [poller] launched.
Schritt 5: Drei Läufe aus dem Test
Unter Aktionen im Repository zeigt Forgejo jeden Lauf mit Status und Dauer:

Der erste Lauf scheiterte im Schritt Ausrollen:

Das Hugo-Image läuft als Benutzer 1000. Die meisten Dateien im Zielordner gehörten diesem Benutzer, weil Teil 5 mit dem Image gebaut und mit rsync -a samt Besitzer kopiert hat. Der Ordner .well-known mit security.txt stammt aus Teil 4 und gehörte root. Die Datei ließ sich nicht überschreiben und der Ordner nicht löschen. Die Lösung ist einmalig chown -R 1000:1000 auf den Zielordner und die Datei unter static/.well-known/ im Projekt.
Der zweite Lauf mit einem neuen Beitrag war erfolgreich, der Beitrag fehlte aber auf der Seite. Sein Datum lag ein paar Minuten in der Zukunft, und solche Beiträge baut Hugo ohne den Schalter --buildFuture nicht. Die Pipeline meldet das nicht als Fehler. Nach der Korrektur des Datums war die neue Seite 4,8 Sekunden nach dem git push im Netz, gemessen mit einer Schleife, die die Adresse abfragte, bis der Titel erschien.
Die Content-Security-Policy aus Teil 5 blieb dabei gültig: Die Hashes der Inline-Skripte in der neu gebauten Seite stimmten mit denen im Caddyfile überein. Ändern Sie Vorlagen mit Inline-Skripten, müssen Sie die Hashes wie in Teil 5 beschrieben neu berechnen.
Verifizieren: Nach einem Push erscheint der Lauf unter Aktionen, und die geänderte Seite ist wenige Sekunden später erreichbar.
Typische Fehler
| Meldung oder Beobachtung | Ursache und Lösung |
|---|---|
runner: 0 server connections configured, terminating | Runner nicht registriert. Registrierung aus Schritt 4 ausführen. |
failed to save runner config: open .runner: permission denied | Registrierung ohne Schreibrecht im Ordner. Mit --user 0:0 starten. |
find: /ziel/.well-known/security.txt: Permission denied | Dateien im Ziel gehören einem anderen Benutzer als dem Job. Rechte angleichen. |
| Lauf erfolgreich, neuer Beitrag fehlt | Datum in der Zukunft oder draft = true. Datum prüfen oder bewusst --buildFuture setzen. |
Häufige Fragen
Warum Forgejo und nicht Gitea?
Forgejo ist aus Gitea hervorgegangen und wird von einer gemeinnützigen Organisation getragen, auch Codeberg läuft damit. Die Bedienung ist sehr ähnlich, Actions sind wie bei Gitea eingebaut.
Kann der Runner auch auf einem anderen Rechner laufen?
Ja. Er verbindet sich nur ausgehend mit Forgejo. So lassen sich Jobs etwa im HomeLab ausführen, dann braucht der Job aber einen anderen Weg, um die fertige Seite auf den Server zu bringen, etwa rsync über SSH.
Wie mache ich das Ausrollen robuster?
Der Workflow löscht zuerst und kopiert dann. Dazwischen gibt es immer eine kurze Lücke, und scheitert das Kopieren, bleibt die Seite bis zum nächsten erfolgreichen Lauf unvollständig. Im ersten Lauf war genau das zu sehen: Alles außer .well-known war gelöscht und wurde danach neu kopiert. Sauberer ist es, in ein neues Verzeichnis zu bauen und Caddy erst danach darauf umzustellen, etwa über einen symbolischen Link. Das Linkziel muss dann auch im Caddy-Container eingebunden sein.
Fazit
Mit Forgejo und dem Runner auf dem VPS wird aus dem Bauen und Kopieren der Website ein git push, die neue Seite war im Test nach knapp 5 Sekunden online. Die Läufe aus dem Test zeigen, worauf es ankommt: Dateirechte im Zielordner müssen zum Job passen, und eine erfolgreiche Pipeline heißt nicht, dass alles auf der Seite ist. Den Docker-Socket für den Runner sollten Sie bewusst freigeben und nur eigenen Repositorys anvertrauen.
Weiterführende Anleitungen und Quellen
- Teil 5: Eigene Website mit Hugo
- Teil 6: Anubis vor der Website
- Teil 4: Caddy als Eingangstür
- Docker und Docker Compose auf Linux installieren
- Docker Compose absichern
- Forgejo: Actions für Administratoren
- Forgejo: Runner installieren
- Forgejo: Konfiguration
- Hugo: Entwürfe und Beiträge mit Datum in der Zukunft


