# Kochwas — Deployment & Operations ## Deployment-Topologie ``` Browser ↓ HTTPS (kochwas.siegeln.net) Cloudflare DNS (A-Record auf Pi-IP oder Tunnel) ↓ Raspberry Pi 5 (arm64, Debian/Ubuntu) ↓ Traefik v3 (Docker, Container "traefik" im Netz traefik_proxy) ↓ reverse proxy Kochwas-Container (Node 22 Alpine, Port 3000, internal bridge) ↔ SearXNG-Container (Sidecar im gleichen Stack, Port 8080 intern) ``` - **Traefik** terminiert TLS mit Wildcard-Cert `*.siegeln.net` von Let's Encrypt (DNS-01 Challenge über Cloudflare-API). - **SearXNG** läuft als Sidecar im kochwas-Compose. Kochwas spricht ihn über `http://searxng:8080` intern an. - **Gitea Registry** `gitea.siegeln.net/claude/kochwas` hostet das arm64-Image. - **Daten** liegen im Volume `/opt/docker/kochwas/data/` (SQLite + images/). ## Build & Publish (Gitea Actions) Workflow in `.gitea/workflows/docker.yml`: 1. Trigger: push auf `main` 2. Checkout, Setup QEMU + Buildx 3. Login an `gitea.siegeln.net` mit Secret `REGISTRY_TOKEN` (PAT mit `write:package` + `read:package` Scope) 4. `docker/build-push-action` baut **nativ arm64** (nicht via emuliertem amd64!), mit `cache-from/to: type=registry,ref=...:buildcache` 5. Push als `:latest` und `:${commit}` Wenn die Pipeline rot ist, häufig: - `unauthorized`: Token fehlt oder ohne Package-Scope. PAT unter Gitea → Settings → Applications → Generate Token. - Build-Cache i/o-Timeout: Registry-Cache benutzen, nicht GHA-Artifact-Cache. ## Deploy auf den Pi ```bash ssh admin@pi5 cd /opt/docker/kochwas docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d docker compose logs -f kochwas ``` Was der Pi braucht (einmalig): - `/opt/docker/kochwas/docker-compose.prod.yml` — gespiegelt aus dem Repo - `/opt/docker/kochwas/.env` mit `KOCHWAS_TAG=latest` (optional) und `SEARXNG_SECRET=...` - `/opt/docker/kochwas/searxng/settings.yml` — aus dem Repo, mit `limiter: false` und `public_instance: false` - `/opt/docker/kochwas/data/` existiert (für SQLite + images) - Netzwerk `traefik_proxy` existiert, damit Traefik den Container sieht ## Traefik-Integration Labels am kochwas-Container (siehe `docker-compose.prod.yml`): ```yaml - traefik.enable=true - traefik.docker.network=traefik_proxy - traefik.http.routers.kochwas.rule=Host(`kochwas.siegeln.net`) - traefik.http.routers.kochwas.entrypoints=websecure - traefik.http.routers.kochwas.tls.certresolver=cloudflareResolver - traefik.http.routers.kochwas.tls.domains[0].main=siegeln.net - traefik.http.routers.kochwas.tls.domains[0].sans=*.siegeln.net - traefik.http.services.kochwas.loadbalancer.server.port=3000 ``` Die `tls.domains`-Zeilen sorgen dafür, dass der Router das Wildcard-Cert nutzt statt einen neuen per-Host-Cert zu holen. **Nie per-Host für neue Subdomains** — Let's Encrypt Rate-Limit (5 failed Authorizations pro Identifier pro Stunde, 50 Certs pro Registered Domain pro Woche). ### Wenn Cert fehlt / TLS-Fehler 1. `echo | openssl s_client -servername kochwas.siegeln.net -connect kochwas.siegeln.net:443 2>/dev/null | openssl x509 -noout -issuer -subject` — ist der Issuer „TRAEFIK DEFAULT CERT"? Dann hat Traefik kein Cert. 2. `sudo jq '.cloudflareResolver.Certificates | map(.domain.main)' /opt/docker/traefik/letsencrypt/acme.json` — ist `siegeln.net` (mit SAN `*.siegeln.net`) dabei? 3. `docker logs traefik 2>&1 | grep -iE 'lego|acme|cloudflare|kochwas' | tail -60` — Fehler? - `Invalid access token` → Cloudflare-API-Token abgelaufen, neu erstellen mit `Zone → DNS → Edit` Scope, `CF_DNS_API_TOKEN` im Traefik-Compose setzen, `docker compose up -d traefik` - `429 rateLimited` → Warten (zeitangabe im Error) oder auf Wildcard umstellen ## Troubleshooting ### Container läuft, Traefik filtert ihn raus Symptom: Traefik-Logs sagen `Filtering unhealthy or starting container`. Ursache: Healthcheck schlägt fehl. Der Check ruft `wget 127.0.0.1:3000/api/health` (muss IPv4 sein!). ```bash docker inspect kochwas-kochwas-1 --format '{{json .State.Health}}' | jq docker exec kochwas-kochwas-1 wget -qO- 127.0.0.1:3000/api/health ``` ### SearXNG gibt 403 zurück Log: `Internet-Suche zurzeit nicht möglich: HTTP 403` Ursache: Bot-Detection. Fix war schon einmal nötig — `src/lib/server/http.ts` setzt via `extraHeaders` `X-Forwarded-For: 127.0.0.1` und `X-Real-IP: 127.0.0.1`. Wenn trotzdem 403: `searxng/settings.yml` prüfen: ```yaml use_default_settings: true server: limiter: false public_instance: false secret_key: ${SEARXNG_SECRET:-dev-secret-change-in-prod} search: formats: [html, json] ``` Der Server-Container muss diese Datei per Volume Mount sehen. Nach Änderung: `docker compose restart searxng`. ### Thumbnail-Cache leeren ```bash docker exec kochwas-kochwas-1 sqlite3 /data/kochwas.db 'DELETE FROM thumbnail_cache;' ``` Oder gezielt eine URL: ```bash docker exec kochwas-kochwas-1 sqlite3 /data/kochwas.db \ "DELETE FROM thumbnail_cache WHERE url = 'https://www.chefkoch.de/rezepte/...';" ``` ### Datenbank-Backup manuell ```bash ssh admin@pi5 'docker exec kochwas-kochwas-1 sqlite3 /data/kochwas.db ".backup /data/backup.db"' scp admin@pi5:/opt/docker/kochwas/data/backup.db ./kochwas-$(date +%F).db ``` Die App hat ein eingebautes Backup unter `/admin` (ZIP-Export mit DB + Bildern). Restore via `/admin` ebenfalls. ## Umgebungsvariablen | Name | Default | Bedeutung | |---|---|---| | `SEARXNG_URL` | `http://localhost:8888` | SearXNG-Endpoint, im Compose auf `http://searxng:8080` | | `KOCHWAS_THUMB_TTL_DAYS` | `30` | TTL für Thumbnail-Cache in der SQLite | | `DATABASE_PATH` | `data/kochwas.db` | Pfad zur SQLite, relativ oder absolut | | `IMAGE_DIR` | `data/images` | Pfad für lokale Bild-Dateien | | `PORT` | `3000` | Node-HTTP-Port (adapter-node) | Siehe `.env.example` im Repo. ## Häufige Commits als Referenz - **Healthcheck-Fix** → `Dockerfile` (localhost → 127.0.0.1, tightened interval) - **SearXNG-Bot-Bypass** → `src/lib/server/http.ts` (extraHeaders) - **Traefik-Wildcard** → `docker-compose.prod.yml` (tls.domains Labels) - **Thumbnail-Cache in SQLite** → `003_thumbnail_cache.sql` + `searxng.ts` Git-Log ist die Wahrheit; diese Datei ist eine Orientierung. ## PWA / Offline-Modus Kochwas ist eine installierbare PWA. Erkennbar an: - `static/manifest.webmanifest` (Manifest + Icons: SVG + 192×192 + 512×512, alle maskable) - `src/service-worker.ts` (Cache + Sync) Caches im Browser (siehe DevTools → Application → Cache Storage): - `kochwas-shell-` — App-Shell (JS/CSS/Static-Icons), cache-first - `kochwas-data-v1` — Rezept-HTMLs + API-JSON (network-first, 3 s Timeout → Cache-Fallback) - `kochwas-images-v1` — Bilder (cache-first) - `kochwas-meta` — Cache-Manifest (Liste der gecachten Rezept-IDs unter `/__cache-manifest__`) Sync-Verhalten: - **Initial-Sync** (nach erstem Install): SW lädt alle Rezepte + Bilder im Hintergrund. Fortschritt im `SyncIndicator`-Pill unten rechts. - **Update-Sync** (bei jedem App-Start online): Diff gegen Cache-Manifest, nur Delta nachladen, gelöschte IDs räumen. - **Storage-Quota-Check**: < 100 MB frei → abbrechen mit Fehler-Toast. Bei SW-Problemen Debug-Pfad: 1. Admin → „App"-Tab → „Offline-Cache leeren" (destructive, zweistufig bestätigt) 2. Alternative: DevTools → Application → Service Workers → Unregister, dann Seite neu laden. E2E-Tests (Playwright): `npm run test:e2e`. Setzt `npm run build` voraus (Playwright startet automatisch `npm run preview`). Icons einmalig rendern: `npm run render:icons` (schreibt nach `static/icon-*.png`, committen). ## Dev-System / Remote-E2E `https://kochwas-dev.siegeln.net/` ist ein separates Deployment (eigener Container, eigene DB unter `/opt/docker/kochwas-dev/data/`). Zweck: E2E-Tests gegen eine prod-nahe Umgebung ohne Angst vor DB-Schäden. Die Remote-Suite (`tests/e2e/remote/`, Config `playwright.remote.config.ts`) darf dort frei CRUDen — User stellt die DB bei Bedarf per Backup wieder her. ```bash npm run test:e2e:remote # gegen kochwas-dev E2E_REMOTE_URL=https://... npm run test:e2e:remote # andere URL ``` Wichtige Config-Eigenschaften: - `workers: 1` — DB-Race-Sicherheit bei CRUD-Tests. - `serviceWorkers: 'block'` — verhindert Chromium-Crashes durch akkumulierten SW-State über 40+ Contexts. - Fixtures unter `tests/e2e/remote/fixtures/`: `profile.ts` (Profile-Auswahl via localStorage vor Seitenladen), `api-cleanup.ts` (idempotente DELETE-Helfer für afterEach). **Niemals gegen `kochwas.siegeln.net` (ohne `-dev`)** die destruktiven Tests laufen lassen — das ist Prod.