Erste lauffähige Version
This commit is contained in:
@@ -0,0 +1,547 @@
|
||||
# Serienbrief – DOCX‑Mailmerge mit Django, Celery & LibreOffice
|
||||
|
||||
Web‑Anwendung zur Erstellung personalisierter Serienbriefe aus DOCX‑Vorlagen und CSV‑Empfängerlisten. Ergebnis ist ein zusammengeführtes PDF mit einem Brief pro Empfänger:in.
|
||||
|
||||
Zielumgebung: internes LAN, hinter zentralem Reverse‑Proxy (TLS‑Terminierung extern). Produktionsnah konzipiert: Hardening, Resource‑Limits, Healthchecks, Backup‑Job.
|
||||
|
||||
---
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
1. [Funktionsumfang](#funktionsumfang)
|
||||
2. [Architekturübersicht](#architekturübersicht)
|
||||
3. [Tech‑Stack](#techstack)
|
||||
4. [Komponenten im Detail](#komponenten-im-detail)
|
||||
5. [Verzeichnisstruktur](#verzeichnisstruktur)
|
||||
6. [Datenfluss eines Serienbrief‑Jobs](#datenfluss-eines-serienbriefjobs)
|
||||
7. [Datenmodell](#datenmodell)
|
||||
8. [Sicherheitskonzept](#sicherheitskonzept)
|
||||
9. [Netzwerk & Ports](#netzwerk--ports)
|
||||
10. [Volumes & Persistenz](#volumes--persistenz)
|
||||
11. [Konfiguration](#konfiguration)
|
||||
12. [Inbetriebnahme](#inbetriebnahme)
|
||||
13. [Entwicklung in VS Code](#entwicklung-in-vs-code)
|
||||
14. [Betrieb & Wartung](#betrieb--wartung)
|
||||
15. [Datenschutz & Compliance](#datenschutz--compliance)
|
||||
16. [Bekannte Einschränkungen & Roadmap](#bekannte-einschränkungen--roadmap)
|
||||
|
||||
---
|
||||
|
||||
## Funktionsumfang
|
||||
|
||||
- DOCX‑Vorlagen mit Jinja‑artigen Platzhaltern (`{{ feldname }}`) via **docxtpl**
|
||||
- CSV‑Upload als Empfängerliste (UTF‑8, Komma‑Trenner)
|
||||
- Asynchrone Verarbeitung über Celery — UI bleibt responsiv, Status‑Polling per HTMX
|
||||
- DOCX → PDF Konversion mit headless **LibreOffice**
|
||||
- Zusammenführung aller Einzel‑PDFs via **pypdf**
|
||||
- Authentifizierung über Djangos Auth‑System, Brute‑Force‑Schutz mit **django‑axes**
|
||||
- Geschütztes Ausliefern generierter PDFs via **X‑Accel‑Redirect** (Nginx)
|
||||
- Audit‑Log pro Job (Status‑Wechsel, Fehler, abgearbeitete Zeilen)
|
||||
- Tägliches Backup (pg_dump + Media‑Tar, 14 Tage Retention)
|
||||
|
||||
---
|
||||
|
||||
## Architekturübersicht
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Externer LAN‑Proxy (Nginx, TLS‑Terminierung)│
|
||||
│ https://serienbrief.lan │
|
||||
└────────────────────┬─────────────────────────┘
|
||||
│ HTTP (intern)
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────────┐
|
||||
│ Compose‑Stack »serienbrief« │
|
||||
│ │
|
||||
│ ┌───────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ nginx │──▶│ web (Django) │──▶│ db (Postgres 16) │ │
|
||||
│ │ App‑Proxy │ │ Gunicorn │ │ │ │
|
||||
│ └─────┬─────┘ └──────┬───────┘ └──────────────────┘ │
|
||||
│ │ X‑Accel │ enqueue │
|
||||
│ │ static/media ▼ │
|
||||
│ │ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ └────────▶│ redis (Broker)│◀─│ worker (Celery) │ │
|
||||
│ └──────────────┘ │ LibreOffice+pypdf│ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ beat (Cron) │ │ backup (pg_dump) │ │
|
||||
│ └──────────────┘ └──────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Zwei Bridge‑Netze trennen Verantwortlichkeiten:
|
||||
|
||||
- **frontend** – nur `nginx` ↔ `web`. Einziges Netz mit Port‑Bind nach außen.
|
||||
- **backend** – `web`, `worker`, `beat`, `db`, `redis`, `backup`. Kein direktes Internet‑/LAN‑Exposing.
|
||||
|
||||
---
|
||||
|
||||
## Tech‑Stack
|
||||
|
||||
| Schicht | Komponente | Version |
|
||||
|---|---|---|
|
||||
| Host‑OS | Ubuntu Server LTS | 22.04 / 24.04 |
|
||||
| Container Runtime | Docker Engine | 27.x+ |
|
||||
| Orchestrierung | Docker Compose v2 | — |
|
||||
| App‑Framework | Django | 5.1.4 |
|
||||
| WSGI (Prod) | Gunicorn | — |
|
||||
| Task Queue | Celery | 5.4 |
|
||||
| Broker / Result Backend | Redis | 7‑alpine |
|
||||
| Datenbank | PostgreSQL | 16‑alpine |
|
||||
| DOCX‑Templating | docxtpl | 0.19 |
|
||||
| DOCX→PDF | LibreOffice (headless) | bundled in Image |
|
||||
| PDF‑Merge | pypdf | 5.1 |
|
||||
| App‑Proxy | Nginx | 1.27‑alpine |
|
||||
| Auth‑Hardening | django‑axes | aktuell |
|
||||
| UI‑Interaktion | django‑htmx | aktuell |
|
||||
| DB‑Adapter | psycopg | 3.2.3 (binary) |
|
||||
| Python | CPython | 3.12 (slim‑bookworm) |
|
||||
|
||||
---
|
||||
|
||||
## Komponenten im Detail
|
||||
|
||||
### `nginx` – App‑interner Reverse‑Proxy
|
||||
- Terminiert **kein** TLS (übernimmt der externe Proxy)
|
||||
- Liefert Static‑ und Media‑Dateien direkt aus den Volumes aus
|
||||
- Setzt `X‑Accel‑Redirect` für Auth‑geschützte PDFs um: Django prüft die Berechtigung, Nginx streamt die Datei ohne Python im Pfad
|
||||
- Hört auf `${APP_BIND_IP:-127.0.0.1}:${APP_BIND_PORT:-8080}` (Default Loopback)
|
||||
- Healthcheck gegen `/healthz`
|
||||
|
||||
### `web` – Django + Gunicorn
|
||||
- Rolle `web` (per `ROLE`‑Env unterschieden, gleiches Image wie worker/beat)
|
||||
- Stateless, read‑only Root‑FS, `/tmp` als tmpfs
|
||||
- Settings‑Split: `base.py`, `dev.py`, `production.py`
|
||||
- Entrypoint wartet auf DB‑Readiness, führt `migrate` und (in Prod) `collectstatic` aus
|
||||
- Healthcheck via `/healthz`
|
||||
|
||||
### `worker` – Celery
|
||||
- Verarbeitet `process_mailmerge_job`‑Tasks
|
||||
- LibreOffice‑User‑Profil im tmpfs (`/tmp/lo_profile_<task_id>`) → keine Leaks zwischen Jobs
|
||||
- `--concurrency=2`, `--max-tasks-per-child=50` (RAM‑Leak‑Schutz bei LibreOffice)
|
||||
|
||||
### `beat` – Celery Scheduler
|
||||
- Nutzt `DatabaseScheduler` von django‑celery‑beat
|
||||
- Geplant für: Retention‑Cleanup (alte Jobs/PDFs), Audit‑Log‑Rotation
|
||||
- *Hinweis: Konkrete Periodic Tasks aktuell noch nicht registriert (siehe Roadmap)*
|
||||
|
||||
### `db` – PostgreSQL 16
|
||||
- Eigenes Volume `postgres_data`
|
||||
- Passwort via Docker‑Secret (`/run/secrets/postgres_password`)
|
||||
- Init‑Skripte unter `./postgres/init` (z.B. zusätzliche Extensions)
|
||||
- `shm_size: 256mb` für komplexere Queries
|
||||
|
||||
### `redis` – Broker & Result Backend
|
||||
- Passwortgeschützt (`REDIS_PASSWORD` aus `.env`)
|
||||
- AOF persistence + RDB snapshot (`save 900 1`)
|
||||
- `maxmemory 256mb` mit `allkeys‑lru`
|
||||
|
||||
### `backup` – nightly pg_dump + media tar
|
||||
- Custom‑Loop mit `sleep 86400` (kein systemd nötig)
|
||||
- pg_dump im Custom‑Format (`-Fc`) → `pg_restore`‑kompatibel
|
||||
- Media‑Tar aus `media_files`‑Volume (read‑only Mount)
|
||||
- Retention 14 Tage (konfigurierbar)
|
||||
- Schreibt nach `./backups` auf dem Host
|
||||
|
||||
---
|
||||
|
||||
## Verzeichnisstruktur
|
||||
|
||||
```
|
||||
serienbrief/
|
||||
├── app/ # Django‑Projekt
|
||||
│ ├── Dockerfile # Multi‑Stage: builder → runtime‑base → runtime / dev
|
||||
│ ├── entrypoint.sh # wait_for_db → migrate → collectstatic → exec CMD
|
||||
│ ├── manage.py
|
||||
│ ├── requirements.txt
|
||||
│ ├── requirements-dev.txt # debugpy, ipython, ruff, pytest, debug-toolbar
|
||||
│ ├── config/ # Project package
|
||||
│ │ ├── celery.py
|
||||
│ │ ├── urls.py
|
||||
│ │ ├── wsgi.py
|
||||
│ │ └── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── dev.py
|
||||
│ │ └── production.py
|
||||
│ ├── mailmerge/ # Hauptapp
|
||||
│ │ ├── models.py # LetterTemplate, MailMergeJob, JobLogEntry
|
||||
│ │ ├── views.py
|
||||
│ │ ├── admin.py
|
||||
│ │ ├── forms.py
|
||||
│ │ ├── urls.py
|
||||
│ │ ├── tasks.py # Celery tasks
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── docx_renderer.py # docxtpl + LibreOffice
|
||||
│ │ │ └── pdf_merge.py # pypdf
|
||||
│ │ └── management/commands/
|
||||
│ │ └── wait_for_db.py
|
||||
│ └── templates/
|
||||
│ ├── base.html
|
||||
│ ├── registration/login.html
|
||||
│ └── mailmerge/…
|
||||
├── nginx/
|
||||
│ ├── nginx.conf
|
||||
│ └── conf.d/serienbrief.conf
|
||||
├── postgres/init/ # SQL init scripts
|
||||
├── secrets/
|
||||
│ └── postgres_password.txt # 0600, NICHT in Git
|
||||
├── backups/ # nightly dumps + media tars
|
||||
├── docker-compose.yml # Produktion
|
||||
├── docker-compose.override.yml # Dev (automatisch gemerged)
|
||||
├── .env # NICHT in Git
|
||||
├── .env.example
|
||||
├── .devcontainer/ # VS Code Dev Container
|
||||
├── .vscode/ # launch / tasks / settings / extensions
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Datenfluss eines Serienbrief‑Jobs
|
||||
|
||||
1. **Upload Vorlage:** `LetterTemplate` mit DOCX‑Datei wird angelegt (Admin oder UI)
|
||||
2. **Job‑Erstellung:** User wählt Vorlage + lädt CSV hoch → `MailMergeJob(status=queued)`
|
||||
3. **Enqueue:** View löst `process_mailmerge_job.delay(job.pk)` aus
|
||||
4. **Worker holt Task:**
|
||||
- Liest CSV (UTF‑8, validiert Header gegen Platzhalter)
|
||||
- Iteriert Zeilen → docxtpl rendert pro Zeile eine temporäre DOCX in `/tmp`
|
||||
- LibreOffice (`soffice --headless --convert-to pdf …`) erzeugt pro DOCX ein PDF
|
||||
- pypdf merged alle PDFs in eine Ausgabedatei unter `media_files`
|
||||
- `JobLogEntry`s pro Schritt; Status `running → done` bzw. `failed`
|
||||
5. **UI‑Polling:** HTMX fragt `/jobs/<id>/status/` alle ~2 s ab → Fortschrittsbalken
|
||||
6. **Download:** Nutzer klickt Download → View prüft Auth → Antwort enthält `X‑Accel‑Redirect: /protected/…` → Nginx streamt das PDF
|
||||
|
||||
---
|
||||
|
||||
## Datenmodell
|
||||
|
||||
```text
|
||||
LetterTemplate
|
||||
├── id, name (unique), description
|
||||
├── docx_file (FileField → media/templates/)
|
||||
├── created_by (FK User), created_at, updated_at
|
||||
└── is_active
|
||||
|
||||
MailMergeJob
|
||||
├── id, template (FK LetterTemplate)
|
||||
├── csv_file (FileField → media/jobs/<id>/input.csv)
|
||||
├── output_pdf (FileField → media/jobs/<id>/output.pdf)
|
||||
├── status: queued | running | done | failed
|
||||
├── total_rows, processed_rows
|
||||
├── created_by, created_at, started_at, finished_at
|
||||
└── error_message
|
||||
|
||||
JobLogEntry
|
||||
├── id, job (FK MailMergeJob, related_name="log_entries")
|
||||
├── level: info | warning | error
|
||||
├── message, created_at
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sicherheitskonzept
|
||||
|
||||
Im Sinne der Space‑Vorgabe »Security‑by‑Default«:
|
||||
|
||||
### Container‑Hardening
|
||||
- `no-new-privileges: true` bei allen Services
|
||||
- `read_only: true` für `web`, `worker`, `beat` (writable nur via tmpfs `/tmp`)
|
||||
- Non‑root‑User `app` (UID/GID 1000, anpassbar per Build‑Args)
|
||||
- Minimales Image (`python:3.12-slim-bookworm`), Multi‑Stage‑Build trennt Build‑ und Laufzeit‑Dependencies
|
||||
- Ressourcen‑Limits (memory + cpus) auf allen Services
|
||||
- `tmpfs`‑Größen explizit limitiert (`size=256M`, etc.)
|
||||
|
||||
### Netzwerk
|
||||
- Zwei isolierte Bridges (`frontend`, `backend`)
|
||||
- Nur `nginx` bindet nach außen, default auf **Loopback**
|
||||
- DB und Redis ausschließlich auf `backend`
|
||||
- Externer Proxy übernimmt TLS, HSTS, ggf. IP‑Allowlist
|
||||
|
||||
### Secrets
|
||||
- Postgres‑Passwort via Docker‑Secret (`./secrets/postgres_password.txt`, Mode 0600)
|
||||
- Redis‑Passwort + Django‑Secret‑Key in `.env` (nicht in Git)
|
||||
- `.env` und `secrets/` sind in `.gitignore` ausgenommen
|
||||
|
||||
### Anwendungs‑Sicherheit
|
||||
- django‑axes: Lockout nach n Fehlversuchen
|
||||
- CSRF aktiv, Trusted Origins explizit gepflegt
|
||||
- `SECURE_PROXY_SSL_HEADER` gesetzt, damit Django HTTPS hinter dem externen Proxy korrekt erkennt
|
||||
- `USE_X_FORWARDED_HOST=True`
|
||||
- X‑Accel‑Redirect statt direktem File‑Download → Nginx liefert nur, was Django freigibt
|
||||
- Session‑Cookies `HttpOnly`, `SameSite=Lax`, `Secure` (in Prod)
|
||||
|
||||
### Defense‑in‑Depth (Empfehlungen)
|
||||
- AppArmor‑Profile pro Container (z.B. via `--security-opt apparmor=...`) – aktuell nicht aktiv
|
||||
- seccomp‑Profile (Default reicht, custom optional)
|
||||
- Image‑Scanning (`trivy`, `grype`) in CI integrieren
|
||||
- Renovate/Dependabot für Base‑Image und Python‑Deps
|
||||
|
||||
---
|
||||
|
||||
## Netzwerk & Ports
|
||||
|
||||
| Port (Host) | Service | Sichtbarkeit | Zweck |
|
||||
|---|---|---|---|
|
||||
| 8080 | nginx | `${APP_BIND_IP}` | HTTP für externen Proxy |
|
||||
| 8000 | web | nur Dev | Django runserver |
|
||||
| 5678 | web (Dev) | nur Dev | debugpy |
|
||||
| 5432 | db | nur Dev | Postgres (Tools wie pgAdmin) |
|
||||
| 6379 | redis | nur Dev | Redis‑CLI |
|
||||
|
||||
In Produktion bleiben **nur** Port 8080 (intern an externen Proxy) erreichbar.
|
||||
|
||||
---
|
||||
|
||||
## Volumes & Persistenz
|
||||
|
||||
| Volume | Inhalt | Backup? |
|
||||
|---|---|---|
|
||||
| `postgres_data` | Postgres‑Daten | pg_dump nightly |
|
||||
| `redis_data` | AOF + RDB | nein (Broker‑State, regenerierbar) |
|
||||
| `static_files` | Generierte Static‑Files | nein (regenerierbar via `collectstatic`) |
|
||||
| `media_files` | Hochgeladene Templates, CSVs, generierte PDFs | tar nightly |
|
||||
| `nginx_cache` | Nginx Cache | nein |
|
||||
| `nginx_run` | Nginx PID/Socket | nein |
|
||||
|
||||
Bind‑Mount im Dev: `./app:/app` für Live‑Reload.
|
||||
|
||||
---
|
||||
|
||||
## Konfiguration
|
||||
|
||||
`.env` (aus `.env.example` abgeleitet) — Auszug:
|
||||
|
||||
```env
|
||||
# App
|
||||
APP_VERSION=1.0.0
|
||||
APP_BIND_IP=127.0.0.1
|
||||
APP_BIND_PORT=8080
|
||||
APP_UID=1000
|
||||
APP_GID=1000
|
||||
|
||||
# Django
|
||||
DJANGO_SECRET_KEY=<openssl rand -base64 64>
|
||||
DJANGO_DEBUG=False
|
||||
DJANGO_ALLOWED_HOSTS=serienbrief.lan,127.0.0.1,localhost
|
||||
CSRF_TRUSTED_ORIGINS=https://serienbrief.lan
|
||||
SECURE_PROXY_SSL_HEADER=HTTP_X_FORWARDED_PROTO,https
|
||||
USE_X_FORWARDED_HOST=True
|
||||
|
||||
# Postgres
|
||||
POSTGRES_DB=serienbrief
|
||||
POSTGRES_USER=serienbrief
|
||||
DATABASE_URL=postgres://serienbrief:<HEX-PWD>@db:5432/serienbrief
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=<openssl rand -hex 32>
|
||||
CELERY_BROKER_URL=redis://:<REDIS_PASSWORD>@redis:6379/0
|
||||
CELERY_RESULT_BACKEND=redis://:<REDIS_PASSWORD>@redis:6379/1
|
||||
|
||||
# Business
|
||||
JOB_RETENTION_DAYS=30
|
||||
```
|
||||
|
||||
**Wichtig:**
|
||||
- `secrets/postgres_password.txt` muss exakt denselben Wert enthalten wie in `DATABASE_URL`
|
||||
- Datei darf **kein** abschließendes Newline enthalten — Generierung: `openssl rand -hex 32 | tr -d '\n' > secrets/postgres_password.txt && chmod 600 secrets/postgres_password.txt`
|
||||
- Hex statt Base64, weil Base64‑`=` URL‑Parsing in `DATABASE_URL` bricht
|
||||
|
||||
---
|
||||
|
||||
## Inbetriebnahme
|
||||
|
||||
### Voraussetzungen
|
||||
- Ubuntu 22.04/24.04 LTS, aktueller Patch‑Stand
|
||||
- Docker Engine 27.x+ und Compose v2 (apt‑Pakete `docker-ce` + `docker-compose-plugin`, nicht Snap)
|
||||
- Host‑User in `docker`‑Gruppe (`sudo usermod -aG docker $USER`, dann Re‑Login)
|
||||
|
||||
### Erststart
|
||||
|
||||
```bash
|
||||
# 1. Repo holen / entpacken in z.B. ~/projekte/serienbrief
|
||||
cd ~/projekte/serienbrief
|
||||
|
||||
# 2. Konfiguration aus Template ableiten
|
||||
cp .env.example .env
|
||||
$EDITOR .env # Werte ausfüllen, siehe oben
|
||||
|
||||
# 3. Postgres‑Passwort als Datei
|
||||
openssl rand -hex 32 | tr -d '\n' > secrets/postgres_password.txt
|
||||
chmod 600 secrets/postgres_password.txt
|
||||
# → diesen Wert auch in DATABASE_URL eintragen
|
||||
|
||||
# 4. Image bauen
|
||||
docker compose build
|
||||
|
||||
# 5. Hochfahren
|
||||
docker compose up -d
|
||||
|
||||
# 6. Status & Logs
|
||||
docker compose ps -a
|
||||
docker compose logs -f web
|
||||
|
||||
# 7. Superuser anlegen
|
||||
docker compose exec web python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### Externen Proxy konfigurieren (Beispiel)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name serienbrief.lan;
|
||||
|
||||
ssl_certificate /etc/ssl/lan/serienbrief.crt;
|
||||
ssl_certificate_key /etc/ssl/lan/serienbrief.key;
|
||||
|
||||
location / {
|
||||
proxy_pass http://<docker-host-ip>:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_redirect off;
|
||||
client_max_body_size 50M; # für CSV/DOCX‑Uploads
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entwicklung in VS Code
|
||||
|
||||
- `.devcontainer/devcontainer.json` referenziert den Compose‑Stack mit Dev‑Override
|
||||
- `.vscode/launch.json` enthält Configs:
|
||||
- **Django: runserver (attach)** → debugpy auf 5678
|
||||
- **Django: pytest** → führt Tests im Container aus
|
||||
- `.vscode/tasks.json` für `makemigrations`, `migrate`, `shell`, `collectstatic`
|
||||
- `.vscode/extensions.json` empfiehlt: Python, Pylance, Django, Docker, Even Better TOML, Ruff
|
||||
|
||||
### Workflow
|
||||
|
||||
```bash
|
||||
# Stack hochfahren (Override greift automatisch)
|
||||
docker compose up -d
|
||||
|
||||
# Code‑Änderungen werden via Bind‑Mount sofort sichtbar
|
||||
# runserver reloaded automatisch
|
||||
|
||||
# Debugging: F5 in VS Code → attach an debugpy:5678
|
||||
```
|
||||
|
||||
### Wichtige Unterschiede Dev ↔ Prod
|
||||
|
||||
| Aspekt | Dev | Prod |
|
||||
|---|---|---|
|
||||
| WSGI | `runserver` | Gunicorn |
|
||||
| Code | Bind‑Mount `./app:/app` | im Image |
|
||||
| Healthcheck | deaktiviert | aktiv (`/healthz`) |
|
||||
| `read_only` | aus | an |
|
||||
| `DJANGO_DEBUG` | True | False |
|
||||
| `collectstatic` | auto im Entrypoint | im Entrypoint |
|
||||
| `target` | `dev` (mit debugpy, ipython, pytest) | `runtime` |
|
||||
|
||||
---
|
||||
|
||||
## Betrieb & Wartung
|
||||
|
||||
### Häufige Operationen
|
||||
|
||||
```bash
|
||||
# Logs eines einzelnen Services
|
||||
docker compose logs -f worker
|
||||
|
||||
# In den Container schauen
|
||||
docker compose exec web bash
|
||||
docker compose exec db psql -U serienbrief
|
||||
|
||||
# Migration nach Modelländerung
|
||||
docker compose exec web python manage.py makemigrations
|
||||
docker compose exec web python manage.py migrate
|
||||
|
||||
# Statics neu generieren
|
||||
docker compose exec web python manage.py collectstatic --noinput
|
||||
|
||||
# Celery‑Queue Status
|
||||
docker compose exec worker celery -A config inspect active
|
||||
docker compose exec worker celery -A config inspect reserved
|
||||
```
|
||||
|
||||
### Updates
|
||||
|
||||
```bash
|
||||
# Code aktualisieren
|
||||
git pull
|
||||
|
||||
# Image neu bauen
|
||||
docker compose build --pull
|
||||
|
||||
# Mit Zero‑Downtime nicht möglich (single‑host) → kurzer Restart
|
||||
docker compose up -d
|
||||
|
||||
# DB‑Migrationen
|
||||
docker compose exec web python manage.py migrate
|
||||
```
|
||||
|
||||
### Backup & Restore
|
||||
|
||||
```bash
|
||||
# Manuelles Backup erzwingen
|
||||
docker compose restart backup
|
||||
docker compose logs --tail=50 backup
|
||||
|
||||
# Restore (Beispiel)
|
||||
docker compose exec -T db pg_restore -U serienbrief -d serienbrief --clean < backups/db_20260521_030000.dump
|
||||
```
|
||||
|
||||
### Monitoring (Empfehlung, nicht enthalten)
|
||||
- `docker stats` für ad‑hoc Container‑Ressourcen
|
||||
- Healthcheck‑Status: `docker compose ps`
|
||||
- Container‑Logs via zentralem syslog/journald (Docker `json-file` rotiert 5×10 MB)
|
||||
- Optional: Prometheus + cAdvisor + node_exporter, Loki für Logs
|
||||
|
||||
---
|
||||
|
||||
## Datenschutz & Compliance
|
||||
|
||||
- **Rechtsgrundlage:** Verarbeitung personenbezogener Adress‑/Empfängerdaten je nach Anwendungsfall Art. 6 Abs. 1 lit. b/c/e DSGVO; bei besonderen Datenkategorien (Art. 9 DSGVO) gesonderte Rechtsgrundlage erforderlich
|
||||
- **Speicherort:** Innerbetrieblicher Server, kein Cloud‑Transfer
|
||||
- **Zugriffssteuerung:** Nur authentifizierte User; Trennung Templates/Jobs aktuell flach — Mandanten‑/Rollenmodell ggf. ergänzen (siehe Roadmap)
|
||||
- **Aufbewahrung:** Jobs + CSVs werden nach `JOB_RETENTION_DAYS` (default 30) gelöscht — Cleanup‑Task noch zu implementieren (siehe Roadmap)
|
||||
- **Audit:** Jeder Job hat ein `JobLogEntry`‑Protokoll (wer, wann, was, Fehler)
|
||||
- **Backup‑Verschlüsselung:** Bind‑Mount `./backups` liegt aktuell unverschlüsselt — für produktiven Einsatz LUKS/Filesystem‑Encryption am Host **dringend empfohlen**
|
||||
- **DSFA‑Relevanz:** Bei Verarbeitung besonderer Datenkategorien oder umfangreicher Profilbildung ist eine Datenschutz‑Folgenabschätzung gemäß Art. 35 DSGVO durchzuführen, bevor produktiver Echtbetrieb startet
|
||||
|
||||
---
|
||||
|
||||
## Bekannte Einschränkungen & Roadmap
|
||||
|
||||
### Tech‑Debt
|
||||
- Postgres‑Passwort doppelt gehalten (`DATABASE_URL` + Secret‑Datei) → refactor zu Single Source via Entrypoint, der `DATABASE_URL` aus `*_FILE`‑Env zusammenbaut
|
||||
- Keine LDAP/AD‑Anbindung — aktuell lokale Django‑User
|
||||
- Keine Periodic Tasks im Beat registriert (Retention‑Cleanup, Log‑Rotation)
|
||||
- CSV‑Header‑Validierung ist warn‑only, kein Abbruch bei fehlenden Pflichtfeldern
|
||||
- Keine Vorschau (Single‑Letter‑Preview) vor dem Komplett‑Render
|
||||
- Image enthält LibreOffice (~400 MB) — könnte in separates Worker‑Image ausgelagert werden
|
||||
|
||||
### Roadmap (kurzfristig)
|
||||
- [ ] Single‑Letter‑Preview im Job‑Erstellungs‑Flow
|
||||
- [ ] Retention‑Cleanup als periodic task
|
||||
- [ ] CSV‑Validierung strict mit Pflichtfeldliste pro Template
|
||||
- [ ] Mandanten‑/Berechtigungsmodell (Abteilung X sieht nur eigene Templates)
|
||||
- [ ] LDAP‑Auth über `django‑auth‑ldap` (AD‑Integration GB)
|
||||
|
||||
### Roadmap (mittelfristig)
|
||||
- [ ] AppArmor‑Profile pro Container
|
||||
- [ ] Image‑Scanning in CI (Trivy)
|
||||
- [ ] Separate Worker‑Image ohne Web‑Dependencies
|
||||
- [ ] AES‑Verschlüsselung der hochgeladenen CSVs at rest
|
||||
- [ ] Optional: Konvertierung über Gotenberg statt LibreOffice‑im‑Image (cleaneres Separation of Concerns)
|
||||
|
||||
---
|
||||
|
||||
## Lizenz & Verantwortlichkeit
|
||||
|
||||
Internes Projekt. Verantwortlich für Konzept & Betrieb: **Datenschutzkoordination / IT‑Systemadministration**.
|
||||
|
||||
Keine externe Lizenz festgelegt — Verteilung ausschließlich innerbetrieblich.
|
||||
Reference in New Issue
Block a user