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
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
# Was nicht in den Build-Kontext gehört.
|
||||||
|
#
|
||||||
|
# Das Dockerfile kopiert ohnehin nur gezielt einzelne Verzeichnisse, es
|
||||||
|
# landet also nichts davon im Image. Aber der gesamte Kontext wird an den
|
||||||
|
# Docker-Daemon übertragen - und .env enthält das Admin-Passwort.
|
||||||
|
|
||||||
|
.env
|
||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.github
|
||||||
|
.claude
|
||||||
|
|
||||||
|
# Laufzeitdaten (im Betrieb Volumes)
|
||||||
|
data/
|
||||||
|
uploads/
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Entwicklung
|
||||||
|
tests/
|
||||||
|
.pytest_cache/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# Wird im Build erzeugt bzw. nur dort gebraucht
|
||||||
|
tailwindcss
|
||||||
|
Plan.md
|
||||||
|
projekt.md
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Vorlage für die lokale Entwicklung: als ".env" kopieren und anpassen.
|
||||||
|
# Die echte .env gehört nicht ins Repository (siehe .gitignore).
|
||||||
|
|
||||||
|
# Passwort fürs Erfassen. Leer = Erfassungsbereich gesperrt.
|
||||||
|
ADMIN_PASSWORD=bitte-eigenes-setzen
|
||||||
|
|
||||||
|
# Signiert das Sitzungs-Cookie. Erzeugen mit:
|
||||||
|
# python -c "import secrets; print(secrets.token_urlsafe(48))"
|
||||||
|
SECRET_KEY=nur-lokal-bitte-im-betrieb-ersetzen
|
||||||
|
|
||||||
|
# Nur fürs Testen über http://localhost. Im Betrieb hinter HTTPS: true
|
||||||
|
COOKIE_HTTPS_ONLY=false
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
name: Docker-Image bauen
|
||||||
|
|
||||||
|
# Baut das Image, sobald ein Versions-Tag (v1.2.3) gepusht wird, und legt es
|
||||||
|
# in der eingebauten Container-Registry von Gitea ab. Bewusst NICHT bei
|
||||||
|
# jedem Push auf main: so bleibt das Zusammenführen von Branches folgenlos
|
||||||
|
# und ein Release ist ein bewusster Schritt.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: gitea.boing86.myds.me
|
||||||
|
IMAGE_NAME: docker/kleiderboerse
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Quellcode auschecken
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
docker run --rm -v "$PWD":/app -w /app python:3.11-slim bash -c '
|
||||||
|
pip install --quiet -r requirements-dev.txt
|
||||||
|
python -m pytest -q
|
||||||
|
'
|
||||||
|
|
||||||
|
- name: Version bestimmen
|
||||||
|
id: meta
|
||||||
|
env:
|
||||||
|
# Bewusst über die Umgebung statt direkt als ${{ ... }} im Skript:
|
||||||
|
# Ausdrücke werden vor der Shell eingesetzt, und Tag-Namen dürfen
|
||||||
|
# Zeichen wie " oder $ enthalten. Als Variable kann daraus kein
|
||||||
|
# zusätzlicher Befehl werden.
|
||||||
|
REF_TYPE: ${{ github.ref_type }}
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
if [ "$REF_TYPE" = "tag" ]; then
|
||||||
|
VERSION="$REF_NAME"
|
||||||
|
VERSION="${VERSION#v}" # v1.2.3 -> 1.2.3
|
||||||
|
IS_RELEASE=true
|
||||||
|
else
|
||||||
|
VERSION="manual-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
IS_RELEASE=false
|
||||||
|
fi
|
||||||
|
# Die Registry behandelt Namen case-insensitiv -> konsequent klein.
|
||||||
|
VERSION="$(echo "$VERSION" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
|
||||||
|
# Nur bei echten Tags eine Version in die Fusszeile schreiben; bei
|
||||||
|
# manuellen Läufen bleibt sie leer und es erscheint der Commit.
|
||||||
|
if [ "$IS_RELEASE" = "true" ]; then
|
||||||
|
echo "tag_version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "tag_version=" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
echo "Version: $VERSION (Release: $IS_RELEASE)"
|
||||||
|
|
||||||
|
- name: Image bauen
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
docker build \
|
||||||
|
--tag "$REGISTRY/$IMAGE_NAME:${{ steps.meta.outputs.version }}" \
|
||||||
|
--build-arg "APP_VERSION=${{ steps.meta.outputs.tag_version }}" \
|
||||||
|
--build-arg "APP_REVISION=${{ github.sha }}" \
|
||||||
|
--label "org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}" \
|
||||||
|
--label "org.opencontainers.image.revision=${{ github.sha }}" \
|
||||||
|
--label "org.opencontainers.image.version=${{ steps.meta.outputs.version }}" \
|
||||||
|
.
|
||||||
|
|
||||||
|
# Prüft das fertige Image, bevor irgendetwas veröffentlicht wird.
|
||||||
|
# Geprüft wird im Container selbst (kein Port-Mapping nötig, damit der
|
||||||
|
# Test unabhängig vom Netzwerk-Setup des Runners funktioniert).
|
||||||
|
- name: Image testen
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
BILD="$REGISTRY/$IMAGE_NAME:${{ steps.meta.outputs.version }}"
|
||||||
|
CID=$(docker run -d -e ADMIN_PASSWORD=pruef -e SECRET_KEY=pruef "$BILD")
|
||||||
|
trap 'docker rm -f "$CID" >/dev/null 2>&1 || true' EXIT
|
||||||
|
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
# Einzeilig: ein mehrzeiliger String bräche den YAML-Block auf,
|
||||||
|
# weil dessen Fortsetzung links vom Einzug stünde.
|
||||||
|
if docker exec "$CID" python -c "import urllib.request as u; u.urlopen('http://localhost:8000/health')" 2>/dev/null; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$i" = "30" ]; then
|
||||||
|
echo "Anwendung im Container nicht erreichbar geworden:"
|
||||||
|
docker logs "$CID"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Stammdaten: ohne Kategorien und Grössen lässt sich nichts
|
||||||
|
# erfassen. Untergrenzen bewusst unter dem Ist-Stand, damit der
|
||||||
|
# Test nicht bei jeder Ergänzung anschlägt.
|
||||||
|
# -i ist zwingend: ohne angehängtes stdin bekommt "python -" ein
|
||||||
|
# leeres Programm, tut nichts und meldet Erfolg. Der Test könnte
|
||||||
|
# dann nie fehlschlagen.
|
||||||
|
docker exec -i "$CID" python - < scripts/image-pruefen.py
|
||||||
|
|
||||||
|
# Das CSS muss im Image liegen - es entsteht in einer eigenen
|
||||||
|
# Build-Stufe und würde bei einem Fehler dort still fehlen.
|
||||||
|
docker exec "$CID" test -s /app/app/static/style.css
|
||||||
|
echo "style.css vorhanden."
|
||||||
|
|
||||||
|
- name: An Registry anmelden
|
||||||
|
env:
|
||||||
|
# Optional: eigenes Token/Konto als Repo-Secret hinterlegen, falls
|
||||||
|
# der automatische GITEA_TOKEN nicht in die Registry schreiben darf.
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
AUTO_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
AKTEUR: ${{ github.actor }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
TOKEN="${REGISTRY_TOKEN:-$AUTO_TOKEN}"
|
||||||
|
BENUTZER="${REGISTRY_USER:-$AKTEUR}"
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "Kein Token verfügbar: REGISTRY_TOKEN als Repo-Secret hinterlegen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Anmeldung an $REGISTRY als $BENUTZER"
|
||||||
|
# --password-stdin: so steht das Token weder in der Prozessliste
|
||||||
|
# noch im Protokoll.
|
||||||
|
echo "$TOKEN" | docker login "$REGISTRY" -u "$BENUTZER" --password-stdin
|
||||||
|
|
||||||
|
- name: Image veröffentlichen
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
docker push "$REGISTRY/$IMAGE_NAME:${{ steps.meta.outputs.version }}"
|
||||||
|
|
||||||
|
# "latest" nur für echte Versions-Tags, nicht für manuelle Läufe.
|
||||||
|
if [ "${{ steps.meta.outputs.is_release }}" = "true" ]; then
|
||||||
|
docker tag "$REGISTRY/$IMAGE_NAME:${{ steps.meta.outputs.version }}" \
|
||||||
|
"$REGISTRY/$IMAGE_NAME:latest"
|
||||||
|
docker push "$REGISTRY/$IMAGE_NAME:latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Abmelden
|
||||||
|
if: always()
|
||||||
|
run: docker logout "$REGISTRY" || true
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Fertiges Image starten – ohne Quellcode, ohne selbst zu bauen.
|
||||||
|
#
|
||||||
|
# So geht's:
|
||||||
|
# 1. Diese Datei als "docker-compose.yml" in einen leeren Ordner legen
|
||||||
|
# 2. Unten ein eigenes Passwort und einen eigenen SECRET_KEY eintragen
|
||||||
|
# 3. Im Ordner ausführen: docker compose up -d
|
||||||
|
# 4. Im Browser öffnen: http://localhost:8090
|
||||||
|
#
|
||||||
|
# Beenden: docker compose down
|
||||||
|
# Aktualisieren: docker compose pull && docker compose up -d
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: gitea.boing86.myds.me/docker/kleiderboerse:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
ports:
|
||||||
|
# links der Port am eigenen Rechner, rechts der im Container.
|
||||||
|
# Ist 8090 schon belegt, einfach auf z.B. "8091:8000" ändern.
|
||||||
|
- "8090:8000"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
# Passwort fürs Erfassen. Solange hier nichts steht, ist der
|
||||||
|
# Erfassungsbereich gesperrt; Galerie und Reservieren funktionieren
|
||||||
|
# trotzdem. Bewusst leer ausgeliefert: ein mitgeliefertes Passwort
|
||||||
|
# stünde in dieser Anleitung und wäre damit allgemein bekannt.
|
||||||
|
ADMIN_PASSWORD: ""
|
||||||
|
|
||||||
|
# Signiert das Sitzungs-Cookie. Ohne festen Wert ist man nach jedem
|
||||||
|
# Neustart abgemeldet. Einen erzeugen mit:
|
||||||
|
# docker run --rm python:3.11-slim python -c \
|
||||||
|
# "import secrets; print(secrets.token_urlsafe(48))"
|
||||||
|
SECRET_KEY: ""
|
||||||
|
|
||||||
|
# Die App läuft hinter einem Reverse-Proxy mit HTTPS. Auf "false" nur
|
||||||
|
# zum Testen über http://localhost - sonst ginge das Sitzungs-Cookie
|
||||||
|
# auch über unverschlüsselte Verbindungen mit.
|
||||||
|
COOKIE_HTTPS_ONLY: "true"
|
||||||
|
|
||||||
|
# Reservieren ist ohne Anmeldung möglich. Diese Grenze ist damit das
|
||||||
|
# Einzige, was zwischen einem Skript und dem ganzen Bestand steht.
|
||||||
|
RATE_LIMIT_RESERVE: "5/hour"
|
||||||
|
RATE_LIMIT_LOGIN: "10/hour"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Hier liegen Datenbank und Bilder. Ohne diese beiden Zeilen wäre nach
|
||||||
|
# einem Neustart alles weg.
|
||||||
|
- kleiderboerse-db:/data
|
||||||
|
- kleiderboerse-bilder:/uploads
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
kleiderboerse-db:
|
||||||
|
kleiderboerse-bilder:
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""Prüft das fertige Image, bevor es veröffentlicht wird.
|
||||||
|
|
||||||
|
Wird von der CI in den Container gepipt:
|
||||||
|
docker exec -i "$CID" python - < scripts/image-pruefen.py
|
||||||
|
|
||||||
|
Bewusst eine eigene Datei statt eines Here-Dokuments im Workflow: ein
|
||||||
|
unindentierter Block bricht den YAML-Block auf, und als Datei lässt sich
|
||||||
|
das Ganze auch lokal gegen ein gebautes Image laufen lassen.
|
||||||
|
|
||||||
|
Die Untergrenzen liegen bewusst unter dem Ist-Stand, damit der Test nicht
|
||||||
|
bei jeder zusätzlichen Kategorie anschlägt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
BASIS = "http://localhost:8000"
|
||||||
|
|
||||||
|
|
||||||
|
def hole(pfad: str):
|
||||||
|
with urllib.request.urlopen(BASIS + pfad) as antwort:
|
||||||
|
return json.load(antwort)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
kategorien = hole("/api/v1/categories")
|
||||||
|
groessen = hole("/api/v1/sizes")
|
||||||
|
print(f"Kategorien: {len(kategorien)}, Grössen: {len(groessen)}")
|
||||||
|
assert len(kategorien) >= 5, "zu wenige Kategorien - Stammdaten fehlen?"
|
||||||
|
assert len(groessen) >= 20, "zu wenige Grössen - Stammdaten fehlen?"
|
||||||
|
|
||||||
|
with urllib.request.urlopen(BASIS + "/") as antwort:
|
||||||
|
seite = antwort.read().decode()
|
||||||
|
assert "Kleider-Börse" in seite, "Galerie wird nicht ausgeliefert"
|
||||||
|
# Das CSS entsteht in einer eigenen Build-Stufe und würde bei einem
|
||||||
|
# Fehler dort still fehlen - die Seite käme dann ohne Gestaltung.
|
||||||
|
assert "/static/style.css" in seite, "kein CSS eingebunden"
|
||||||
|
print("Galerie und CSS: ok")
|
||||||
|
|
||||||
|
# Ohne Anmeldung darf nichts geschrieben werden.
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(urllib.request.Request(
|
||||||
|
BASIS + "/api/v1/items", method="POST",
|
||||||
|
data=b'{"size":"80"}',
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
))
|
||||||
|
except urllib.error.HTTPError as fehler:
|
||||||
|
assert fehler.code == 401, f"ergab {fehler.code}, erwartet 401"
|
||||||
|
else:
|
||||||
|
sys.exit("Schreiben war ohne Anmeldung möglich!")
|
||||||
|
print("Schreibschutz greift.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user