Files
AirCardIOS/deploy/README.md
T
ClaudeandClaude Opus 5.5 144181f2c9 chore(deploy): nginx härten, deploy.sh mit Sperre, Zeitlimit und ohne Webhook-Geheimnis
nginx: server_tokens off, HSTS, exaktes /v1 liefert 404 (Regex-Block statt
^~ /v1/). deploy.sh: AIRCARD_WEBHOOK_TOKEN nicht an npm, Sperrverzeichnis
gegen parallele Läufe (verwaiste Sperren werden übernommen), timeout 900
für npm ci und Build. Neuer Test deploy/deploy.test.js gegen das echte Skript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:08:09 +00:00

6.9 KiB

Deployment aircard-ios.org

Die Seite wird auf dem Webklar-Server aus dem Gitea-Repo knso/AirCardIOS gebaut und von nginx ausgeliefert. Traefik stellt TLS und Weiterleitungen. Dieser Ordner enthält alles, was dafür im Repo liegt; die Dateien auf dem Server sind Kopien davon.

Bestandteile

Datei im Repo Ziel auf dem Server (/home/webklar/apps/) Aufgabe
deploy/deploy.sh aircard-ios.org/deploy.sh (ausführbar) klont/aktualisiert das Repo, baut web/ und kopiert dist/ nach out/
deploy/webhook-server.js aircard-ios.org/webhook-server.js nimmt den Gitea-Webhook an, startet deploy.sh nacheinander, pollt alle 15 Minuten
deploy/nginx/aircard-ios.org.conf aircard-ios.org/nginx/aircard-ios.org.conf nginx-Konfiguration der Seite
deploy/traefik/aircard.yml traefik/dynamic/aircard.yml Route /v1 mit Header X-Appwrite-Project: 6ac2130a00289343d6d5 an Appwrite
deploy/compose-snippet.yml docker-compose.yml (nach dem eship-Block) Dienste aircard-build und aircard

Weitere Ordner unter aircard-ios.org/ (nicht im Repo): build/ (Klon des Repos, Arbeitsordner des Build-Containers), out/ (gebaute Seite, von nginx ausgeliefert), dev/ (Arbeitskopie für die Entwicklung, nicht gemountet).

Einrichten (einmalig)

APP=/home/webklar/apps
SRC=$APP/aircard-ios.org/dev/deploy

mkdir -p $APP/aircard-ios.org/build $APP/aircard-ios.org/out $APP/aircard-ios.org/nginx
cp $SRC/deploy.sh $APP/aircard-ios.org/deploy.sh && chmod +x $APP/aircard-ios.org/deploy.sh
cp $SRC/webhook-server.js $APP/aircard-ios.org/webhook-server.js
cp $SRC/nginx/aircard-ios.org.conf $APP/aircard-ios.org/nginx/aircard-ios.org.conf

# Compose: Backup, dann den Inhalt von compose-snippet.yml unter `services:` direkt nach dem eship-Block einfügen
cp $APP/docker-compose.yml $APP/docker-compose.yml.bak-2026-10-04-aircard
cd $APP && docker compose config -q; echo $?    # 0; einzige erlaubte Ausgabe: Warnung "attribute `version` is obsolete"

# Traefik-Route atomar ablegen (Datei-Provider lädt sie ohne Neustart)
cp $SRC/traefik/aircard.yml $APP/traefik/dynamic/aircard.yml.tmp && mv $APP/traefik/dynamic/aircard.yml.tmp $APP/traefik/dynamic/aircard.yml

Die .env neben der docker-compose.yml muss GITEA_DEPLOY_TOKEN (Lesezugriff auf knso/AirCardIOS) und GITEA_DEPLOY_WEBHOOK_SECRET enthalten. Beide Werte gehören nie in Logs, Chats oder Commits.

Starten

cd /home/webklar/apps && docker compose up -d aircard-build aircard

Nur diese beiden Dienste starten, nie ein nacktes docker compose up -d (das würde nicht laufende Dienste wie eship-build wieder anlegen). Der Build-Container installiert beim Start git, startet den Webhook-Server und baut sofort einmal (initial). Den Fortschritt zeigt docker logs -f webklar-aircard-build; am Ende steht Deployment abgeschlossen (oder eine Zeile mit ERROR:).

Ohne AIRCARD_WEBHOOK_TOKEN (also ohne GITEA_DEPLOY_WEBHOOK_SECRET in der .env) beendet sich der Webhook-Server mit Exit-Code 1, statt Anfragen ungeprüft durchzulassen.

Manuell deployen

