StefanandClaude Opus 5 31f59fd3c8 Phase 5: Gitea Actions, Betriebs-Compose und README
Ein Versions-Tag (v*) löst den Bau aus, ein Push auf main nicht - so bleibt
das Zusammenführen von Branches folgenlos und ein Release ist ein bewusster
Schritt. Die CI baut, testet das FERTIGE Image und veröffentlicht erst
danach; "latest" nur bei echten Tags.

Beim Durchspielen des Prüfschritts zwei Fehler gefunden, die beide erst in
der CI aufgefallen wären - und einer davon gar nicht:

1. "docker exec" ohne -i hängt kein stdin an. Das Here-Dokument lief damit
   ins Leere, python bekam ein leeres Programm, tat nichts und meldete
   Erfolg. Ein Test, der nie hätte fehlschlagen können. Lokal nachgestellt
   und mit -i behoben.
2. Die YAML war ungültig: eingebettete Python-Blöcke standen links vom
   Einzug und brachen den run-Block auf. Das Prüfskript liegt jetzt als
   scripts/image-pruefen.py vor - dadurch lässt es sich auch lokal gegen
   ein gebautes Image laufen lassen - und die Warteschleife ist einzeilig.

Aus der Kantone-App übernommen: github.ref_name geht über env in die
Shell statt direkt als Ausdruck (Tag-Namen dürfen " und $ enthalten, und
der Job hält ein Registry-Token), Anmeldung per --password-stdin, docker
logout mit if: always(), Rechte auf contents:read und packages:write.

docker-compose.betrieb.yml liefert bewusst KEIN Passwort mit: ohne eigenes
bleibt der Erfassungsbereich gesperrt. Genau das war die schwerste Lücke
der Kantone-App.

Ausserdem fehlte eine .dockerignore - der ganze Kontext samt .git und .env
ging an den Docker-Daemon, obwohl das Dockerfile nur gezielt kopiert.

81 Tests grün, der CI-Prüfschritt lokal gegen das gebaute Image
durchgespielt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
2026-08-30 08:40:57 +02:00
2026-08-29 21:15:44 +02:00
2026-08-29 21:03:10 +02:00

Kinderkleider-Börse

Zu klein gewordene Kinderkleidung fotografieren, in einer Galerie zeigen und an Familie, Freunde und Nachbarn weitergeben. Wer etwas brauchen kann, reserviert es mit einem Klick ohne Konto, ohne Anmeldung.

Fürs Erfassen genügt das Smartphone: alles fotografieren, alle Fotos auf einmal hochladen, die Angaben in Ruhe nachtragen.


In vier Schritten

1. Einen leeren Ordner anlegen, zum Beispiel kleiderboerse.

2. Darin eine Datei namens docker-compose.yml anlegen und den Inhalt von docker-compose.betrieb.yml hineinkopieren.

3. Zwei Werte eintragen:

ADMIN_PASSWORD: "dein-passwort"
SECRET_KEY: "..."

Den SECRET_KEY erzeugen:

docker run --rm python:3.11-slim python -c "import secrets; print(secrets.token_urlsafe(48))"

Bleibt ADMIN_PASSWORD leer, ist der Erfassungsbereich gesperrt die Galerie funktioniert trotzdem. Absichtlich ist kein Passwort vorgegeben: ein mitgeliefertes stünde in dieser Anleitung und wäre damit bekannt.

4. Starten und öffnen:

docker compose up -d

http://localhost:8090


Erfassen

Nach dem Anmelden gibt es zwei Wege:

Viele auf einmal der Weg für den Erstbestand. Zuerst alles mit der Kamera-App fotografieren, dann alle Fotos zusammen hochladen. Pro Foto entsteht ein Entwurf; unter Entwürfe trägst du danach Grösse und Kategorie nach. Grösse, Kategorie, Saison und Zustand werden dabei vom zuletzt gespeicherten Teil übernommen beim Abarbeiten einer Kiste sind das vier Felder weniger pro Stück.

Einzeln Formular mit Foto, für spätere Nachträge.

Der Titel ist freiwillig. Bleibt er leer, steht dort automatisch Kategorie und Grösse, also zum Beispiel „Jacken 98/104". Wird die Grösse später korrigiert, ändert sich der Titel mit.

Reservieren

Besucher brauchen kein Konto. Nach dem Reservieren erscheint ein Link, mit dem sich die eigene Reservierung wieder aufheben lässt dieser Link sollte aufbewahrt werden. Ohne ihn kann nur der Betreiber die Reservierung lösen.

