From 960bcd6dbf9ee23bbd25dd6626953d2487382ce5 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sat, 18 Jul 2026 18:25:10 +0200 Subject: [PATCH] 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 --- .gitignore | 3 +++ CLAUDE.md | 18 +++++++++++++ open.md | 11 ++++---- .../pflegepilot/.dockerignore | 16 +++++++++++ pflegepilot_alpha_v2/pflegepilot/Dockerfile | 21 +++++++++++++++ pflegepilot_alpha_v2/pflegepilot/app.py | 27 ++++++++++++------- .../pflegepilot/docker-compose.yml | 16 +++++++++++ .../pflegepilot/requirements.txt | 2 ++ 8 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 pflegepilot_alpha_v2/pflegepilot/.dockerignore create mode 100644 pflegepilot_alpha_v2/pflegepilot/Dockerfile create mode 100644 pflegepilot_alpha_v2/pflegepilot/docker-compose.yml create mode 100644 pflegepilot_alpha_v2/pflegepilot/requirements.txt diff --git a/.gitignore b/.gitignore index b5927ac..aabf4fe 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/CLAUDE.md b/CLAUDE.md index 92bc055..9c41a24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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.** diff --git a/open.md b/open.md index ba65fe7..3340956 100644 --- a/open.md +++ b/open.md @@ -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 diff --git a/pflegepilot_alpha_v2/pflegepilot/.dockerignore b/pflegepilot_alpha_v2/pflegepilot/.dockerignore new file mode 100644 index 0000000..a664239 --- /dev/null +++ b/pflegepilot_alpha_v2/pflegepilot/.dockerignore @@ -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 diff --git a/pflegepilot_alpha_v2/pflegepilot/Dockerfile b/pflegepilot_alpha_v2/pflegepilot/Dockerfile new file mode 100644 index 0000000..cdf052d --- /dev/null +++ b/pflegepilot_alpha_v2/pflegepilot/Dockerfile @@ -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"] diff --git a/pflegepilot_alpha_v2/pflegepilot/app.py b/pflegepilot_alpha_v2/pflegepilot/app.py index b3f1f50..d37e578 100644 --- a/pflegepilot_alpha_v2/pflegepilot/app.py +++ b/pflegepilot_alpha_v2/pflegepilot/app.py @@ -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) diff --git a/pflegepilot_alpha_v2/pflegepilot/docker-compose.yml b/pflegepilot_alpha_v2/pflegepilot/docker-compose.yml new file mode 100644 index 0000000..7a9300f --- /dev/null +++ b/pflegepilot_alpha_v2/pflegepilot/docker-compose.yml @@ -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: diff --git a/pflegepilot_alpha_v2/pflegepilot/requirements.txt b/pflegepilot_alpha_v2/pflegepilot/requirements.txt new file mode 100644 index 0000000..5833b7c --- /dev/null +++ b/pflegepilot_alpha_v2/pflegepilot/requirements.txt @@ -0,0 +1,2 @@ +flask>=3.1,<4 +gunicorn>=23,<24