Document root cause (Docker embedded DNS 127.0.0.11 forwarding flakiness on QNAP), permanent fix (--dns-search .), and three alternatives (host network, dockerd daemon.json, pre-warm action cache). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 KiB
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)
-
Shared Folder
Containerexistieren lassen — falls nicht vorhanden, in File Station → New Shared Folder → NameContainer. -
Per SSH die Daten-Verzeichnisse anlegen mit den korrekten Ownerships für die Container-UIDs:
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
- 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
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)
- Container Station → Applications → Create
- Application Name:
gitea - 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) - Create + Start
- Browser:
https://gitea.hartmut-noerenberg.com→ Admin-User anlegen, Repos/Orgs einrichten
Phase 2: Runner hinzufügen
- In Gitea als Admin: Site Administration → Actions → Runners → Create new Runner → Token kopieren
- 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>
- Key:
- Stack neu deployen
- Logs prüfen:
docker logs -f gitea-act-runner # Erwartet: "Runner registered successfully" + "Listening for tasks" - In Gitea: Site Administration → Actions → Runners →
qnap-runner-1mit StatusIdle
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/:
# 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_URLmuss im internen Docker-Netz erreichbar sein (http://gitea:3000), nicht über Nginx-Proxy- Fehler
open /data/.runner: no such file or directory→ der customentrypointüberschreibt das Standard-Auto-Register-Skript des Images. Lösung: expliziteract_runner register-Aufruf vordaemon(siehe oben im Entrypoint-Block) - Fehler
instance address is emptytrotz gesetzter Env-Vars → Docker Compose interpoliert$VARim YAML bevor der Container startet. Im Entrypoint-Skript müssen Variablen als$$VARgeschrieben werden, damit ein literales$an den Container geht und von der Shell zur Laufzeit aufgelöst wird
Postgres startet nicht, "permission denied":
postgres-datagehört nicht UID 70 →sudo chown -R 70:70 /share/Container/gitea/postgres-data
Gitea startet nicht, "cannot create /data/...":
gitea-datagehört nicht UID 1000 →sudo chown -R 1000:1000 /share/Container/gitea/gitea-data
Jobs scheitern bei Docker-Operationen:
- Socket-Mount prüfen
container.networkin 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, dasdockermitbringt (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 imgitea_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):
127.0.0.11ist 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.--dns 8.8.8.8 --dns 1.1.1.1incontainer.optionsinjiziert die DNS-Server in/etc/resolv.confinnerhalb des Job-Containers — das behebtserver misbehaving, aber nur wenn der Daemon die Option korrekt anwendet (act_runner≥ 0.2.11).- Parallele Job-Starts erzeugen kurzzeitig 5–10 gleichzeitige DNS-Lookups → Upstream drosselt → hängende TCP-Connects ohne sauberes Fail.
Dauerhafter Fix:
# 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:
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:
{
"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:
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:
# 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.comgesetzt?- Gitea-Container braucht Outbound-Internetzugang zu github.com