Docker-Containerisierung der App (Flask + SQLite, kein Rewrite)

Dockerfile, docker-compose.yml, requirements.txt und .env (gitignored) fuer
pflegepilot_alpha_v2/pflegepilot ergaenzt. Ein App-Service ueber Gunicorn,
Isolation der Daten ueber zwei benannte Volumes (db, static) statt eines
separaten DB-Containers, da SQLite eingebettet ist. Secret-Key kommt jetzt
aus der Umgebungsvariable SECRET_KEY (lokaler Fallback bleibt fuer
"python start.py" ohne Docker erhalten), Host-Port und Worker-Anzahl sind
ueber .env konfigurierbar. Bewusst keine Domain hinterlegt, da die
DynDNS-Domain erst spaeter auf die Testbench gelegt wird.

Dabei einen Bug in der DB-Migrationsreihenfolge gefunden und behoben: die
nutzer-Tabelle wurde zu spaet angelegt und liess auf einer komplett leeren
Datenbank (z.B. frisches Docker-Volume) mehrere Spalten fehlen, was den
Demo-Konto-Bootstrap zum Absturz brachte.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-07-18 18:25:10 +02:00
1 parent 6bfb7a07b6
commit 960bcd6dbf
8 files changed
+99 -15

No files matched your search

+3
View File
@@ -14,5 +14,8 @@ pflegepilot_alpha_v2/pflegepilot/static/fotos/
.claude/
pflegepilot_alpha_v2/pflegepilot/.claude/settings.local.json
# Docker secrets - never commit, created per-deployment (see Dockerfile/docker-compose.yml)
pflegepilot_alpha_v2/pflegepilot/.env
# graphify output - regeneratable
graphify-out/
+18
View File
@@ -27,6 +27,24 @@ python start.py # initializes/migrates the SQLite DB, then serves on http
Default login after first start: `admin` / `admin` (created automatically by `init_db()` when the `nutzer` table is empty).
## Docker deployment
The app also runs containerized, as-is (Flask + SQLite, no rewrite) — `Dockerfile`, `docker-compose.yml`, `.dockerignore`, `requirements.txt`, and `.env` all live in `pflegepilot_alpha_v2/pflegepilot/`, alongside the app itself. This is an additional deployment path, not a replacement for local dev — `python start.py` still works unchanged.
```bash
cd pflegepilot_alpha_v2/pflegepilot
docker compose build
docker compose up -d # serves on http://localhost:${APP_PORT:-5050}
docker compose logs -f app
docker compose down # add -v to also wipe the db/static volumes
```
- **Single app service, no separate DB container**: SQLite is an embedded, file-based engine, not a networked service — there is nothing to containerize separately without swapping to Postgres, which would be a rewrite and is explicitly out of scope for this task (that's the separate, not-yet-planned `open.md` item 4). "Isolation between app and data" is instead achieved with two **named Docker volumes** — `pflegepilot_db` (mounted at `/app/db`) and `pflegepilot_static` (mounted at `/app/static`) — so patient data outlives any `docker compose build`/`up` cycle of the app container and can be backed up or moved independently of the image.
- **Runs on Gunicorn, not the Flask dev server**: the container `CMD` first runs `init_db()` once as a plain `python -c` step (idempotent — see the schema-evolution pattern above, safe to re-run on every container start), then execs `gunicorn` bound to `0.0.0.0:5050` inside the container. Worker count is `GUNICORN_WORKERS` (env var, default 2).
- **Secrets and config live in `.env`** (real file, not `.env.example` — solo project, no template needed), gitignored, and referenced by `docker-compose.yml` via `env_file`. Currently holds: `SECRET_KEY` (random 64-hex-char value, generated once with `python -c "import secrets; print(secrets.token_hex(32))"` — `app.secret_key` in `app.py` reads `os.environ.get("SECRET_KEY", "pflegepilot-alpha-2025")`, so local `python start.py` still works with zero setup via the hardcoded fallback, while Docker always overrides it with a real secret), `APP_PORT` (host-side port mapping, default 5050), `GUNICORN_WORKERS`. Never commit `.env` or paste its contents anywhere.
- **No domain/URL baked in anywhere, intentionally**: the container only knows about its internal port (`5050`, fixed) and the host port it's mapped to (`APP_PORT`, configurable). Whatever hostname eventually points at that port — a DynDNS domain forwarded to the testbench, or anything else later — needs zero code or `.env` changes here; the app doesn't care what hostname it's reached through. The one thing to revisit *when* a real domain + reverse proxy with TLS is added in front of this later: session cookies aren't currently marked `Secure` (fine for a plain-HTTP testbench today), so add `SESSION_COOKIE_SECURE=True` and proxy-header trust (`ProxyFix`) at that point, not before.
- **Bug found and fixed while adding this**: the `nutzer` table isn't in `schema.sql` (it was added later, purely via the `init_db()` migration list) — it used to be created near the *end* of that list, after several `ALTER TABLE nutzer ADD COLUMN ...` entries earlier in the same list. On any database that already had a `nutzer` table this was invisible (the ALTERs just worked normally), but on a genuinely fresh database — e.g. the first boot against a brand-new Docker volume — the ALTERs ran before the table existed, failed, and were silently swallowed by the migration loop's `except: pass`, leaving `nutzer` permanently missing `abo_bis`/`abo_typ`/`ist_demo` and crashing the demo-account bootstrap insert. Fixed by moving `CREATE TABLE IF NOT EXISTS nutzer` to the very top of the migration list with the full current column set included directly, so table creation always precedes anything that depends on it, regardless of whether the DB is fresh or pre-existing. Verified by tearing down the Docker volumes (`docker compose down -v`) and rebuilding from nothing — the app now boots clean.
## Architecture
**Single-file Flask backend, vanilla-JS SPA frontend, zero build tooling.**
+6 -5
View File
@@ -9,11 +9,12 @@ Reihenfolge unten nach Abhängigkeit/Aufwand sortiert (nicht nach ursprüngliche
- ~~Demo-Datenbestand setzt sich automatisch alle 6 Stunden zurück~~
- Umsetzung: `nutzer.ist_demo`-Konto "demo"/"demo", "Demo ausprobieren"-Button auf dem Login-Screen, "Demo-Modus"-Badge in der Topbar, vorausgefüllte fiktive Patientin "Grete Musterfrau" (Antrag, 2 Leistungen, 2 Aufgaben, Pflegetagebuch), Lazy-Reset alle 6h bei Login/Request (kein Cron/Thread nötig), E-Mail-Versand im Demo-Konto gesperrt (403), Demo-Konto von der Abo-Zahlungspflicht ausgenommen. Details siehe CLAUDE.md.
- [ ] **2. Docker-Containerisierung der aktuellen App (Flask + SQLite, kein Rewrite)**
- App als Docker-Image/Compose-Setup lauffähig machen, so wie sie heute ist
- Isolierung zwischen App-Container und Datenbank (getrennte Services/Volumes)
- Keine Secrets/Env-Werte in der docker-compose.yml selbst — eigene .env-Datei dafür (keine .env.example nötig, Solo-Projekt)
- Ports müssen über Variablen konfigurierbar sein, nicht hart codiert
- [x] ~~**2. Docker-Containerisierung der aktuellen App (Flask + SQLite, kein Rewrite)**~~ *(erledigt)*
- ~~App als Docker-Image/Compose-Setup lauffähig machen, so wie sie heute ist~~
- ~~Isolierung zwischen App-Container und Datenbank (getrennte Services/Volumes)~~
- ~~Keine Secrets/Env-Werte in der docker-compose.yml selbst — eigene .env-Datei dafür (keine .env.example nötig, Solo-Projekt)~~
- ~~Ports müssen über Variablen konfigurierbar sein, nicht hart codiert~~
- Umsetzung: `Dockerfile` + `docker-compose.yml` + `.env` (gitignored) + `requirements.txt` in `pflegepilot_alpha_v2/pflegepilot/`. Ein Service (SQLite ist eingebettet, kein eigener DB-Server nötig/sinnvoll ohne Postgres-Rewrite); Isolation stattdessen über zwei benannte Docker-Volumes (`pflegepilot_db`, `pflegepilot_static`), die App-Container-Rebuilds überleben. `.env` enthält `SECRET_KEY` (zufällig generiert), `APP_PORT` (Host-Port, Default 5050), `GUNICORN_WORKERS` — läuft produktiv über Gunicorn statt Flask-Dev-Server. Bewusst keine Domain im Code/`.env` hinterlegt, da die DynDNS-Domain erst nachträglich auf die Testbench gelegt wird — der Container kennt nur seinen Port. Nebenbei einen echten Bug in der DB-Migrationsreihenfolge gefunden und behoben (`nutzer`-Tabelle wurde zu spät angelegt, fiel nur auf einer wirklich leeren DB auf — z.B. frisches Docker-Volume). Details siehe CLAUDE.md, Abschnitt "Docker deployment". Getestet: Build, Start, Login (admin+demo), Neustart mit Datenpersistenz über die Volumes, sauberer Boot auf komplett leerer DB.
- [ ] **3. Update-Benachrichtigung / Changelog-System**
- Nach dem ersten Login nach einem Update: kurzer anklickbarer Hinweis, was sich geändert hat
@@ -0,0 +1,16 @@
__pycache__/
*.pyc
.claude/
db/*.db
static/fotos/*
static/briefe/*
static/dokumente/*
static/logo/*
!static/fotos/.gitkeep
!static/briefe/.gitkeep
!static/dokumente/.gitkeep
!static/logo/.gitkeep
.env
.git/
.gitignore
README.md
@@ -0,0 +1,21 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py katalog.py krankenkassen.py start.py ./
COPY db/schema.sql db/schema.sql
COPY templates/ templates/
RUN useradd -m -u 1000 pflegepilot \
&& mkdir -p db static/fotos static/briefe static/dokumente static/logo \
&& chown -R pflegepilot:pflegepilot /app
USER pflegepilot
EXPOSE 5050
# init_db() ist idempotent (siehe app.py) - unbedenklich bei jedem Containerstart erneut auszufuehren
CMD ["sh", "-c", "python -c 'from app import init_db; init_db()' && exec gunicorn --bind 0.0.0.0:5050 --workers ${GUNICORN_WORKERS:-2} --timeout 120 app:app"]
+17 -10
View File
@@ -16,7 +16,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, "db", "pflegepilot.db")
app = Flask(__name__)
app.secret_key = "pflegepilot-alpha-2025"
app.secret_key = os.environ.get("SECRET_KEY", "pflegepilot-alpha-2025")
# ─── DB helpers ──────────────────────────────────────────────────────────────
def get_db():
@@ -43,6 +43,22 @@ def init_db():
db.executescript(f.read())
# Migrations für bestehende Datenbanken
for sql in [
# nutzer existiert nicht in schema.sql (kam erst später dazu) - muss vor
# allen "ALTER TABLE nutzer ..."/"... REFERENCES nutzer(id)"-Zeilen unten
# stehen, sonst schlagen die auf einer komplett frischen DB (z.B. neues
# Docker-Volume) still fehl und die späteren Spalten fehlen dauerhaft.
"""CREATE TABLE IF NOT EXISTS nutzer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
benutzername TEXT UNIQUE NOT NULL,
passwort_hash TEXT NOT NULL,
name TEXT,
rolle TEXT DEFAULT 'nutzer',
aktiv INTEGER DEFAULT 1,
abo_bis TEXT,
abo_typ TEXT,
ist_demo INTEGER DEFAULT 0,
erstellt_am TEXT DEFAULT (datetime('now','localtime'))
)""",
"ALTER TABLE patienten ADD COLUMN zielgruppe TEXT DEFAULT 'alterspflege'",
"ALTER TABLE leistungen ADD COLUMN katalog_id TEXT",
"ALTER TABLE patienten ADD COLUMN foto_pfad TEXT",
@@ -90,15 +106,6 @@ def init_db():
geaendert_von TEXT DEFAULT 'Nutzer',
geaendert_am TEXT DEFAULT (datetime('now','localtime'))
)""",
"""CREATE TABLE IF NOT EXISTS nutzer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
benutzername TEXT UNIQUE NOT NULL,
passwort_hash TEXT NOT NULL,
name TEXT,
rolle TEXT DEFAULT 'nutzer',
aktiv INTEGER DEFAULT 1,
erstellt_am TEXT DEFAULT (datetime('now','localtime'))
)""",
]:
try:
db.execute(sql)
@@ -0,0 +1,16 @@
services:
app:
build: .
image: pflegepilot:latest
env_file:
- .env
ports:
- "${APP_PORT:-5050}:5050"
volumes:
- pflegepilot_db:/app/db
- pflegepilot_static:/app/static
restart: unless-stopped
volumes:
pflegepilot_db:
pflegepilot_static:
@@ -0,0 +1,2 @@
flask>=3.1,<4
gunicorn>=23,<24