# Gitea + Act Runner — Single-File Compose (QNAP Container Station) Eine einzige `docker-compose.yml` zum Direkt-Einfügen in Container Station. Persistente Daten liegen unter `/share/Container/gitea/` (stabiler Pfad, überlebt Stack-Recreate). Runner-Config wird beim Start inline generiert. ## Vorbereitung auf der QNAP (einmalig) 1. **Shared Folder `Container` existieren lassen** — falls nicht vorhanden, in File Station → New Shared Folder → Name `Container`. 2. **Per SSH die Daten-Verzeichnisse anlegen** mit den korrekten Ownerships für die Container-UIDs: ```bash sudo mkdir -p /share/Container/gitea/gitea-data \ /share/Container/gitea/postgres-data \ /share/Container/gitea/act-runner-data # Postgres-Container läuft als UID 70 sudo chown -R 70:70 /share/Container/gitea/postgres-data # Gitea läuft intern als git user (UID 1000) sudo chown -R 1000:1000 /share/Container/gitea/gitea-data /share/Container/gitea/act-runner-data ``` 3. **Registrierungs-Token-Ablauf (wie vorher):** Erst Gitea + DB deployen (act_runner-Block auskommentiert oder mit leerem Token). Dann im Web-UI Runner-Token erzeugen → als Env-Var im Stack hinterlegen → act_runner deployen. ## docker-compose.yml ```yaml version: "3" services: gitea: image: gitea/gitea:latest container_name: gitea environment: - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=db:5432 - GITEA__database__NAME=gitea - GITEA__database__USER=gitea - GITEA__database__PASSWD=UGi2VZA7SgYGov - GITEA__server__DOMAIN=gitea.hartmut-noerenberg.com - GITEA__server__SSH_DOMAIN=gitea.hartmut.noerenberg.com - GITEA__server__ROOT_URL=https://gitea.hartmut-noerenberg.com/ - GITEA__server__SSH_PORT=2222 - GITEA__server__HTTP_PORT=3000 # Gitea Actions aktivieren - GITEA__actions__ENABLED=true - GITEA__actions__DEFAULT_ACTIONS_URL=https://github.com - GITEA__actions__LOG_COMPRESSION=zstd restart: unless-stopped networks: - gitea - nginxproxy_nginxintern volumes: - /share/Container/gitea/gitea-data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro ports: - "3000:3000" - "2222:22" depends_on: - db db: image: postgres:16-alpine container_name: gitea-db restart: unless-stopped # Geben wir Postgres großzügig Zeit für sauberen Shutdown beim Stop/Replace. # Ohne diesen Grace muss beim nächsten Start Crash-Recovery laufen # (fsync über alle Files) — auf HDD-backed QNAP-Storage dauert das # schnell 5-10 Minuten und blockt Gitea beim Start. # 120s ist bewusst großzügig: bei viel WAL-Write (CI-Läufe mit Artefakten) # kann auch ein sauberer Shutdown 30-60s dauern. stop_grace_period: 120s environment: - POSTGRES_USER=gitea - POSTGRES_PASSWORD=UGi2VZA7SgYGov - POSTGRES_DB=gitea networks: - gitea volumes: - /share/Container/gitea/postgres-data:/var/lib/postgresql/data act_runner: image: gitea/act_runner:latest container_name: gitea-act-runner restart: unless-stopped depends_on: - gitea # WICHTIG: dns am act_runner-Container selbst setzen, NICHT nur in # container.options (das wirkt nur auf Job-Sub-Container). act_runner # clont `actions/checkout` etc. aus seinem eigenen Prozess heraus nach # /data/workflows — dafür zählt seine eigene /etc/resolv.conf. Ohne # diese Zeilen steht dort 127.0.0.11 (Dockers embedded DNS im # gitea_gitea-Netz), was auf QNAP unzuverlässig forwarded ("server # misbehaving") und jedes action-Clone killt. dns: - 8.8.8.8 - 1.1.1.1 dns_search: [] environment: - GITEA_INSTANCE_URL=http://gitea:3000 - GITEA_RUNNER_REGISTRATION_TOKEN=218iFl8s3a6uJxntyoobzu24pQJBGGVIWmdtJbXh - GITEA_RUNNER_NAME=qnap-runner-1 # catthehacker/ubuntu:act-latest statt node:20-bookworm, weil sonst # `docker`-CLI in Job-Containern fehlt und Workflows wie release-image.yml # (docker login/buildx) mit "docker: command not found" scheitern. - GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:act-latest,ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04 - CONFIG_FILE=/config.yaml networks: - gitea volumes: - /share/Container/gitea/act-runner-data:/data - /var/run/docker.sock:/var/run/docker.sock entrypoint: - /bin/sh - -c - | cat > /config.yaml <<'EOF' log: level: info runner: file: /data/.runner capacity: 4 timeout: 3h insecure: false fetch_timeout: 5s fetch_interval: 2s cache: enabled: true dir: /data/cache container: network: gitea_gitea privileged: false # --dns: Docker's embedded DNS auf 127.0.0.11 im gitea_gitea-Netz # forwarded auf QNAP leider unzuverlässig ("server misbehaving"), # was jedes `git clone https://github.com/actions/checkout` killt. # Expliziter Upstream-DNS im Job-Container umgeht das Problem. options: "--dns 8.8.8.8 --dns 1.1.1.1" workdir_parent: /workspace valid_volumes: - /var/run/docker.sock host: workdir_parent: /data/workflows EOF if [ ! -f /data/.runner ]; then act_runner register --no-interactive \ --instance "$$GITEA_INSTANCE_URL" \ --token "$$GITEA_RUNNER_REGISTRATION_TOKEN" \ --name "$$GITEA_RUNNER_NAME" \ --labels "$$GITEA_RUNNER_LABELS" \ --config /config.yaml fi exec act_runner daemon --config /config.yaml networks: gitea: external: false nginxproxy_nginxintern: external: true ``` ## Deploy-Ablauf in Container Station **Phase 1: Gitea + DB (ohne Runner)** 1. Container Station → **Applications → Create** 2. Application Name: `gitea` 3. Obige YAML einfügen, **aber den gesamten `act_runner`-Service-Block temporär auskommentieren** (mit `#` vor jeder Zeile, oder einfach löschen und später wieder einfügen) 4. Create + Start 5. Browser: `https://gitea.hartmut-noerenberg.com` → Admin-User anlegen, Repos/Orgs einrichten **Phase 2: Runner hinzufügen** 6. In Gitea als Admin: **Site Administration → Actions → Runners → Create new Runner** → Token kopieren 7. In Container Station: Stack `gitea` → **Edit** → `act_runner`-Block wieder einfügen → unter **Environment Variables** hinzufügen: - Key: `GITEA_RUNNER_REGISTRATION_TOKEN` - Value: `` 8. Stack neu deployen 9. Logs prüfen: ```bash docker logs -f gitea-act-runner # Erwartet: "Runner registered successfully" + "Listening for tasks" ``` 10. In Gitea: **Site Administration → Actions → Runners** → `qnap-runner-1` mit Status `Idle` ## Warum absolute Pfade Relative Pfade (`./gitea-data`) werden von Container Station relativ zum internen Application-Directory aufgelöst (`/share/CACHEDEV1_DATA/Container/container-station-data/application//…`). Beim Ersetzen oder Neuanlegen eines Stacks kann Container Station dieses Directory neu erzeugen oder löschen — das führt zum Datenverlust wie beim letzten Versuch. Absolute Pfade unter `/share/Container/gitea/` sind **außerhalb** der Container-Station-Verwaltung. Stack kann beliebig gelöscht, umbenannt, migriert werden — die Daten bleiben, weil Container Station sie nicht als "seine" Volumes betrachtet. ## Repo-Secrets für CI/CD Im capakraken-Repo → **Settings → Actions → Secrets** eintragen: | Secret | Zweck | | ----------------------- | -------------------------------------- | | `STAGING_SSH_KEY` | Private SSH-Key für Deploy | | `STAGING_SSH_HOST` | Staging-Hostname | | `STAGING_SSH_PORT` | SSH-Port (meist `22`) | | `STAGING_SSH_USER` | Deploy-User | | `STAGING_DEPLOY_PATH` | Deploy-Verzeichnis auf Staging-Host | | `STAGING_APP_HOST_PORT` | App-Port auf dem Host | | `STAGING_GHCR_USERNAME` | Registry-User | | `STAGING_GHCR_TOKEN` | Registry-Token mit Package-Write-Scope | | `PROD_*` | Analog für Produktion | ## Backup-Empfehlung (nach diesem Vorfall umso wichtiger) Tägliches Backup per Cron oder QNAP-Snapshot auf `/share/Container/gitea/`: ```bash # Beispiel — in QNAP Cron oder Systemd-Timer sudo tar -czf /share/Backups/gitea-$(date +%Y%m%d).tar.gz /share/Container/gitea/ # Retention: letzte 14 Tage behalten find /share/Backups/ -name 'gitea-*.tar.gz' -mtime +14 -delete ``` Zusätzlich: QNAP **Storage & Snapshots** → Volume-Snapshots für `/share/Container/` aktivieren. ## Sicherheits-Notiz `/var/run/docker.sock` ist gemountet, damit `release-image.yml` Images bauen kann. Das gibt jedem Workflow-Job vollen Zugriff auf den QNAP-Docker-Daemon — akzeptabel für Single-Tenant mit eigenen Repos. Für untrusted Repos stattdessen docker-in-docker Sidecar (auf Anfrage). ## Troubleshooting **Runner registriert sich nicht:** - Token abgelaufen → neuen in Gitea-UI erzeugen → Env-Var aktualisieren → `act_runner`-Container neu starten - `GITEA_INSTANCE_URL` muss im internen Docker-Netz erreichbar sein (`http://gitea:3000`), nicht über Nginx-Proxy - Fehler `open /data/.runner: no such file or directory` → der custom `entrypoint` überschreibt das Standard-Auto-Register-Skript des Images. Lösung: expliziter `act_runner register`-Aufruf vor `daemon` (siehe oben im Entrypoint-Block) - Fehler `instance address is empty` trotz gesetzter Env-Vars → Docker Compose interpoliert `$VAR` im YAML **bevor** der Container startet. Im Entrypoint-Skript müssen Variablen als `$$VAR` geschrieben werden, damit ein literales `$` an den Container geht und von der Shell zur Laufzeit aufgelöst wird **Postgres startet nicht, "permission denied":** - `postgres-data` gehört nicht UID 70 → `sudo chown -R 70:70 /share/Container/gitea/postgres-data` **Gitea startet nicht, "cannot create /data/...":** - `gitea-data` gehört nicht UID 1000 → `sudo chown -R 1000:1000 /share/Container/gitea/gitea-data` **Jobs scheitern bei Docker-Operationen:** - Socket-Mount prüfen - `container.network` in der inline-generierten Runner-Config muss zum echten Docker-Netzwerknamen passen (`docker network ls`) - Fehler `docker: command not found` → Job-Container hat kein Docker-CLI. Runner-Label muss ein Image verwenden, das `docker` mitbringt (z.B. `catthehacker/ubuntu:act-latest`). `node:*`-Images reichen nicht, weil dort nur Node installiert ist - Fehler `Get "https://github.com/..." ... dial tcp: lookup github.com on 127.0.0.11:53: server misbehaving` → Docker-interner DNS im `gitea_gitea`-Netz forwarded unzuverlässig. Fix: `container.options: "--dns 8.8.8.8 --dns 1.1.1.1"` in der Runner-Config setzen, damit Job-Container externen DNS direkt nutzen **DNS-Timeouts / `server misbehaving` beim `actions/checkout`-Clone — komplette Lösung:** Symptom: Jobs scheitern mit ```text Get "https://github.com/actions/checkout/info/refs?service=git-upload-pack": dial tcp: lookup github.com on 127.0.0.11:53: server misbehaving ``` oder hängen minutenlang bei `cloning https://github.com/actions/checkout`. ### Die Fallstricke (wichtig zum Verstehen, warum es ZWEI Fixes braucht) `act_runner` führt beim Start eines Jobs **zwei unabhängige** Clone-Operationen aus: 1. **Im act_runner-Prozess selbst** (vor Job-Container-Start): clont Actions nach `/data/workflows/...`, benutzt seine eigene `/etc/resolv.conf`. 2. **Im Job-Sub-Container** (während Job-Run): benutzt seine eigene `/etc/resolv.conf`. **Beides** zeigt per Default auf `127.0.0.11` (Dockers embedded DNS im `gitea_gitea`-Netz), das wiederum an den QNAP-Host-Upstream forwarded. Dieser Upstream ist auf QNAP oft unzuverlässig → `server misbehaving`. Der `container.options: "--dns ..."`-Eintrag in der Runner-`config.yaml` betrifft **nur Fall 2** (Job-Sub-Container). Fall 1 (act_runner selbst) braucht einen separaten Fix am Compose-Service. ### Copy-Paste-Lösung (beide Ebenen gleichzeitig) **1) Am `act_runner`-Service in der compose — setzt seine eigene `/etc/resolv.conf` auf Upstream-DNS** (in der obigen compose.yml schon eingebaut): ```yaml act_runner: image: gitea/act_runner:latest # ... restliche Config ... dns: - 8.8.8.8 - 1.1.1.1 dns_search: [] ``` **2) In der inline-generierten `/config.yaml` — setzt Upstream-DNS in jedem Job-Sub-Container** (ebenfalls schon eingebaut): ```yaml container: network: gitea_gitea options: "--dns 8.8.8.8 --dns 1.1.1.1 --dns-search ." # `--dns-search .` entfernt jede geerbte Search-Domain → keine verirrten NXDOMAIN-Retries ``` Nach dem Ändern: Stack neu deployen, damit der act_runner-Container mit der neuen DNS-Config startet. ### Verifikation nach dem Deploy ```bash # 1. DNS aus Sicht des act_runner-Containers selbst — muss sofort eine IP liefern docker exec gitea-act-runner sh -c 'cat /etc/resolv.conf && nslookup github.com' # Erwartet: nameserver 8.8.8.8 / 1.1.1.1, nicht 127.0.0.11 # Name: github.com, Address: 140.82.x.x # 2. DNS aus Sicht eines Job-Sub-Containers docker run --rm --network gitea_gitea --dns 8.8.8.8 alpine:3 \ sh -c 'apk add --no-cache bind-tools >/dev/null && dig +short github.com' # Erwartet: sofortige IP-Antwort ``` Hängen oder `server misbehaving` → siehe Alternativen unten. ### Alternative A — Docker-Daemon global fixen (robuster, wirkt auf ALLE Container) In `/etc/docker/daemon.json` auf dem QNAP: ```json { "dns": ["8.8.8.8", "1.1.1.1", "9.9.9.9"], "dns-opts": ["ndots:1", "timeout:2", "attempts:3"] } ``` Dann Docker-Daemon restart (Container Station → Advanced → Restart Docker). Macht die compose-seitigen `dns:`-Einträge überflüssig, hilft aber auch jedem anderen Container. ### Alternative B — Pre-warm der Action-Repos (umgeht den Clone komplett) `act_runner` cached bereits geklonte Action-Repos unter `/data/cache/actions`. Einmal manuell anstoßen: ```bash docker exec gitea-act-runner sh -c ' mkdir -p /data/cache/actions/github.com/actions && cd /data/cache/actions/github.com/actions && for repo in checkout setup-node cache upload-artifact download-artifact; do [ -d "$repo" ] || git clone --depth 1 "https://github.com/actions/$repo" done ' ``` Danach laufen Jobs ohne DNS-Dependency zu github.com durch, solange der Cache nicht gelöscht wird. ### Alternative C — Host-Network für Job-Container ```yaml container: network: host # options ohne --dns ``` Nachteil: Jobs sehen Host-Ports (Security-Impact bei Multi-Tenant). Nur als Notnagel. ### Parallele-Job-Drosselung Parallele Job-Starts erzeugen kurzzeitig 5–10 gleichzeitige DNS-Lookups; wenn dein Upstream-DNS drosselt, hängen Connects ohne sauberes Fail. Dann in der Runner-`config.yaml`: ```yaml runner: capacity: 2 # statt 4 — reduziert parallele Starts ``` **Debug-Snippet — wer resolved gerade was:** ```bash # Alle Container mit ihrer resolv.conf-Config for c in $(docker ps --format '{{.Names}}'); do echo "=== $c ==="; docker exec "$c" cat /etc/resolv.conf 2>/dev/null done ``` **`uses: actions/checkout@v4` schlägt fehl:** - `GITEA__actions__DEFAULT_ACTIONS_URL=https://github.com` gesetzt? - Gitea-Container braucht Outbound-Internetzugang zu github.com