From 20167b8ca8ed108f00932f625d1f54854ed8c018 Mon Sep 17 00:00:00 2001 From: Adrian Altner Date: Wed, 22 Apr 2026 00:56:17 +0200 Subject: [PATCH] Add German translation for Forgejo Actions runner setup documentation --- .../de/setting-up-forgejo-actions-runner.md | 237 ++++++++++++++++++ .../en/setting-up-forgejo-actions-runner.md | 16 +- 2 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/content/posts/de/setting-up-forgejo-actions-runner.md diff --git a/src/content/posts/de/setting-up-forgejo-actions-runner.md b/src/content/posts/de/setting-up-forgejo-actions-runner.md new file mode 100644 index 0000000..5767861 --- /dev/null +++ b/src/content/posts/de/setting-up-forgejo-actions-runner.md @@ -0,0 +1,237 @@ +--- +title: 'Forgejo Actions Runner für self-hosted CI/CD einrichten' +description: 'Wie ich manuelle SSH-Deploys durch eine Push-to-Deploy-Pipeline mit einem self-hosted Forgejo Actions Runner auf demselben VPS ersetzt habe.' +pubDate: 'Apr 22 2026' +heroImage: '../../../assets/blog-placeholder-2.jpg' +category: de/technik +tags: + - forgejo + - ci + - self-hosted + - devops + - podman +translationKey: forgejo-actions-runner +--- + +Nachdem ich meine Git-Repos von GitHub auf eine self-hosted Forgejo-Instanz umgezogen hatte, war der nächste logische Schritt, das Deployment von meinem Laptop wegzubekommen. Statt lokal `./scripts/deploy.sh` auszuführen und zu hoffen, dass nichts uncommittet ist, sollte `git push` den Build anstoßen und den Container automatisch ausrollen. + +Dieser Beitrag dokumentiert das komplette Setup: Forgejo Actions Runner auf demselben VPS installieren, an einen Workflow koppeln und Secrets sauber aus dem Repo halten. + +## Das Setup + +- **VPS**: eine Debian-Maschine, die sowohl Forgejo (rootless Podman-Container) als auch die Astro-Website (`/opt/websites/adrian-altner.de`, verwaltet über einen `podman-compose@` systemd-Service) hostet. +- **Forgejo**: v11 LTS, rootless, läuft unter einem eigenen `git` System-User. +- **Ziel**: bei jedem Push auf `main` das Production-Image neu bauen und den Service neu starten — alles auf derselben Maschine. + +## Warum ein eigener Runner-User + +Der Runner führt beliebigen Code aus Workflow-Dateien aus. Ihn als `git`-User laufen zu lassen (der Zugriff auf Forgejos Datenbank und jedes Repo hat) wäre keine gute Idee. Ich habe einen separaten System-User mit abgeschottetem Home-Verzeichnis angelegt: + +```bash +sudo useradd --system --create-home \ + --home-dir /var/lib/forgejo-runner \ + --shell /bin/bash forgejo-runner +``` + +Dieser User bekommt standardmäßig kein sudo — wir erteilen gezielt nur die Rechte, die der Deploy tatsächlich braucht. + +## Runner-Binary installieren + +Der Runner wird als einzelnes statisches Binary aus Forgejos eigener Registry verteilt. Ich hole mir das neueste Release programmatisch: + +```bash +LATEST=$(curl -s https://code.forgejo.org/api/v1/repos/forgejo/runner/releases \ + | grep -oE '"tag_name":"[^"]+"' | head -1 | cut -d'"' -f4) +VER="${LATEST#v}" + +cd /tmp +curl -L -o forgejo-runner \ + "https://code.forgejo.org/forgejo/runner/releases/download/${LATEST}/forgejo-runner-${VER}-linux-amd64" +chmod +x forgejo-runner +sudo mv forgejo-runner /usr/local/bin/ +``` + +Ein kurzes `forgejo-runner --version` bestätigt v12.9.0 — die aktuelle Major-Version, kompatibel mit Forgejo v10, v11 und allem darüber. + +## Actions in Forgejo aktivieren + +Actions sind bei einer Forgejo-Instanz standardmäßig aus. Die minimale Konfiguration kommt in die `app.ini` (bei mir im rootless-Container-Volume unter `/home/git/forgejo-data/custom/conf/app.ini`): + +```ini +[actions] +ENABLED = true +DEFAULT_ACTIONS_URL = https://code.forgejo.org +``` + +`DEFAULT_ACTIONS_URL` ist wichtig, weil der GitHub Actions Marketplace nicht direkt erreichbar ist — Forgejo pflegt eigene Mirrors der gängigen Actions wie `actions/checkout` unter `code.forgejo.org/actions/*`. Nach einem Container-Restart taucht das Verzeichnis `actions_artifacts` in den Logs auf. + +## Runner registrieren + +Runner können auf ein einzelnes Repo, einen User-Account oder die gesamte Instanz registriert werden. Ich habe mit einer Repo-Registrierung für meine Website angefangen und dann auf User-Scope umgestellt, damit derselbe Runner alle meine Repos bedienen kann, ohne sich neu registrieren zu müssen. + +Der Registrierungstoken kommt aus `Benutzer-Einstellungen → Actions → Runner → Neuen Runner erstellen`: + +```bash +sudo -iu forgejo-runner /usr/local/bin/forgejo-runner register \ + --no-interactive \ + --instance https://git.altner.cloud \ + --token \ + --name arcturus-runner \ + --labels "self-hosted:host" +``` + +Das Label `self-hosted:host` bedeutet: "Jobs mit Label `self-hosted` laufen direkt auf dem Host". Kein Container-Runtime für den Runner selbst nötig — Podman haben wir ja schon für die Anwendung. + +Umstellung eines bestehenden Runners von Repo- auf User-Scope: Service stoppen, alten Runner-Eintrag in der Forgejo-UI löschen, `/var/lib/forgejo-runner/.runner` lokal entfernen, neuen User-Level-Token holen, neu registrieren, Service starten. Gleiches Binary, anderer Scope. + +## Docker-Abhängigkeit abschalten + +Beim ersten Start hat sich der Runner geweigert zu laufen: + +``` +Error: daemon Docker Engine socket not found and docker_host config was invalid +``` + +Auch wenn man nur das Host-Label nutzt, prüft der Runner beim Start auf einen Docker-Socket. Da der Server nur rootless Podman hat, habe ich eine Config-Datei erzeugt und den Docker-Check explizit deaktiviert: + +```bash +sudo -iu forgejo-runner /usr/local/bin/forgejo-runner generate-config \ + > /tmp/runner-config.yaml +sudo mv /tmp/runner-config.yaml /var/lib/forgejo-runner/config.yaml +sudo chown forgejo-runner:forgejo-runner /var/lib/forgejo-runner/config.yaml + +sudo -iu forgejo-runner sed -i \ + -e 's|docker_host: .*|docker_host: "-"|' \ + -e 's| labels: \[\]| labels: ["self-hosted:host"]|' \ + /var/lib/forgejo-runner/config.yaml +``` + +## Systemd-Service + +```ini +[Unit] +Description=Forgejo Actions Runner +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=forgejo-runner +Group=forgejo-runner +WorkingDirectory=/var/lib/forgejo-runner +ExecStart=/usr/local/bin/forgejo-runner --config /var/lib/forgejo-runner/config.yaml daemon +Restart=on-failure +RestartSec=5s +NoNewPrivileges=false +ProtectSystem=full +ProtectHome=read-only +ReadWritePaths=/var/lib/forgejo-runner + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now forgejo-runner +``` + +## Nur die nötigen sudo-Rechte + +Der Deploy-Step muss ein Podman-Image bauen und den systemd-Service neu starten, der es ausführt. Beides braucht Root. Statt dem Runner-User breites sudo zu geben, habe ich eine eng gefasste Allowlist unter `/etc/sudoers.d/forgejo-runner-deploy` angelegt: + +``` +forgejo-runner ALL=(root) NOPASSWD: /usr/bin/podman build *, \ + /usr/bin/podman container prune *, \ + /usr/bin/podman image prune *, \ + /usr/bin/podman builder prune *, \ + /usr/bin/systemctl restart podman-compose@adrian-altner.de.service, \ + /usr/bin/rsync * +``` + +`visudo -cf` prüft die Syntax, bevor man sich versehentlich komplett aus sudo aussperrt. + +## Der Workflow + +Workflows liegen unter `.forgejo/workflows/*.yml`. Der Deploy-Flow macht dasselbe wie mein altes Shell-Skript, nur ohne SSH: + +```yaml +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: self-hosted + env: + DEPLOY_DIR: /opt/websites/adrian-altner.de + steps: + - uses: actions/checkout@v4 + + - name: Sync to deploy directory + run: | + sudo rsync -a --delete \ + --exclude='.env' \ + --exclude='.env.production' \ + --exclude='.git/' \ + --exclude='node_modules/' \ + ./ "${DEPLOY_DIR}/" + + - name: Build image + run: | + cd "${DEPLOY_DIR}" + sudo podman build \ + --build-arg WEBMENTION_TOKEN="${{ secrets.WEBMENTION_TOKEN }}" \ + -t localhost/adrian-altner.de:latest . + + - name: Restart service + run: sudo systemctl restart podman-compose@adrian-altner.de.service + + - name: Prune + run: | + sudo podman container prune -f 2>/dev/null || true + sudo podman image prune --external -f 2>/dev/null || true + sudo podman image prune -f 2>/dev/null || true + sudo podman builder prune -af 2>/dev/null || true +``` + +## Secrets bleiben in Forgejo + +Alles Sensible — in meinem Fall API-Tokens für webmention.io und webmention.app — liegt in `Settings → Actions → Secrets` und wird als `${{ secrets.NAME }}` in den Job injiziert. Forgejo speichert sie verschlüsselt, und Workflow-Logs maskieren die Werte automatisch. Die Tokens werden an genau zwei Stellen referenziert: in der CI-Workflow-Datei (committet) und im verschlüsselten Forgejo-Store (nie im Repo). + +Der Build-Time-Token wird als `ARG` in den Container gereicht, nur während des Build-Stages benutzt und ist im finalen Runtime-Image nicht enthalten — ein schnelles `podman run --rm env | grep -i webmention` bestätigt das. + +## Der eine Stolperstein: Node auf dem Host + +Der erste echte Workflow-Lauf ist sofort gestorben mit: + +``` +Cannot find: node in PATH +``` + +`actions/checkout@v4` ist eine JavaScript-basierte Action. Bei einem Runner mit Host-Label läuft sie direkt auf dem VPS und braucht einen Node-Interpreter im `PATH`. Ein `apt install` später war der Runner zufrieden: + +```bash +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +sudo apt-get install -y nodejs +sudo systemctl restart forgejo-runner +``` + +## Ergebnis + +Von einem kalten `git push origin main` bis zur komplett durchgelaufenen Pipeline — Checkout, rsync, Podman-Build, systemd-Restart, Prune, Webmention-Pings — vergehen etwa 1 Minute 15 Sekunden. Keine SSH-Keys zu rotieren, kein Laptop involviert, kein Mysterium über den Stand der Live-Version. + +Der Runner selbst belegt im Idle rund 5 MB RAM und pollt Forgejo alle zwei Sekunden auf neue Jobs. Der Ressourcen-Overhead ist vernachlässigbar verglichen mit dem Komfort von Push-to-Deploy auf Infrastruktur, die mir komplett gehört. + +## Runner für neue Projekte wiederverwenden + +Weil der Runner auf User-Scope registriert ist, reduziert sich das Anhängen von CI an ein neues Repo auf drei Schritte: + +1. Eine `.forgejo/workflows/deploy.yml` mit `runs-on: self-hosted` ins Repo packen. +2. Projekt-spezifische Secrets unter den Actions-Settings des Repos anlegen. +3. Falls das Projekt einen eigenen systemd-Service hat, `/etc/sudoers.d/forgejo-runner-deploy` um eine Zeile `systemctl restart ` erweitern. Sonst muss auf dem Server nichts geändert werden. + +Die einmaligen Infrastrukturkosten — User-Account, Binary, Config, systemd-Unit, Node-Runtime, sudoers — amortisieren sich über jedes weitere Projekt. diff --git a/src/content/posts/en/setting-up-forgejo-actions-runner.md b/src/content/posts/en/setting-up-forgejo-actions-runner.md index 912d6f6..a9edc7d 100644 --- a/src/content/posts/en/setting-up-forgejo-actions-runner.md +++ b/src/content/posts/en/setting-up-forgejo-actions-runner.md @@ -67,7 +67,9 @@ DEFAULT_ACTIONS_URL = https://code.forgejo.org ## Registering the runner -For a single repo, repo-scoped runners are the cleanest option. The registration token came from `Settings → Actions → Runners → Create new Runner` in the Forgejo UI: +Runners can be scoped to a single repo, to a user account, or to the whole instance. I started with a repo-scoped registration for my website, then moved it to user-scope so the same runner can serve every repo I own without re-registration. + +The registration token came from `User Settings → Actions → Runners → Create new Runner`: ```bash sudo -iu forgejo-runner /usr/local/bin/forgejo-runner register \ @@ -80,6 +82,8 @@ sudo -iu forgejo-runner /usr/local/bin/forgejo-runner register \ The label `self-hosted:host` means "jobs labelled `self-hosted` run directly on the host". No container runtime required for the runner itself — we already have Podman for the application. +To switch an existing runner from repo to user scope: stop the service, delete the old runner entry in the Forgejo UI, remove `/var/lib/forgejo-runner/.runner` locally, grab a new user-level token, re-register, start the service. Same binary, different scope. + ## Making it not-need-Docker On first boot, the runner refused to start with: @@ -221,3 +225,13 @@ sudo systemctl restart forgejo-runner From a cold `git push origin main`, the whole pipeline — checkout, rsync, Podman build, systemd restart, prune, Webmention pings — completes in about 1 minute 15 seconds. No SSH keys to rotate, no laptop involved, no mystery about which version of the code is live. The runner itself uses about 5 MB of RAM while idle, polling Forgejo every two seconds for new jobs. Resource overhead is negligible compared to the convenience of push-to-deploy on infrastructure I fully control. + +## Reusing the runner for new projects + +Because the runner is registered at user scope, adding CI to a new repository boils down to three steps: + +1. Drop a `.forgejo/workflows/deploy.yml` into the repo with `runs-on: self-hosted`. +2. Add any project-specific secrets under the repo's Actions settings. +3. If the project has its own systemd service, extend `/etc/sudoers.d/forgejo-runner-deploy` with a line allowing `systemctl restart `. Nothing else on the server needs to change. + +The one-time infrastructure cost — user account, binary, config, systemd unit, Node runtime, sudoers — gets amortised across every project from here on.