docker exec webklar-aircard-build sh /deploy.sh

Ohne DEPLOY_TRIGGER baut das Skript immer neu, auch wenn sich der Commit nicht geändert hat.

Es läuft immer nur ein Deployment: Ist schon eines aktiv (Sperrverzeichnis build/.git/aircard-deploy.lock), schreibt das Skript „Deployment läuft bereits“ und endet mit Exit-Code 0. Eine verwaiste Sperre (Prozess weg oder älter als 60 Minuten) übernimmt der nächste Lauf selbst. npm ci und der Build brechen nach 15 Minuten ab (timeout 900).

Gitea-Webhook

Repo knso/AirCardIOS → Einstellungen → Webhooks → Gitea:

Feld Wert
Ziel-URL https://aircard-ios.org/webhook/deploy
HTTP-Methode POST
Content-Type application/json
Secret Wert von GITEA_DEPLOY_WEBHOOK_SECRET aus /home/webklar/apps/.env (volle Länge, nur dort nachsehen)
Auslöser nur Push-Ereignisse
Branch-Filter main
Aktiv ja

„Test delivery“ muss 202 ergeben. 401 heißt: Secret stimmt nicht. 200 mit {"ignored":true} heißt: Push auf einen anderen Branch als main.

Verhalten

  • Webhook (POST /webhook/deploy): prüft die Signatur im Header x-gitea-signature (HMAC-SHA256 über den Rohbody). 401 bei falscher oder fehlender Signatur, 413 bei Body über 1 MB, 200 bei Push auf einen anderen Branch, 202 bei Push auf main; der Build läuft danach im Hintergrund. Alles andere antwortet 404.
  • Warteschlange: Builds laufen nacheinander, höchstens einer läuft und einer wartet. Weitere Pushes während ein Lauf wartet, werden mit 202 beantwortet und fügen keinen Lauf hinzu (der wartende Lauf holt ohnehin den neuesten Stand).
  • Trigger: webhook und manuelle Aufrufe bauen immer. initial (Container-Start) und poll (alle 15 Minuten) bauen nur, wenn es einen neuen Commit gibt oder out/index.html fehlt.
  • Gemerkter Commit: build/.git/aircard-last-deployed-commit enthält den zuletzt versuchten Commit, geschrieben vor dem Build. Ein kaputter Build wird deshalb nicht alle 15 Minuten wiederholt; die alte Seite bleibt online, bis ein neuer Commit oder ein Webhook-/manueller Lauf kommt.
  • Fehler: Ein fehlgeschlagener Lauf wird mit [webhook] … deployment failed protokolliert und beendet den Server nicht. Fehlerzeilen des Skripts beginnen mit ERROR:.
  • Build-Variablen: VITE_APPWRITE_ENDPOINT und VITE_APPWRITE_PROJECT_ID kommen aus der Container-Umgebung (compose); AIRCARD_APPWRITE_PROJECT_ID wird aus /home/webklar/apps/.env gelesen. Vite liest sie direkt.
  • .env-Variablen: AIRCARD_APPWRITE_PROJECT_ID (Projekt-ID 6ac2130a00289343d6d5) und AIRCARD_APPWRITE_API_KEY (für das Setup-Skript) müssen in /home/webklar/apps/.env eingetragen sein.

Änderungen später

  • deploy.sh ändern: Datei auf dem Server mit cp überschreiben (nicht mv, sonst zeigt der Datei-Mount auf die alte Datei). Gilt beim nächsten Lauf.
  • webhook-server.js ändern: ebenso mit cp ersetzen, dann cd /home/webklar/apps && docker compose restart aircard-build.
  • nginx-Konfiguration ändern: mit cp ersetzen, dann docker exec webklar-aircard nginx -t && docker exec webklar-aircard nginx -s reload.
  • Traefik-Route ändern: Datei atomar ersetzen wie oben; Löschen der Datei entfernt die Route wieder.

www.aircard-ios.org

Solange dig +short www.aircard-ios.org @8.8.8.8 nichts liefert, gibt es nur die Hauptdomain. Zeigt der A-Record auf 91.99.156.85, werden die auskommentierten www-Labels am Dienst aircard in docker-compose.yml aktiviert (Backup vorher, dann docker compose config -q und docker compose up -d aircard). Sie leiten per 301 auf https://aircard-ios.org weiter und haben eigene Router, damit ein www-Fehler das Zertifikat der Hauptdomain nicht blockiert.

Tests

node --test deploy/webhook-server.test.js

Die Tests brauchen nur Node und starten die Server auf Port 0.