Der Name der reservierenden Person erscheint nicht in der Galerie, dort steht nur „reserviert". Sichtbar ist er ausschliesslich nach der Anmeldung.


Betrieb

Aus dem Internet erreichbar machen

Die App gehört hinter einen Reverse-Proxy, der HTTPS beendet. Wichtig dabei:

  • X-Forwarded-Proto und X-Forwarded-For weiterreichen. Ohne das erkennt die App nicht, dass der Browser verschlüsselt verbunden ist, und alle Rate-Limits zählen auf die Adresse des Proxys ein einzelner Besucher sperrte damit alle anderen aus.
  • COOKIE_HTTPS_ONLY auf "true" lassen.

Die Galerie ist frei zugänglich, wird aber per noindex und robots.txt von Suchmaschinen ferngehalten: sonst wären Fotos und Texte noch auffindbar, wenn die Börse längst abgeräumt ist.

Sicherung

Zwei Volumes enthalten alles, was nicht im Code steht:

Volume Inhalt
kleiderboerse-db Datenbank (/data)
kleiderboerse-bilder hochgeladene Fotos (/uploads)
docker run --rm -v kleiderboerse-db:/d -v "$PWD":/sicherung alpine \
    tar czf /sicherung/db.tar.gz -C /d .
docker run --rm -v kleiderboerse-bilder:/b -v "$PWD":/sicherung alpine \
    tar czf /sicherung/bilder.tar.gz -C /b .

Einstellungen

Alle über Umgebungsvariablen, keine im Image:

Variable Standard Bedeutung
ADMIN_PASSWORD Passwort fürs Erfassen. Leer = gesperrt.
ADMIN_PASSWORD_HASH Alternative: fertiger bcrypt-Hash. Hat Vorrang.
SECRET_KEY Signiert das Sitzungs-Cookie. Ohne festen Wert nach jedem Neustart abgemeldet.
COOKIE_HTTPS_ONLY true Sitzungs-Cookie nur über HTTPS.
RATE_LIMIT_RESERVE 5/hour Reservierungen pro IP.
RATE_LIMIT_LOGIN 10/hour Anmeldeversuche pro IP.
SESSION_IDLE_SECONDS 7200 Abmeldung nach Ruhezeit.
MAX_UPLOAD_BYTES 10485760 Grösse eines einzelnen Fotos.
DATABASE_PATH /data/kleiderboerse.sqlite Ort der Datenbank.
UPLOAD_PATH /uploads Ort der Fotos.

Entwickeln

cp .env-example .env        # dann ADMIN_PASSWORD darin anpassen
docker compose up -d --build

http://localhost:8090, API-Oberfläche unter /docs

Tests:

docker run --rm -v "$PWD":/app -w /app python:3.11-slim bash -c \
    'pip install -q -r requirements-dev.txt && python -m pytest -q'

Datenbank-Migration nach einer Modelländerung:

alembic revision --autogenerate -m "Beschreibung"

Aufbau

Ein Dienst, eine SQLite-Datei. FastAPI liefert sowohl die REST-API unter /api/v1/… als auch die Seiten aus; HTMX tauscht beim Filtern nur die Liste aus. Tailwind wird im Docker-Build über die Standalone-Binärdatei übersetzt kein Node.js nötig.

app/
├── main.py         Einstiegspunkt, Middleware, Sicherheits-Header
├── config.py       Einstellungen aus Umgebungsvariablen
├── models.py       Datenmodelle
├── crud.py         Datenbankzugriffe
├── security.py     Anmeldung, Sitzung, CSRF, Rate-Limits
├── images.py       Upload prüfen und nach WebP wandeln
├── routers/        API- und Seiten-Routen
└── templates/      Jinja2-Vorlagen

Fremde Dateien (HTMX, Swagger, Schriften) liegen bewusst lokal im Repository statt von einem CDN geladen zu werden: die Content-Security-Policy erlaubt nur script-src 'self', und ein CDN-Aufruf gäbe die IP-Adresse jedes Besuchers an einen Dritten weiter.

Veröffentlichen

Ein Versions-Tag löst den Bau des Images aus:

git tag -a v1.0.0 -m "Version 1.0.0  ..."
git push origin v1.0.0

Die CI baut, testet das fertige Image und stellt es erst danach in die Registry. latest wird nur bei echten Versions-Tags gesetzt. Ein Push auf main löst nichts aus.

S
Description
mini Webseite für alte zu kleine Kinder Kleider
Readme
774 KiB
2026-09-14 17:32:00 +02:00
Languages
JavaScript 90.2%
Python 7.8%
HTML 1.4%
Shell 0.3%
Dockerfile 0.2%