Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Server & Netzwerk 05.11.2026 · 9 min Lesezeit

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

Symbolbild: Server mit leuchtendem Kabel zur Weltkarte, Titel Website per git push ausrollen

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:

Aktionen in Forgejo mit drei Läufen, einer davon fehlgeschlagen
Aktionen im Repository: Der letzte Lauf war erfolgreich (1), der erste ist fehlgeschlagen (2). Jeder Lauf dauerte laut Anzeige rund 2 Sekunden.

Der erste Lauf scheiterte im Schritt Ausrollen:

Fehlgeschlagener Schritt Ausrollen mit Permission denied für security.txt
Fehlgeschlagener Schritt Ausrollen (1) mit den Meldungen (2): Ordner .well-known und security.txt gehörten root, der Hugo-Container läuft als Benutzer 1000.

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 BeobachtungUrsache und Lösung
runner: 0 server connections configured, terminatingRunner nicht registriert. Registrierung aus Schritt 4 ausführen.
failed to save runner config: open .runner: permission deniedRegistrierung ohne Schreibrecht im Ordner. Mit --user 0:0 starten.
find: /ziel/.well-known/security.txt: Permission deniedDateien im Ziel gehören einem anderen Benutzer als dem Job. Rechte angleichen.
Lauf erfolgreich, neuer Beitrag fehltDatum 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

Server im NetzForgejoForgejo ActionsCI/CDHugoGitDocker