# 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 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 / hängende `git clone` ohne Fehlermeldung:** Symptom: Job steht minutenlang bei `cloning https://github.com/actions/checkout` bzw. `actions/setup-node` ohne weiteren Output; kein `server misbehaving`, kein Timeout. Gleichzeitig scheitern parallele Jobs im selben Run sporadisch sofort mit `lookup github.com on 127.0.0.11:53: server misbehaving`. Ursachen (mehrere verketten sich): 1. `127.0.0.11` ist Dockers embedded DNS-Resolver. Er forwarded an die Upstream-Resolver der Docker-Daemon-Config. Auf QNAP ist dieser Upstream häufig ein (langsamer/überlasteter) ISP-DNS oder fehlschlagender Provider-Resolver. 2. `--dns 8.8.8.8 --dns 1.1.1.1` in `container.options` injiziert die DNS-Server in `/etc/resolv.conf` **innerhalb** des Job-Containers — das behebt `server misbehaving`, aber nur wenn der Daemon die Option korrekt anwendet (`act_runner` ≥ 0.2.11). 3. Parallele Job-Starts erzeugen kurzzeitig 5–10 gleichzeitige DNS-Lookups → Upstream drosselt → hängende TCP-Connects ohne sauberes Fail. **Dauerhafter Fix:** ```yaml # config.yaml des act_runner 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 ``` **Alternative 1 — Host-Network:** ```yaml container: network: host # options: "" entfernen, --dns ist dann irrelevant ``` Nachteil: Jobs können auf Host-Ports zugreifen (Security-Impact bei Multi-Tenant). **Alternative 2 — Dockerd default-dns fixieren (macht auch andere Container robuster):** 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). Wirkt auf alle Container, auch ohne `--dns`-Option pro Job. **Alternative 3 — Pre-warm der Action-Repos (umgeht den Clone):** `act_runner` cached bereits geklonte Action-Repos unter `/data/cache/actions`. Einmal manuell anstoßen: ```bash docker exec -it act_runner sh -c ' mkdir -p /data/cache/actions/github.com/actions && cd /data/cache/actions/github.com/actions && git clone --depth 1 --branch v4 https://github.com/actions/checkout && git clone --depth 1 --branch v4.0.4 https://github.com/actions/setup-node && git clone --depth 1 --branch v4 https://github.com/actions/cache && git clone --depth 1 --branch v4 https://github.com/actions/upload-artifact ' ``` Danach laufen Jobs ohne DNS-Dependency zu github.com durch (solange der Cache nicht gelöscht wird). **Debug-Check:** ```bash # DNS aus Job-Container-Sicht verifizieren docker run --rm --network gitea_gitea --dns 8.8.8.8 alpine:3 \ sh -c 'apk add --no-cache bind-tools && dig +short github.com' ``` Liefert das sofort eine IP, ist DNS OK. Hängt es → DNS-Upstream-Problem (Alternative 2 oder 3 nötig). **`uses: actions/checkout@v4` schlägt fehl:** - `GITEA__actions__DEFAULT_ACTIONS_URL=https://github.com` gesetzt? - Gitea-Container braucht Outbound-Internetzugang zu github.com