New recipes (manual, AI draft and all imports) now start as public; private can still be chosen per recipe. Existing recipes are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Kochbuch
Selbst gehostetes Kochbuch für Familie und Freunde – fürs Handy gemacht.
- Rezepte mit Zutaten (Mengen, Einheiten, Gruppen wie „Für die Soße“), Schritten, Tags, Notizen, Quelle und mehreren Fotos (direkt aus der Handykamera, auch HEIC).
- Privat oder öffentlich pro Rezept. Private Rezepte sieht nur der Autor – auch kein Admin, in keiner Liste, Suche und unter keiner URL (inkl. Fotos). Das ist mit automatisierten Tests abgesichert.
- Suche nach Titel, Zutaten und Tags, Favoriten, Kommentare, Portionsrechner und ein Kochmodus mit großer Schrift, Abhak-Listen und Bildschirm-bleibt-an.
- Import: Link zu einer Rezeptseite (schema.org), Instagram-Beitrag, Foto/Screenshot oder eingefügter Text. Mit OpenAI-Key werden Texte und Bilder per KI strukturiert. Das Ergebnis ist immer ein Entwurf, den du prüfst und erst dann speicherst.
- Nur auf Einladung: keine offene Registrierung. Admins verwalten Nutzer, Einladungen und Passwort-Resets.
- Als App installierbar (PWA); unter Android erscheint „Kochbuch“ im Teilen-Menü, z. B. direkt aus Instagram.
Technik: Django 5.2 (Python 3.12), PostgreSQL 17, gunicorn. Zwei Container, ca. 150–250 MB RAM.
Inhalt
- Installation
- Reverse Proxy (Nginx + Cloudflare)
- Konfiguration
- Erste Schritte
- KI-Funktionen
- Instagram-Import
- Update
- Backup & Wiederherstellung
- Entwicklung & Tests
- Fehlersuche
- Sicherheit
Installation
Voraussetzungen: Docker mit dem Compose-Plugin (docker compose version), Git.
git clone <gitea-url>/kochbuch.git /opt/stacks/kochbuch
cd /opt/stacks/kochbuch
./scripts/init-env.sh [email protected] # erzeugt .env mit zufälligen Secrets + Admin-Passwort
nano .env # APP_BASE_URL, APP_PORT, TRUSTED_PROXIES, OPENAI_API_KEY prüfen
docker compose up -d --build
docker compose ps # "app" sollte nach ~30 s "healthy" sein
init-env.sh gibt das Admin-Passwort einmal aus. Damit meldest du dich an. Nach dem ersten Start kannst du
ADMIN_PASSWORD aus der .env löschen – ein bestehendes Konto wird nie überschrieben.
Ohne das Skript: cp .env.example .env und mindestens SECRET_KEY, POSTGRES_PASSWORD, APP_BASE_URL,
ADMIN_EMAIL und ADMIN_PASSWORD ausfüllen.
Daten liegen in zwei Docker-Volumes: kochbuch_pgdata (Datenbank) und kochbuch_media (Fotos).
Reverse Proxy (Nginx + Cloudflare)
Die App spricht nur HTTP auf APP_PORT (Standard 8000). TLS macht dein Proxy.
server {
server_name kochbuch.dammann.vip;
# listen 443 ssl; Zertifikate … wie bei deinen anderen Diensten
client_max_body_size 40m; # mehrere Handyfotos pro Upload
location / {
proxy_pass http://<docker-host>:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 120s; # KI-Aufrufe können bis zu ~90 s dauern
}
}
Wichtig – echte Client-IP: Login-Sperren arbeiten pro IP. Die App wertet X-Forwarded-For nur aus, wenn
die Anfrage von einer Adresse aus TRUSTED_PROXIES kommt. Hinter Cloudflare sieht Nginx zunächst
Cloudflare-IPs. Zwei Möglichkeiten:
- Empfohlen: Nginx ermittelt die echte IP selbst
und in der
# in http { } – aktuelle Liste: https://www.cloudflare.com/ips/ set_real_ip_from 173.245.48.0/20; # … weitere Cloudflare-Netze … real_ip_header CF-Connecting-IP;.env:TRUSTED_PROXIES=<IP deines Nginx>(z. B.172.18.0.0/16, wenn Nginx im Docker-Netz läuft). - Oder in der
.envCLIENT_IP_HEADER=CF-Connecting-IP. Nur sicher, wenn dein Nginx ausschließlich über Cloudflare erreichbar ist – sonst kann der Header gefälscht werden.
Cloudflare-SSL-Modus „Full (strict)“ verwenden. Falls die App „CSRF-Prüfung fehlgeschlagen“ meldet:
stimmt APP_BASE_URL exakt mit der Adresse im Browser überein (inkl. https://)?
Konfiguration
Alle Variablen stehen kommentiert in .env.example. Die wichtigsten:
| Variable | Bedeutung | Standard |
|---|---|---|
SECRET_KEY |
Langer Zufallswert (Sessions, Tokens) | – (Pflicht) |
APP_BASE_URL |
Öffentliche Adresse, z. B. https://kochbuch.dammann.vip |
– (Pflicht) |
APP_PORT / APP_BIND_ADDRESS |
Port/Interface auf dem Host | 8000 / 0.0.0.0 |
TRUSTED_PROXIES |
IPs/Netze, deren Forwarded-Headern vertraut wird | private Netze |
CLIENT_IP_HEADER |
z. B. CF-Connecting-IP (siehe oben) |
leer |
COOKIE_SECURE |
Cookies nur über HTTPS | true |
POSTGRES_PASSWORD |
Datenbank-Passwort | – (Pflicht) |
ADMIN_EMAIL / ADMIN_PASSWORD |
Erster Admin (nur beim ersten Start) | leer |
OPENAI_API_KEY |
Schaltet die KI-Funktionen frei | leer = KI aus |
OPENAI_MODEL |
Modell für Text und Bilder (muss Vision + Structured Outputs können) | gpt-5-mini |
OPENAI_BASE_URL |
Optional: OpenAI-kompatibler Endpunkt oder Proxy | leer |
OPENAI_REASONING_EFFORT |
Für Reasoning-Modelle; bei anderen Modellen leer lassen | low |
AI_RATE_LIMIT_PER_USER_PER_DAY |
KI-Aufrufe pro Nutzer/Tag, 0 = unbegrenzt |
0 |
AI_RATE_LIMIT_GLOBAL_PER_DAY |
KI-Aufrufe gesamt/Tag, 0 = unbegrenzt |
0 |
IMAGE_MAX_UPLOAD_MB / IMAGE_MAX_PER_RECIPE |
Foto-Limits | 15 / 10 |
LOGIN_MAX_FAILURES_PER_EMAIL / _PER_IP |
Login-Sperre nach Fehlversuchen | 5 / 30 in 15 Min. |
GUNICORN_WORKERS / GUNICORN_THREADS |
Server-Prozesse/Threads | 2 / 4 |
Nach Änderungen an der .env: docker compose up -d (startet die App mit den neuen Werten neu).
Erste Schritte
- Jemanden einladen: Menü → Einladungen → Link erzeugen und z. B. per WhatsApp schicken. Der Link ist einmalig, standardmäßig 7 Tage gültig und wird nur einmal angezeigt.
- Passwort vergessen: Menü → Nutzer verwalten → Aktionen → „Reset-Link erzeugen“ (24 h gültig) oder „Temporäres Passwort“ (muss nach dem Login sofort geändert werden).
- Nutzer entfernen: „Deaktivieren“ – das Konto kann sich nicht mehr anmelden, seine Rezepte bleiben. Bei deaktivierten Nutzern gibt es zusätzlich „Rezepte löschen …“.
- Als App installieren: Im Handy-Browser „Zum Startbildschirm hinzufügen“.
Zutaten eingeben: eine Zutat pro Zeile, z. B. 200 g Mehl, gesiebt oder 2-3 Zehen Knoblauch. Eine Zeile
mit Doppelpunkt am Ende (Für die Soße:) startet eine Gruppe. Schritte durch Leerzeilen trennen.
KI-Funktionen
Mit OPENAI_API_KEY stehen zur Verfügung:
- „✨ Mit KI aufbereiten“ im Rezeptformular: strukturiert deinen Text, formuliert Schritte, schlägt Tags vor.
- Import per Foto/Screenshot (Kochbuchseite, Screenshot einer App oder eines Instagram-Beitrags).
- Import per eingefügtem Text und als Fallback für Webseiten ohne strukturierte Rezeptdaten.
Die KI liefert über Structured Outputs (striktes JSON-Schema) ein Ergebnis, das zusätzlich serverseitig validiert wird. Gespeichert wird nichts ohne deine Bestätigung. Fotos für die Erkennung werden verkleinert an OpenAI geschickt und nicht gespeichert.
Ohne Key funktioniert alles andere normal; KI-Buttons sind ausgeblendet bzw. zeigen einen Hinweis.
Kosten: Ein Aufruf kostet mit gpt-5-mini typischerweise deutlich unter einem Cent (Bilder etwas mehr).
Als Kostenbremse kannst du AI_RATE_LIMIT_* setzen und bei OpenAI ein Monatslimit für das Projekt hinterlegen.
Meldet die App „Modell nicht verfügbar“, setze OPENAI_MODEL auf ein aktuelles Modell deines Accounts.
Instagram-Import
Instagram bietet keinen Weg mehr, Beiträge ohne Login auszulesen. Die offizielle oEmbed-Schnittstelle braucht eine Meta-App und liefert keine Beitragstexte. Die App macht deshalb einen ehrlichen Abruf der öffentlichen Einbettungsseite des Beitrags – ohne Login, Cookies oder Tricks. Instagram blockiert das von Servern aus häufig. Dann zeigt die App direkt die zuverlässigen Alternativen:
- Screenshot des Beitrags (mit ausgeklappter Beschreibung) über „Foto oder Screenshot“ importieren, oder
- Beschreibung kopieren und bei „Text einfügen“ einfügen (der Link wird als Quelle übernommen).
Am Android-Handy geht's am schnellsten über Teilen → Kochbuch (App vorher installieren).
Update
cd /opt/stacks/kochbuch
./scripts/backup.sh # vorher sichern
git pull
docker compose up -d --build # Migrationen laufen beim Start automatisch
docker image prune -f # alte Images aufräumen (optional)
PostgreSQL bleibt auf Version 17 (postgres:17-alpine). Ein Wechsel der Major-Version braucht
Backup → neues Volume → Restore.
Backup & Wiederherstellung
Backup (Datenbank, Fotos und .env in einem Archiv, die neuesten 14 bleiben erhalten):
./scripts/backup.sh # nach ./backups
./scripts/backup.sh /mnt/nas/kochbuch # anderes Ziel
KEEP=30 ./scripts/backup.sh # 30 Archive behalten
Täglich per Cron (crontab -e):
30 3 * * * cd /opt/stacks/kochbuch && ./scripts/backup.sh /mnt/nas/kochbuch >> /var/log/kochbuch-backup.log 2>&1
Das Archiv enthält die .env mit Passwörtern – entsprechend geschützt ablegen.
Wiederherstellen (ersetzt alle aktuellen Daten):
./scripts/restore.sh backups/kochbuch-20261030-033000.tar.gz
Auf einem neuen Server: Repository klonen, Archiv hineinkopieren, restore.sh ausführen – fehlt die .env,
wird sie aus dem Backup übernommen. Danach docker compose up -d --build.
Entwicklung & Tests
python -m venv .venv && . .venv/bin/activate
pip install -r requirements-dev.txt
python -m pytest # Tests (SQLite, ohne Netzwerk)
python scripts/dev_server.py # http://localhost:8000, Login: [email protected]
docker compose --profile test run --rm test # dieselben Tests gegen PostgreSQL
Die Tests decken u. a. ab: Anmeldung, Einladungen, Resets, Rollen, Sichtbarkeit privater Rezepte über alle
URLs inkl. Bildern, Upload-Validierung, Parser, KI-Antwortvalidierung und Fehlerbehandlung, SSRF-Schutz,
Link-/Instagram-Import. Architektur und Konventionen: CLAUDE.md.
Fehlersuche
| Problem | Lösung |
|---|---|
docker compose ps zeigt unhealthy |
docker compose logs app – meist fehlt SECRET_KEY oder die DB ist nicht erreichbar |
| Login klappt nicht, Seite lädt neu | Läuft der Zugriff über HTTPS? Lokal ohne HTTPS: COOKIE_SECURE=false |
| „CSRF-Prüfung fehlgeschlagen“ | APP_BASE_URL muss exakt der Browser-Adresse entsprechen; Nginx muss Host weiterreichen |
| Alle werden gleichzeitig ausgesperrt | Client-IP falsch erkannt → Abschnitt Reverse Proxy (TRUSTED_PROXIES, CLIENT_IP_HEADER) |
| Foto-Upload bricht ab (413) | client_max_body_size in Nginx erhöhen |
| KI: „Modell nicht verfügbar“ / „API-Key ungültig“ | OPENAI_MODEL bzw. OPENAI_API_KEY in .env prüfen, docker compose up -d |
| KI: Zeitüberschreitung | proxy_read_timeout ≥ 120 s; ggf. OPENAI_REASONING_EFFORT=minimal |
Sicherheit
- Registrierung nur per Einladung; Einladungs- und Reset-Links werden nur gehasht gespeichert.
- Passwörter mit Argon2; Login-Sperre pro E-Mail und IP; CSRF-Schutz auf allen Formularen.
- Strikte Content-Security-Policy, HSTS,
X-Frame-Options: DENY,__Host--Cookies (Secure, HttpOnly, SameSite). - Sichtbarkeit wird bei jedem Zugriff serverseitig geprüft; Fotos werden nur über eine geprüfte URL ausgeliefert.
- Uploads: Typ-Prüfung durch Dekodieren, Größenlimit, Neu-Kodierung (entfernt EXIF/GPS).
- Abruf fremder URLs nur zu öffentlichen IP-Adressen (Schutz vor Zugriffen ins Heimnetz).
- Keine Secrets im Repository – alles über
.env(in.gitignore).