Files
AirCardIOS/deploy/README.md
T
ClaudeandClaude Sonnet 5.5 f6b9917b07 docs: Scope-Namen der Console (documents.*), sicherer .env-Befehl, X-Real-Ip-Risiko
- Der Key aircard-api braucht in der Console documents.read/documents.write (entspricht rows.* der API) sowie files.read/files.write.
- Der Befehl zum Eintragen des Keys nutzt read -rs, schreibt bei leerer Eingabe nichts und beginnt mit einem Zeilenumbruch
  (klebt nie an einer letzten .env-Zeile ohne Umbruch); Hinweis zum Zeilenumbruch auch bei der Eingabe mit nano.
- deploy/README.md: nginx-503 bei fehlendem Dienst, up -d nach .env-Änderung, X-Real-Ip im Docker-Netz als akzeptiertes Risiko.
- Kommentare in server.js und compose-snippet.yml angepasst.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 16:40:17 +00:00

122 lines
10 KiB
Markdown

# 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/api/appwrite.js`, `app.js`, `server.js` | `aircard-ios.org/api/` (nur diese drei Dateien) | API-Dienst `aircard-api` der Community-Galerie, siehe [api/README.md](api/README.md) |
| `deploy/compose-snippet.yml` | `docker-compose.yml` (nach dem eship-Block) | Dienste `aircard-build`, `aircard` und `aircard-api` |
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)
```sh
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 $APP/aircard-ios.org/api
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
cp $SRC/api/appwrite.js $SRC/api/app.js $SRC/api/server.js $APP/aircard-ios.org/api/
# 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, für den API-Dienst außerdem `AIRCARD_APPWRITE_PROJECT_ID`, `AIRCARD_API_KEY` und `AIRCARD_API_SALT`. Diese Werte gehören nie in Logs, Chats oder Commits. Das Salz entsteht, ohne dass der Wert auf dem Bildschirm erscheint:
```sh
printf 'AIRCARD_API_SALT=%s\n' "$(openssl rand -hex 32)" >> /home/webklar/apps/.env
```
Den Key `AIRCARD_API_KEY` legt Kenso nach [docs/appwrite-console.md](../docs/appwrite-console.md) (Abschnitt 7.3) an (Scopes `documents.read`, `documents.write`, `files.read`, `files.write`, so beschriftet die Console die Zeilen-Scopes) und trägt ihn mit dem verdeckten `read -rs`-Befehl ein (leere Eingabe schreibt nichts).
## Starten
```sh
cd /home/webklar/apps && docker compose up -d aircard-build aircard aircard-api
```
Nur diese drei 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. Ebenso beendet sich `aircard-api` mit Exit-Code 1, wenn `AIRCARD_API_KEY`, `AIRCARD_API_SALT` oder `AIRCARD_APPWRITE_PROJECT_ID` fehlen (die Meldung nennt nur den Namen der Variable).
## API-Dienst `aircard-api`
Kleiner Node-Dienst ohne Abhängigkeiten (`deploy/api/`), Container `webklar-aircard-api`. Er führt alle Schreibzugriffe der Community-Galerie mit dem Appwrite-Server-Key aus (Veröffentlichen mit Bildmaß-Prüfung und höchstens 20 Karten je Konto, Zurückziehen, Melden mit Auto-Ausblenden ab 3 Meldern aus verschiedenen Netzen, Admin-Sichtbarkeit). Endpunkte, Statuscodes und Betrieb stehen in [api/README.md](api/README.md).
- **Route**: Traefik-Label am Dienst, Router `aircard-gallery-api` (``Host(`aircard-ios.org`) && PathPrefix(`/api`)``, Priorität 200, Port 8088). Der Name ist absichtlich nicht `aircard-api`: so heißt schon die Appwrite-Route (`/v1`) in `traefik/dynamic/aircard.yml`.
- **Prüfen** nach dem Start: `curl -s https://aircard-ios.org/api/gesundheit` liefert `{"ok":true}`; `docker logs webklar-aircard-api` zeigt `listening on :8088`. Läuft der Dienst nicht, fehlt auch sein Traefik-Router: nginx antwortet dann auf `/api/*` mit `503` und `{"error":"Der Dienst ist gerade nicht erreichbar."}` (Content-Type `application/json`), nie mit der App.
- **Ändern**: Dateien mit `cp` nach `aircard-ios.org/api/` ersetzen, dann `cd /home/webklar/apps && docker compose restart aircard-api`. Nach einer Änderung der `.env` (Key, Salz) reicht `restart` nicht: `docker compose up -d aircard-api`.
- **Log-Grenze**: Der Docker-Daemon des Servers hat keine eigenen Log-Optionen, deshalb steht im Service `logging: json-file` mit `max-size: 5m` und `max-file: 2` (höchstens 10 MB). Der Dienst selbst schreibt kein Zugriffsprotokoll und höchstens 120 Zeilen pro Minute.
- **Kein Port nach außen**: der Dienst vertraut dem Header `X-Real-Ip` (setzt Traefik) für Rate-Limit und Melder-Hash, deshalb nie `ports:` ergänzen. Im Docker-Netz `webklar-network` erreichen ihn andere Container trotzdem und könnten `X-Real-Ip` setzen: akzeptiertes Risiko, siehe [api/README.md](api/README.md) (Client-IP). Vor einem AAAA-Eintrag für aircard-ios.org die Client-IP-Ermittlung prüfen.
## Manuell deployen
```sh
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; für `aircard-api` zusätzlich `AIRCARD_API_KEY` (Key `aircard-api`) und `AIRCARD_API_SALT`.
## Ä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`.
- `deploy/api/*.js` ändern: ebenso mit `cp` ersetzen, dann `cd /home/webklar/apps && docker compose restart aircard-api`.
- 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
```sh
node --test deploy/webhook-server.test.js deploy/deploy.test.js
node --test "deploy/api/*.test.js"
```
Die Tests brauchen nur Node und starten die Server auf Port 0. Die API-Tests laufen gegen einen Fake-Appwrite im Speicher (`deploy/api/fake-appwrite.js`); das echte Appwrite wird nie angesprochen. Das Glob muss in Anführungszeichen stehen.