Add German translation for Forgejo Actions runner setup documentation
All checks were successful
Deploy / deploy (push) Successful in 1m18s
All checks were successful
Deploy / deploy (push) Successful in 1m18s
This commit is contained in:
parent
ada26f3173
commit
20167b8ca8
2 changed files with 252 additions and 1 deletions
237
src/content/posts/de/setting-up-forgejo-actions-runner.md
Normal file
237
src/content/posts/de/setting-up-forgejo-actions-runner.md
Normal file
|
|
@ -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 <REGISTRATION_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 <image> 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 <neuer-service>` 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.
|
||||||
|
|
@ -67,7 +67,9 @@ DEFAULT_ACTIONS_URL = https://code.forgejo.org
|
||||||
|
|
||||||
## Registering the runner
|
## 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
|
```bash
|
||||||
sudo -iu forgejo-runner /usr/local/bin/forgejo-runner register \
|
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.
|
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
|
## Making it not-need-Docker
|
||||||
|
|
||||||
On first boot, the runner refused to start with:
|
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.
|
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.
|
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 <new-service>`. 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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue