Projektbeschrieb und Umsetzungsplan
projekt.md beschreibt die Kinderkleider-Börse (FastAPI, SQLite, Bild-Upload mit WebP-Konvertierung). Plan.md ergänzt die dort offenen Entscheidungen: Frontend-Variante A (Jinja2 + HTMX, kein Node.js), Erfassen hinter Passwort, Reservieren ohne Anmeldung, erreichbar aus dem Internet. Aus dieser Kombination folgt der Schwerpunkt des Plans - ein offener, zustandsändernder Endpunkt im Internet und ein Bild-Upload sind die beiden Angriffsflächen, die den Ausschlag geben. Noch kein Code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
# Zugangsdaten - gehören nie ins Repository
|
||||
.env
|
||||
|
||||
# Laufzeitdaten: liegen im Betrieb in Docker-Volumes, nicht im Code
|
||||
data/
|
||||
uploads/
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.db
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
|
||||
# Erzeugtes CSS (entsteht beim Docker-Build aus der Tailwind-CLI)
|
||||
app/static/style.css
|
||||
tailwindcss
|
||||
|
||||
# Editor / Betriebssystem
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,229 @@
|
||||
# Umsetzungsplan – Kinderkleider-Börse
|
||||
|
||||
Grundlage: `projekt.md`. Dieser Plan ergänzt sie um die Entscheidungen, die dort
|
||||
offen waren, und um die Punkte, die durch den Internet-Zugang dazukommen.
|
||||
|
||||
## Getroffene Entscheidungen
|
||||
|
||||
| Frage | Entscheidung |
|
||||
|---|---|
|
||||
| Frontend | **Variante A** – Jinja2 + HTMX + Tailwind, ein einziger Dienst, kein Node.js |
|
||||
| Erfassen/Bearbeiten/Löschen | Hinter **einem Passwort** |
|
||||
| Reservieren | **Ohne Anmeldung** |
|
||||
| Erreichbarkeit | **Aus dem Internet**, per Reverse-Proxy mit HTTPS |
|
||||
|
||||
Die letzten beiden zusammen sind der eigentliche Knackpunkt dieses Projekts:
|
||||
ein Endpunkt, den jeder im Internet ohne Anmeldung auslösen kann, der den
|
||||
Zustand ändert. Alles unter „Absicherung" ergibt sich daraus.
|
||||
|
||||
---
|
||||
|
||||
## Architektur
|
||||
|
||||
Ein Container, ein Prozess (`uvicorn`), eine SQLite-Datei. FastAPI liefert
|
||||
sowohl die REST-API (`/api/v1/...`) als auch die gerenderten Seiten aus –
|
||||
dieselbe Anwendung, dieselben CRUD-Funktionen. HTMX ersetzt beim Filtern und
|
||||
Reservieren nur Teilbereiche der Seite, statt eine SPA zu bauen.
|
||||
|
||||
**Tailwind ohne Node.js:** nicht per CDN (im Betrieb nicht vorgesehen und wäre
|
||||
eine Fremdverbindung), sondern über die **Standalone-CLI** – eine einzelne
|
||||
Binärdatei, die im Docker-Build einmalig ein fertiges `style.css` erzeugt. Im
|
||||
Image landet nur das fertige CSS.
|
||||
|
||||
**Persistenz:** ein Volume für die Datenbank, ein zweites für die Bilder. Beide
|
||||
ausserhalb des ausgelieferten Codes.
|
||||
|
||||
### Projektstruktur
|
||||
|
||||
Die Struktur in `projekt.md` ist an einer Stelle verrutscht (die Module stehen
|
||||
auf derselben Ebene wie `app/`, `static/` und `templates/` hängen an einer
|
||||
Ebene, die es nicht gibt). Vorschlag:
|
||||
|
||||
```text
|
||||
kleiderboerse/
|
||||
├── app/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # FastAPI-App, Startup, Router einhängen
|
||||
│ ├── config.py # Einstellungen aus Umgebungsvariablen
|
||||
│ ├── database.py # Engine & Session
|
||||
│ ├── models.py # SQLAlchemy-Modelle
|
||||
│ ├── schemas.py # Pydantic-Schemas
|
||||
│ ├── crud.py # Datenbankzugriffe
|
||||
│ ├── security.py # Passwort, Sitzung, CSRF, Rate-Limit
|
||||
│ ├── images.py # Upload-Prüfung, WebP-Konvertierung
|
||||
│ ├── routers/
|
||||
│ │ ├── items.py # /api/v1/items
|
||||
│ │ ├── categories.py # /api/v1/categories, /sizes
|
||||
│ │ └── pages.py # HTML-Seiten (Galerie, Detail, Erfassen)
|
||||
│ ├── static/ # erzeugtes CSS, Icons (Bilder NICHT hier)
|
||||
│ └── templates/
|
||||
├── alembic/ # Migrationen
|
||||
├── data/ # Volume: kleiderboerse.sqlite
|
||||
├── uploads/ # Volume: hochgeladene Bilder
|
||||
├── tests/
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
├── requirements.txt
|
||||
└── README.md
|
||||
```
|
||||
|
||||
Hochgeladene Bilder liegen **nicht** unter `static/`, sondern in einem eigenen
|
||||
Verzeichnis, das über eine eigene Route ausgeliefert wird. Sonst wäre jede
|
||||
hochgeladene Datei unter ihrem Namen direkt abrufbar – und ein als Bild
|
||||
getarntes Skript ebenfalls.
|
||||
|
||||
---
|
||||
|
||||
## Datenmodell – Abweichungen von `projekt.md`
|
||||
|
||||
Das Schema wird grundsätzlich übernommen. Vier Ergänzungen:
|
||||
|
||||
1. **Feste Wertelisten statt freier VARCHAR.** `gender`, `season`, `condition`
|
||||
und `status` bekommen ein Python-Enum plus `CHECK`-Bedingung in der
|
||||
Datenbank. Ohne das schleicht sich über die API früher oder später
|
||||
`"Gril"` oder `"reserviert"` ein, und die Filter greifen dann still nicht
|
||||
mehr.
|
||||
|
||||
2. **`reservation_token` (neu) in `items`.** Zufälliger Wert, der beim
|
||||
Reservieren erzeugt wird. Nur wer ihn hat, kann die eigene Reservierung
|
||||
wieder aufheben. Begründung unten unter „Offene Punkte".
|
||||
|
||||
3. **`sizes` als eigene Tabelle.** `projekt.md` sieht `GET /api/v1/sizes` vor,
|
||||
aber keine Tabelle dazu. Grössen aus den vorhandenen Einträgen zu
|
||||
destillieren (`SELECT DISTINCT size`) klingt bequem, sortiert aber falsch
|
||||
(`98/104` vor `24`) und liefert Tippfehler gleich mit als Filteroption.
|
||||
Darum eine gepflegte Liste mit `sort_order`.
|
||||
|
||||
4. **`updated_at`** wird in der Anwendung gesetzt, nicht per DB-Trigger –
|
||||
bleibt so unabhängig von SQLite-Eigenheiten.
|
||||
|
||||
Ausserdem: beim Löschen eines Eintrags müssen die **Bilddateien mitgelöscht**
|
||||
werden, sonst wächst das Upload-Verzeichnis unbegrenzt mit verwaisten Dateien.
|
||||
`ON DELETE CASCADE` räumt nur die Datenbankzeilen weg, nicht die Dateien.
|
||||
|
||||
---
|
||||
|
||||
## Absicherung
|
||||
|
||||
Die App ist aus dem Internet erreichbar, also gelten hier andere Massstäbe als
|
||||
bei einem Heimnetz-Dienst. Punkte 1 und 2 sind die wichtigsten.
|
||||
|
||||
### 1. Bild-Upload – die grösste Angriffsfläche
|
||||
|
||||
- Dateiname **nie** aus dem Upload übernehmen, sondern selbst erzeugen
|
||||
(UUID + `.webp`). Damit ist Pfad-Manipulation ausgeschlossen.
|
||||
- Vor dem Öffnen: Grösse begrenzen (z.B. 10 MB) und Inhaltstyp prüfen.
|
||||
- `Image.MAX_IMAGE_PIXELS` setzen – sonst kann ein wenige Kilobyte grosses
|
||||
PNG beim Entpacken den Arbeitsspeicher füllen („Dekompressionsbombe").
|
||||
- Nach dem Konvertieren nach WebP wird **nur das Ergebnis** gespeichert, nie
|
||||
das Original. Das entfernt eingebettete Fremdinhalte und die EXIF-Daten –
|
||||
letztere enthalten bei Handyfotos oft die **GPS-Koordinaten der Wohnung**.
|
||||
- Ausliefern mit festem `Content-Type: image/webp` und
|
||||
`X-Content-Type-Options: nosniff`.
|
||||
|
||||
### 2. Reservieren ohne Anmeldung
|
||||
|
||||
Ein offener, zustandsändernder Endpunkt im Internet. Ohne Bremse kann ein
|
||||
Skript in Sekunden alles reservieren oder `reserved_by` als Werbefläche
|
||||
missbrauchen.
|
||||
|
||||
- **Rate-Limit pro IP** (`slowapi`), z.B. 5 Reservierungen pro Stunde.
|
||||
- `reserved_by` begrenzen (Länge, keine URLs) und beim Anzeigen maskieren.
|
||||
Jinja2 maskiert von sich aus – entscheidend ist, `|safe` dort nirgends zu
|
||||
verwenden.
|
||||
- Reservierungen sind **jederzeit vom Betreiber aufhebbar** (Admin-Ansicht),
|
||||
damit eine Missbrauchswelle in einem Rutsch aufgeräumt werden kann.
|
||||
|
||||
### 3. Anmeldung fürs Erfassen
|
||||
|
||||
- Passwort als **bcrypt-Hash** aus einer Umgebungsvariablen. Kein
|
||||
Standardpasswort mitliefern: ohne gesetzten Wert ist der Bereich gesperrt,
|
||||
nicht offen.
|
||||
- Hash **einmal** beim Start berechnen, nicht pro Anfrage.
|
||||
- Sitzungs-Cookie `HttpOnly`, `SameSite=Lax`, `Secure`.
|
||||
- **`Secure` muss `X-Forwarded-Proto` auswerten**, nicht nur die direkte
|
||||
Verbindung – hinter einem TLS-Reverse-Proxy sieht die App sonst „HTTP" und
|
||||
lässt das Cookie unverschlüsselt mitgehen.
|
||||
- Rate-Limit auch auf die Anmeldung, plus Sitzungs-Timeout.
|
||||
- CSRF-Token für alle ändernden Formulare.
|
||||
|
||||
### 4. Allgemein
|
||||
|
||||
- `Content-Security-Policy` mit `script-src 'self'` (HTMX wird lokal
|
||||
ausgeliefert, nicht per CDN), dazu `nosniff` und `frame-ancestors 'none'`.
|
||||
- HTTPS erzwingen; `uvicorn` mit `--proxy-headers` und gesetzten
|
||||
`forwarded-allow-ips`.
|
||||
- Fehlermeldungen ohne interne Details; Stacktraces nur ins Log.
|
||||
- Pagination auf `GET /api/v1/items` – ohne Begrenzung liefert der Endpunkt
|
||||
irgendwann alle Einträge samt Bildpfaden in einer Antwort.
|
||||
|
||||
### 5. Personendaten
|
||||
|
||||
`reserved_by` ist ein Personenname auf einer öffentlich erreichbaren Seite.
|
||||
Vorschlag: auf der Galerie nur „reserviert" anzeigen, den Namen ausschliesslich
|
||||
in der Admin-Ansicht. Zusätzlich beim Erledigen (`mark-given`) den Namen
|
||||
löschen. Das ist datensparsam und macht die Seite gleichzeitig unattraktiver
|
||||
für Spam.
|
||||
|
||||
---
|
||||
|
||||
## Meilensteine
|
||||
|
||||
Die Phasen aus `projekt.md`, ergänzt um Tests und die Absicherung.
|
||||
|
||||
**Phase 1 – Fundament**
|
||||
- Projektgerüst, `requirements.txt`, Einstellungen aus Umgebungsvariablen
|
||||
- SQLAlchemy-Modelle, Alembic-Erstmigration, Kategorien und Grössen vorbefüllen
|
||||
- CRUD-Schicht mit Tests
|
||||
- `GET`/`POST`/`PATCH`/`DELETE /api/v1/items`, `GET /api/v1/categories`, `/sizes`
|
||||
- Prüfpunkt: über Swagger UI ein Kleidungsstück anlegen, filtern, ändern, löschen
|
||||
|
||||
**Phase 2 – Bilder**
|
||||
- Upload per `multipart/form-data`, mehrere Bilder pro Eintrag
|
||||
- Prüfung und WebP-Konvertierung (max. 1200 px Breite) nach Abschnitt 1 oben
|
||||
- Ausliefer-Route, `is_primary`-Logik, Aufräumen beim Löschen
|
||||
- Prüfpunkt: bewusst fehlerhafte Uploads (zu gross, falscher Typ, riesige
|
||||
Pixelmasse, `.php` als `.jpg` getarnt) werden alle abgewiesen
|
||||
|
||||
**Phase 3 – Absicherung**
|
||||
- Anmeldung, Sitzung, CSRF, Rate-Limits, Sicherheits-Header
|
||||
- Prüfpunkt: ohne Anmeldung ist kein Schreibzugriff möglich; ohne gesetztes
|
||||
Passwort ist der Erfassungsbereich gesperrt
|
||||
|
||||
**Phase 4 – Oberfläche**
|
||||
- Mobile-First-Layout, Galerie mit Filterleiste (HTMX)
|
||||
- Detailseite, Erfassungsformular mit Kamera-Aufnahme
|
||||
(`<input type="file" accept="image/*" capture="environment">`)
|
||||
- Reservierungs-Ablauf inklusive Selbst-Freigabe
|
||||
- Prüfpunkt: der ganze Weg auf einem echten Smartphone
|
||||
|
||||
**Phase 5 – Betrieb**
|
||||
- `Dockerfile` (Tailwind-Build inbegriffen), `docker-compose.yml`, Volumes
|
||||
- Healthcheck, Backup-Hinweise für Datenbank und Bilder
|
||||
- README für Einrichtung und Reverse-Proxy
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
1. **Wer darf eine Reservierung wieder aufheben?** `projekt.md` sieht
|
||||
`POST /items/{id}/release` vor, sagt aber nicht, wer ihn aufrufen darf. Ist
|
||||
er offen, kann jeder die Reservierung eines anderen löschen. Vorschlag: beim
|
||||
Reservieren wird ein Token vergeben und als Link angezeigt („Reservierung
|
||||
aufheben") – wer ihn hat, kann die eigene zurücknehmen; der Betreiber kann
|
||||
es ohnehin. Bitte bestätigen, dann kommt `reservation_token` ins Modell.
|
||||
|
||||
2. **Soll die Galerie überhaupt öffentlich sein?** Auch ohne Schreibzugriff
|
||||
zeigt sie Kinderkleidung, Namen und indirekt den Wohnort. Ein einfacher
|
||||
Zugangscode für die ganze Seite (einmal eingeben, bleibt im Cookie) würde
|
||||
Suchmaschinen und Zufallsbesucher aussperren, ohne dass jemand ein Konto
|
||||
braucht. Empfehlung: ja, zusätzlich zu `noindex`. Deine Entscheidung.
|
||||
|
||||
3. **Benachrichtigung bei Reservierung?** Nicht in `projekt.md`. Ohne sie musst
|
||||
du selbst nachschauen. Ein einfacher Weg wäre eine Nachricht per E-Mail oder
|
||||
Telegram. Kann auch später kommen – dann aber besser gleich als eigener
|
||||
Meilenstein statt nachträglich eingeschoben.
|
||||
|
||||
4. **Mehrere Kinder / Grössenverläufe?** Aktuell ist alles ein flacher Bestand.
|
||||
Falls du später nach „von wem" oder „Jahrgang" filtern willst, wäre jetzt
|
||||
der günstige Zeitpunkt für ein zusätzliches Feld.
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
# 📦 Kinderkleider-Börse (Web-App)
|
||||
|
||||
Eine schlanke, mobile-optimierte Web-App zum Katalogisieren, Präsentieren und Reservieren von zu klein gewordener Kinderkleidung für Familie, Freunde und Nachbarn.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Zielsetzungs & Kern-Features
|
||||
|
||||
- **Schnelles Erfassen (Mobile First):** Foto direkt mit dem Smartphone aufnehmen, Grösse & Kategorie wählen, fertig.
|
||||
- **Übersichtliche Galerie & Filter:** Schnelle Filterung nach Konfektionsgrösse, Kategorie, Geschlecht und Saison.
|
||||
- **Einfaches Reservierungssystem:** Interessierte können Kleidungsstücke mit einem Klick reservieren.
|
||||
- **Bildoptimierung:** Automatische Skalierung und WebP-Komprimierung beim Upload, um Speicherplatz und Bandbreite zu sparen.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Technologiestack (Python-basiert)
|
||||
|
||||
- **Backend:** Python 3.11+ mit [FastAPI](https://fastapi.tiangolo.com/) (hohe Performance, automatische OpenAPI/Swagger-Dokumentation)
|
||||
- **Datenbank & ORM:** SQLite mit [SQLAlchemy](https://www.sqlalchemy.org/) & [Alembic](https://alembic.sqlalchemy.org/) (Migrations)
|
||||
- **Bildverarbeitung:** Pillow (PIL) für WebP-Konvertierung und Resizing
|
||||
- **Frontend (Optionen):**
|
||||
- *Variante A (Single File / Monolith):* Jinja2 Templates + HTMX + Tailwind CSS (sehr schnell zu entwickeln, kein Node.js Build-Step nötig)
|
||||
- *Variante B (Decoupled):* Vue.js / React SPA (greift auf die REST-API zu)
|
||||
- **Deployment:** Docker & Docker Compose (leicht auf Unraid / Server auszuführen)
|
||||
|
||||
---
|
||||
|
||||
## 🗄 Datenbank-Schema
|
||||
|
||||
### 1. `categories`
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER (PK) | Eindeutige ID |
|
||||
| `name` | VARCHAR(50) | Name (z. B. Hosen, Jacken, Schuhe, Pullover) |
|
||||
| `slug` | VARCHAR(50) | URL-freundlicher Name |
|
||||
|
||||
### 2. `items` (Haupttabelle)
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER / UUID (PK) | Eindeutige ID |
|
||||
| `title` | VARCHAR(100) | Kurzer Titel (z. B. "Warme Winterjacke rot") |
|
||||
| `description` | TEXT | Optionale Details oder Mängel |
|
||||
| `size` | VARCHAR(20) | Konfektionsgrösse (z. B. `98/104`, `24`, `80`) |
|
||||
| `gender` | VARCHAR(10) | `boy`, `girl`, `unisex` |
|
||||
| `season` | VARCHAR(15) | `spring_summer`, `autumn_winter`, `all_year` |
|
||||
| `condition` | VARCHAR(20) | `new`, `very_good`, `good`, `worn` |
|
||||
| `status` | VARCHAR(20) | `available`, `reserved`, `given_away` |
|
||||
| `category_id` | FK -> `categories.id` | Zuordnung zur Kategorie |
|
||||
| `reserved_by` | VARCHAR(100) | Name/Kontakt der Person, die es reserviert hat |
|
||||
| `created_at` | TIMESTAMP | Erstellungsdatum |
|
||||
| `updated_at` | TIMESTAMP | Letztes Update |
|
||||
|
||||
### 3. `item_images`
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER / UUID (PK) | Eindeutige ID |
|
||||
| `item_id` | FK -> `items.id` | Referenz zum Kleidungsstück |
|
||||
| `image_url` | VARCHAR(255) | Pfad zur Bilddatei (z. B. `/static/uploads/img_1234.webp`) |
|
||||
| `is_primary` | BOOLEAN | Hauptbild für Galerie-Vorschau |
|
||||
|
||||
---
|
||||
|
||||
## 🔌 API-Endpunkte (FastAPI REST)
|
||||
|
||||
### Kleidung verwalten & durchsuchen
|
||||
- `GET /api/v1/items`
|
||||
- **Query-Params:** `?size=98/104&category_id=2&status=available&gender=unisex`
|
||||
- **Response:** Liste gefilterter Kleidungsstücke mit Primärbild.
|
||||
- `GET /api/v1/items/{id}`
|
||||
- **Response:** Einzelnes Kleidungsstück inklusive aller Bilder und Details.
|
||||
- `POST /api/v1/items`
|
||||
- **Content-Type:** `multipart/form-data`
|
||||
- **Body:** JSON-Daten + Bild-Uploads.
|
||||
- `PATCH /api/v1/items/{id}`
|
||||
- **Body:** Partial Update für Status, Titel, Beschreibung etc.
|
||||
- `DELETE /api/v1/items/{id}`
|
||||
- **Description:** Kleidungsstück und zugehörige Bilder löschen.
|
||||
|
||||
### Reservierung & Status
|
||||
- `POST /api/v1/items/{id}/reserve`
|
||||
- **Body:** `{"reserved_by": "Familie Meier"}`
|
||||
- `POST /api/v1/items/{id}/release`
|
||||
- Setzt Status zurück auf `available` und löscht `reserved_by`.
|
||||
- `POST /api/v1/items/{id}/mark-given`
|
||||
- Setzt Status auf `given_away`.
|
||||
|
||||
### Stammdaten
|
||||
- `GET /api/v1/categories`
|
||||
- `GET /api/v1/sizes`
|
||||
|
||||
---
|
||||
|
||||
## 📂 Empfohlene Projekt-Struktur
|
||||
|
||||
```text
|
||||
kinderkleider-app/
|
||||
├── app/
|
||||
├── database.py # DB-Engine & Session Setup
|
||||
├── models.py # SQLAlchemy Modelle
|
||||
├── schemas.py # Pydantic Schemas (Request/Response validation)
|
||||
├── crud.py # Datenbank-Zugriffslogik
|
||||
├── main.py # FastAPI App Entrypoint & Routen
|
||||
├── utils.py # Bild-Komprimierung & Helper
|
||||
│ ├── static/ # Uploaded Images & CSS
|
||||
│ └── templates/ # Jinja2 Templates (falls HTML direkt gerendert wird)
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
├── requirements.txt
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Meilensteine / Roadmap
|
||||
|
||||
- [ ] **Phase 1: Backend Basic MVP**
|
||||
- [ ] FastAPI Setup mit SQLite & SQLAlchemy
|
||||
- [ ] Datenmodelle & Migrationen anlegen
|
||||
- [ ] CRUD Endpunkte für `items` und `categories` testen (Swagger UI)
|
||||
- [ ] **Phase 2: Bild-Handling & Speicher**
|
||||
- [ ] `multipart/form-data` Upload via FastAPI
|
||||
- [ ] Pillow-Integration zur Konvertierung aller Uploads nach WebP (max. 1200px Breite)
|
||||
- [ ] **Phase 3: Frontend Setup**
|
||||
- [ ] Mobile-First Layout mit Tailwind CSS
|
||||
- [ ] Filterleiste für Grösse, Kategorie & Status
|
||||
- [ ] Reservierungs-Modal / Button
|
||||
- [ ] **Phase 4: Containerisierung & Deployment**
|
||||
- [ ] `Dockerfile` & `docker-compose.yml` schreiben
|
||||
- [ ] Persistence per Volume-Mount für Datenbank & Bild-Uploads
|
||||
Reference in New Issue
Block a user