- ARCHITECTURE.md: ingredient/step (waren faelschlich recipe_*) - OPERATIONS.md: IMAGE_DIR (statt IMAGES_PATH) - session-handoff: /api/recipes/[id]/image POST/DELETE ergaenzt Findings aus REVIEW-2026-04-18.md / docs-vs-code.md
174 lines
7.4 KiB
Markdown
174 lines
7.4 KiB
Markdown
# 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-<version>` — App-Shell (JS/CSS/Static-Icons), cache-first
|
||
- `kochwas-data-v1` — Rezept-HTMLs + API-JSON (SWR)
|
||
- `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).
|