Kyle DammannandClaude Opus 5.5 c76fd119cf Make new recipes public by default
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]>
2026-10-30 23:05:05 +01:00
2026-10-30 23:05:05 +01:00

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

  1. Installation
  2. Reverse Proxy (Nginx + Cloudflare)
  3. Konfiguration
  4. Erste Schritte
  5. KI-Funktionen
  6. Instagram-Import
  7. Update
  8. Backup & Wiederherstellung
  9. Entwicklung & Tests
  10. Fehlersuche
  11. 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:

  1. Empfohlen: Nginx ermittelt die echte IP selbst
    # 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;
    
    und in der .env: TRUSTED_PROXIES=<IP deines Nginx> (z. B. 172.18.0.0/16, wenn Nginx im Docker-Netz läuft).
  2. Oder in der .env CLIENT_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:

  1. Screenshot des Beitrags (mit ausgeklappter Beschreibung) über „Foto oder Screenshot“ importieren, oder
  2. 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).
S
Description
No description provided
Readme
208 KiB
Languages
Python 81.7%
HTML 10.2%
CSS 4.3%
Shell 2.4%
JavaScript 1%
Other 0.4%