Einkaufsliste
Selbst gehostete Einkaufslisten-App für Haushalt, WG oder Verein. Läuft als Progressive Web App auf dem Telefon, funktioniert ohne Verbindung weiter und gleicht ab, sobald wieder Netz da ist.
Keine Werbung, kein Tracking, keine externen Schriften oder Skripte. Alle Daten bleiben auf dem eigenen Server.
Funktionen
- Listen teilen – per Einladung mit Rollen (lesen, bearbeiten, verwalten) oder über einen zeitlich begrenzten öffentlichen Link
- Offline nutzbar – Änderungen werden lokal festgehalten und später nachgereicht, ohne dass etwas verlorengeht
- Nach Märkten und Warengruppen sortiert – die Reihenfolge lässt sich an den Weg durch den Laden anpassen
- Strichcodes scannen – eigener EAN-Decoder, Produktdaten von Open Food Facts (Abfrage über den Server, nicht vom Gerät)
- Preise erfassen und vergleichen – je Markt, ohne Personenbezug
- Push-Benachrichtigungen bei Änderungen, gedrosselt auf höchstens eine Meldung je Liste alle zwei Stunden
- Druckansicht auf A4, nach Märkten gegliedert
- Benutzerverwaltung mit einstellbaren Fristen für Bereinigung und Archiv
Technik
| Schicht | Verwendet |
|---|---|
| Backend | Python 3.12, FastAPI, SQLAlchemy 2, Alembic |
| Datenbank | MariaDB 11.4 |
| Frontend | ES-Module, kein Framework, kein Bauschritt |
| Auslieferung | nginx |
| Betrieb | Docker Compose, drei Container |
Voraussetzungen
- Docker mit Compose-Plugin
- Ein SMTP-Zugang für Bestätigungs- und Einladungsmails
- Für den öffentlichen Betrieb: ein Reverse Proxy mit TLS (siehe unten)
Installation
git clone <repository-url> einkaufsapp
cd einkaufsapp
cp .env.example .env
In der .env mindestens setzen:
DB_PASSWORD=… # eigenes Passwort
DB_ROOT_PASSWORD=… # anderes Passwort
ADMIN_EMAIL=du@example.de # das erste Konto
ADMIN_INITIAL_PASSWORD=… # mind. 12 Zeichen, danach ändern
PUBLIC_BASE_URL=https://einkauf.example.de
COOKIE_SECURE=true # false nur beim Testen ohne TLS
SMTP_HOST=mail.example.de
SMTP_PORT=587
SMTP_SECURITY=starttls
SMTP_USER=…
SMTP_PASSWORD=…
SMTP_FROM=einkauf@example.de
Prüfen und starten:
bash tools/check-env.sh .env
docker compose up -d
docker compose logs -f api
Beim ersten Start legt die Anwendung das Administratorkonto an und führt die
Datenbankmigrationen aus. Danach ist sie unter http://<host>:8080/ erreichbar.
Nach der ersten Anmeldung: Passwort ändern und ADMIN_INITIAL_PASSWORD in
der .env leeren – der Wert wird danach ignoriert, stünde aber weiter im
Klartext in der Umgebung.
Push-Benachrichtigungen (freiwillig)
python3 tools/vapid-keys.py
Die drei ausgegebenen Zeilen in die .env übernehmen – vorhandene Zeilen
ersetzen, nicht ergänzen. Bei doppelten Einträgen nimmt Docker Compose den
letzten, und das ist oft die leere Vorlagenzeile.
docker compose up -d
TLS und Reverse Proxy
Die Anwendung fordert keine Zertifikate an und verwaltet sie nicht. Sie liefert ausschließlich HTTP auf dem konfigurierten Port aus. Für TLS ist ein vorgeschalteter Reverse Proxy zuständig.
Ohne HTTPS fehlen wesentliche Funktionen: Service Worker, Kamerazugriff zum
Scannen und Push-Benachrichtigungen sind an einen sicheren Kontext gebunden.
Für lokale Tests gilt localhost als sicher, jede andere Adresse nicht.
Variante A: nginx davor
server {
listen 443 ssl http2;
server_name einkauf.example.de;
ssl_certificate /etc/letsencrypt/live/einkauf.example.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/einkauf.example.de/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Server-Sent Events: ohne diese beiden Zeilen bekommt die App
# keine Änderungen mit, die andere gemacht haben.
proxy_buffering off;
proxy_read_timeout 3600s;
}
}
Wichtig: Setze am vorgelagerten Proxy keine eigene Content-Security-Policy. Der mitgelieferte nginx bringt bereits eine mit. Zwei Richtlinien werden vom Browser als Schnittmenge angewandt – die Anwendung hört dann ohne erkennbaren Fehler auf zu funktionieren.
Die übrigen Sicherheitskopfzeilen (HSTS, X-Frame-Options,
Referrer-Policy) doppelt zu setzen ist unschädlich.
Variante B: Traefik
docker-compose.override.yml im Projektverzeichnis:
services:
web:
ports: [] # kein direkter Port mehr nötig
networks: [einkaufsapp, traefik]
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.http.routers.einkauf.rule=Host(`einkauf.example.de`)"
- "traefik.http.routers.einkauf.entrypoints=websecure"
- "traefik.http.routers.einkauf.tls.certresolver=le"
- "traefik.http.services.einkauf.loadbalancer.server.port=80"
# SSE: Antworten nicht puffern
- "traefik.http.services.einkauf.loadbalancer.responseforwarding.flushinterval=100ms"
networks:
traefik:
external: true
Traefik setzt X-Forwarded-* von sich aus. Auch hier gilt: keine zusätzliche
CSP über eine Middleware.
Nach dem Einrichten prüfen
curl -sI https://einkauf.example.de | grep -i content-security-policy
Es darf genau eine Zeile erscheinen.
Betrieb
Aktualisieren
cd einkaufsapp
git pull
bash tools/check-all.sh # alle Prüfungen
docker compose up -d --build
docker compose logs --tail=30 api
Migrationen laufen beim Start automatisch. Vorher sichern – Migrationen lassen sich nicht immer zurücknehmen.
Sichern
docker compose exec -T db mariadb-dump -u root -p"$DB_ROOT_PASSWORD" \
--single-transaction --quick --routines --events \
--default-character-set=utf8mb4 einkaufsapp | gzip > einkaufsapp-$(date +%F).sql.gz
Sichere den Datenbankauszug, nicht das Volume: Ein Dateisystemabbild einer
laufenden Datenbank ist nicht zuverlässig wiederherstellbar. --single-transaction
liefert einen konsistenten Stand, ohne die Anwendung anzuhalten.
Die .env gehört ebenfalls gesichert – aber getrennt und verschlüsselt. Sie
enthält Datenbankpasswort, SMTP-Zugangsdaten und den privaten VAPID-Schlüssel.
Ausführlich: docs/betrieb.md
Prüfskripte
bash tools/check-all.sh
Prüft Konfiguration, Berechtigungen aller Endpunkte, Datenbankschema gegen die Modelle, nginx-Kopfzeilen, JavaScript-Module, Fristen und den Strichcode-Decoder. Rückgabewert 1 bei Problemen – als Git-Hook oder in einer CI verwendbar.
Funktionstest gegen eine laufende Instanz
bash tools/smoke-test.sh http://localhost:8080 admin@example.de deinpasswort
Legt eine Liste an, fügt Einträge hinzu, hakt ab, räumt auf und löscht sich selbst wieder. Nützlich nach einem Update.
Entwicklungsmodus
cp docker-compose.override.yml.example docker-compose.override.yml
docker compose up -d
Bindet den Quelltext ein und startet uvicorn bei Änderungen neu. Die Datei gehört nicht auf den Produktivserver – dort liefe sonst Code vom Host statt aus dem gebauten Abbild.
Rechtliches
web/html/impressum.html und web/html/datenschutz.html sind Entwürfe mit
Platzhaltern. Vor der Veröffentlichung ausfüllen und prüfen lassen – die
Hinweiskästen im Dokument nennen, worauf zu achten ist.
Beide sind aus jeder Ansicht über die Fußzeile erreichbar.
Dokumentation
| Datei | Inhalt |
|---|---|
doku/ |
Ausführliche Erklärung des Codes, 17 Notizen mit Diagrammen |
docs/betrieb.md |
Sicherung, Wiederherstellung, Fehlersuche |
docs/sicherheit.md |
Prüfergebnisse und bekannte Grenzen |
docs/reverse-proxy.md |
Zusammenspiel mit einem vorgelagerten Proxy |
docs/entwicklung.md |
Entstehung der Anwendung, Phase für Phase |
db/schema.sql |
Vollständiges Datenbankschema |
Struktur
backend/ FastAPI, SQLAlchemy, Alembic-Migrationen
web/ nginx-Konfiguration und das Frontend
db/schema.sql Schema als ein Stück SQL
tools/ Prüfskripte
docs/ Betriebs- und Sicherheitsdokumentation
doku/ Code-Dokumentation zum Nachlesen
Entstehung
Diese Anwendung ist in Zusammenarbeit mit Claude (Anthropic) entstanden. Konzept, Anforderungen, fachliche Entscheidungen und das Testen im echten Betrieb kamen von mir; der Code wurde im Dialog erarbeitet – einschließlich der Fehlersuche, wenn etwas nicht funktionierte.
Ich halte diese Angabe für angebracht: Wer den Code liest, soll wissen, wie er entstanden ist. Die Verantwortung für das, was hier läuft, liegt trotzdem bei mir – geprüft, angepasst und im Alltag erprobt habe ich es selbst. Die Nutzung erfolgt ohne jegliche Garantie und auf eigene Verantwortung des jeweiligens Anwenders. Es wird keine Haftung für Datenverlust oder andere Schäden übernommen.
Lizenz
GPL v3 (General Public Licence Version 3)
Produktdaten zu Strichcodes stammen von Open Food Facts und stehen unter der Open Database License (ODbL).