da0d69c1c3
Adds dns: [8.8.8.8, 1.1.1.1] to the act_runner compose service itself. The existing container.options --dns setting only covers job sub- containers; act_runner's own process also clones actions/checkout and was still using 127.0.0.11. Troubleshooting section rewritten to explain both clone paths and give copy-paste fixes + verification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
373 lines
15 KiB
Markdown
373 lines
15 KiB
Markdown
# 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: `<Token aus Schritt 6>`
|
||
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/<stack>/…`). 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
|