# Preview-Runner: Livegang-Anleitung (R18) > **Umgesetzt am 2026-10-01 (L1), abweichend von diesem Vorschlag:** Der Runner laeuft wie das Portal > als `node:20-alpine` mit schreibgeschuetzt eingehaengtem `build/runner` (`/runner:ro`, > `command: apk add docker-cli git && node src/index.js`) statt eines gebauten Images; `deploy.sh` > macht bei Aenderungen in `runner/` und leerer Warteschlange nur `docker restart webklar-preview-runner` > (Status-Abfrage ueber die Nicht-Sandbox-Adresse, nicht 127.0.0.1). Git-Zugang: `GITEA_DEPLOY_TOKEN` > (nur lesen). Firewall: `/usr/local/sbin/preview-sandbox-firewall.sh` + systemd-Dienst > `preview-sandbox-firewall.service` (Bridge-Name fest `br-preview`), Einzelregeln statt der > fehlerhaften kombinierten Zeile unten. Massgeblich ist die echte Konfiguration auf dem Server. Stand: Vorschlag aus der Abschluss-Prüfung. Nichts davon ist im Repo umgesetzt, `deploy.sh` (`/home/webklar/apps/kundenbereich.webklar.com/deploy.sh`) liegt außerhalb des Repos und wird hier nur als Vorschlag beschrieben. Reihenfolge laut Spezifikation: L1 (Netz, Geheimnisse, Compose, Schema-Skript) vor L2 (Portal mit `PREVIEW_RUNNER=off`), dann L3 (`PREVIEW_RUNNER=on`). ## 1. Netzwerk `preview-sandbox` (VOR dem ersten Start anlegen) ```sh docker network create --subnet 172.30.0.0/24 preview-sandbox ``` Der Runner verweigert den Start, wenn er keine IPv4-Adresse außerhalb dieses Subnetzes hat. `PREVIEW_SANDBOX_SUBNET` muss exakt dem Subnetz entsprechen. Das Subnetz nicht in Compose anders definieren (Netz als `external: true` einbinden). Verzeichnisse auf dem Host anlegen (root-eigen genügt, der Runner setzt Besitzer 1000 selbst): ```sh mkdir -p /srv/webklar-preview-runner/work /srv/webklar-preview-runner/cache /var/www/previews ``` ## 2. Compose-Dienst (in `/home/webklar/apps/docker-compose.yml`) Der Image-Build-Kontext ist `kundenbereich.webklar.com/build/runner` (Dockerfile im Repo unter `runner/`). Hostpfade stehen im Container unter dem IDENTISCHEN Pfad (R6). ```yaml preview-runner: build: context: ./kundenbereich.webklar.com/build/runner image: webklar-preview-runner:latest container_name: webklar-preview-runner restart: unless-stopped init: true mem_limit: 256m # keine ports: - erreichbar nur im Docker-Netz webklar-network (Traefik) und vom Portal environment: - PREVIEW_RUNNER_TOKEN=${PREVIEW_RUNNER_TOKEN} - GITEA_URL=https://git.webklar.com - GITEA_TOKEN=${GITEA_SYSTEM_TOKEN} # lesender Token, der die WEBklar-Repos sehen darf - PREVIEW_BASE_HOST=project.webklar.com - PREVIEWS_ROOT=/var/www/previews - RUNNER_WORK_ROOT=/srv/webklar-preview-runner/work - RUNNER_CACHE_ROOT=/srv/webklar-preview-runner/cache - PREVIEW_SANDBOX_SUBNET=172.30.0.0/24 volumes: - /var/run/docker.sock:/var/run/docker.sock - /var/www/previews:/var/www/previews - /srv/webklar-preview-runner:/srv/webklar-preview-runner networks: - webklar-network - preview-sandbox labels: - traefik.enable=true - traefik.docker.network=webklar-network - traefik.http.services.preview-runner.loadbalancer.server.port=8081 # nur Dienst, kein Router: die Router erzeugt previewDeploy.syncPreviewTls (preview-runner@docker) networks: preview-sandbox: external: true ``` (Den `networks:`-Block in die bestehende Datei einfügen, nicht doppelt anlegen.) Keine Appwrite-, SMTP- oder Stripe-Variablen und kein `PREVIEW_ENV_KEY` im Runner. Erster Start: `docker compose build preview-runner && docker compose up -d preview-runner`. ## 3. Portal-Umgebung (Dienst `kundenbereich-server`, Variablen einzeln auflisten) ```yaml - PREVIEW_RUNNER=${PREVIEW_RUNNER:-off} # L2: off, L3: on - PREVIEW_RUNNER_URL=http://webklar-preview-runner:8080 - PREVIEW_RUNNER_TOKEN=${PREVIEW_RUNNER_TOKEN} - PREVIEW_ENV_KEY=${PREVIEW_ENV_KEY} # 32 Byte, Base64 (openssl rand -base64 32) ``` Das Verzeichnis `upload-data/preview-logs` muss persistent sein (liegt in `./build/`). ## 4. Firewall für die Sandbox-Bridge (Livegang-Schritt, kein Code) Zuerst den Bridge-Namen ermitteln: `BR=br-$(docker network inspect preview-sandbox -f '{{.Id}}' | cut -c1-12)`. ```sh # Sandbox -> Host (INPUT): alles verwerfen, bereits bestehende Antworten erlauben iptables -I INPUT -i $BR -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -i $BR -j DROP # Sandbox -> andere Ziele (DOCKER-USER, wird vor Docker-Regeln ausgewertet) iptables -I DOCKER-USER -s 172.30.0.0/24 -d 169.254.169.254 -j DROP # Hetzner-Metadaten iptables -I DOCKER-USER -s 172.30.0.0/24 -d 10.0.0.0/8 -j DROP iptables -I DOCKER-USER -s 172.30.0.0/24 -d 192.168.0.0/16 -j DROP # 172.16.0.0/12 (andere Docker-Netze) sperren, ausser dem eigenen /24; Runner-Zugriff wird nicht gebraucht iptables -I DOCKER-USER -s 172.30.0.0/24 -d 172.30.0.0/24 -j ACCEPT # Dev-zu-Dev im eigenen Netz (R13, bekannt) iptables -I DOCKER-USER -s 172.30.0.0/24 ! -d 172.30.0.0/24 -d 172.16.0.0/12 -j DROP ``` Reihenfolge beachten: `-I` setzt vorne ein, die ACCEPT-Regel für das eigene /24 muss vor dem 172.16/12-DROP stehen (also zuletzt mit `-I` eingefügt, wie oben die Zeilen in umgekehrter Wirkungsreihenfolge; nach dem Einrichten mit `iptables -S DOCKER-USER` prüfen). DNS läuft über den Docker-Resolver (127.0.0.11) und bleibt erlaubt, Ausgang ins Internet (npm, Registries) ebenfalls. Regeln persistent machen (z. B. `iptables-persistent`), sonst gehen sie beim Neustart verloren. ## 5. deploy.sh-Erweiterung (Vorschlag, bewusst NICHT eingespielt) Voraussetzung: der Build-Container (`kundenbereich-build`) bekommt zusätzlich `/home/webklar/apps/docker-compose.yml:/compose/docker-compose.yml:ro` eingehängt und hat `docker-cli` samt Compose-Plugin. Der Runner wird nur neu gebaut und neu erstellt, wenn sich `runner/` geändert hat UND `GET /status` eine leere Warteschlange meldet (sonst würden laufende Builds/Dev-Server beendet); andernfalls überspringen und protokollieren, der nächste Deploy versucht es erneut. Der Runner wird VOR dem Portal neu gestartet, damit dessen Dev-Registrierung gleich klappt (der Abgleich im Portal holt Verpasstes ohnehin alle 2 Minuten nach). Einfügen direkt vor dem Neustart des Portals; vor dem `git reset --hard` den alten Stand merken (`OLD_HEAD=$(git rev-parse HEAD 2>/dev/null || true)`): ```sh # --- Preview-Runner (nur wenn runner/ geaendert) --- if [ -S /var/run/docker.sock ] && command -v docker >/dev/null 2>&1 && [ -f /compose/docker-compose.yml ]; then NEW_HEAD=$(git rev-parse HEAD) if [ -z "${OLD_HEAD}" ] || ! git diff --quiet "${OLD_HEAD}" "${NEW_HEAD}" -- runner 2>/dev/null; then QUEUE=$(docker exec webklar-preview-runner sh -c \ 'wget -qO- --header "Authorization: Bearer $PREVIEW_RUNNER_TOKEN" http://127.0.0.1:8080/status' 2>/dev/null \ | sed -n 's/.*"queue":\([0-9]*\).*/\1/p') if [ -z "${QUEUE}" ] || [ "${QUEUE}" = "0" ]; then echo "Preview-Runner: Image neu bauen und Container neu erstellen..." docker build -t webklar-preview-runner:latest /app/runner \ && docker compose -f /compose/docker-compose.yml --project-directory /home/webklar/apps \ up -d --no-build --no-deps preview-runner \ || echo "Preview-Runner: Aktualisierung fehlgeschlagen (Portal-Deploy laeuft weiter)" else echo "Preview-Runner: Warteschlange nicht leer (${QUEUE}) - Aktualisierung uebersprungen, naechster Deploy versucht es erneut" fi fi fi ``` Hinweise: Ist der Runner noch nicht angelegt (`docker exec` schlägt fehl, `QUEUE` leer), wird gebaut und erstellt, das ist beim ersten Mal gewollt. `wget` ist im Alpine-Image enthalten. Die Status-Abfrage bitte vor dem Einspielen einmal von Hand gegenprüfen. ## 6. Rückfall `PREVIEW_RUNNER=off` und Portal neu starten: Der bisherige Weg läuft unverändert. Dokumente behalten `previewMode`/`previewState`, die Dev-Router verschwinden beim nächsten TLS-Sync.