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
183 lines
5.8 KiB
Markdown
183 lines
5.8 KiB
Markdown
# 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`](docker-compose.betrieb.yml) hineinkopieren.
|
||
|
||
**3.** Zwei Werte eintragen:
|
||
|
||
```yaml
|
||
ADMIN_PASSWORD: "dein-passwort"
|
||
SECRET_KEY: "..."
|
||
```
|
||
|
||
Den `SECRET_KEY` erzeugen:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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`) |
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
cp .env-example .env # dann ADMIN_PASSWORD darin anpassen
|
||
docker compose up -d --build
|
||
```
|
||
|
||
→ <http://localhost:8090>, API-Oberfläche unter `/docs`
|
||
|
||
Tests:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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.
|