Am Anfang einer Betreuung weiss niemand, welche Vertraege es gibt. Die Antwort steht aber im Kontoauszug: Wer regelmaessig abbucht, hat einen Vertrag. Neu unter Vermoegen & Kasse wird aus hunderten Buchungszeilen eine Arbeitsliste. Aufbau: - Die Erkennung liegt als reine Funktionen in src/lib/finanzen/wiederkehrend.ts, ohne Datenbank. Nur so laesst sich jede Einordnung einzeln nachrechnen. - Der Datenzugriff steht getrennt in wiederkehrend-laden.ts. - Ein kleines Modell haelt fest, was bewusst KEIN Vertrag ist. Es haengt am normalisierten Empfaengernamen, nicht an Buchungen - nur so ueberlebt die Entscheidung den naechsten Import. Zwei Haltungen liegen der Erkennung zugrunde: Im Zweifel getrennt lassen. Zusammengefasst wird nur das Unstrittige: Gross- und Kleinschreibung, Satzzeichen, Umlaute, Rechtsform am Ende. Keine Aehnlichkeitssuche - "Stadtwerke Bamberg" und "Stadtwerke Bayreuth" trennen drei Zeichen und zwei Rechnungssteller. Zwei Zeilen zu viel sind harmlos; eine falsche Gruppe verbirgt einen Vertrag. Nichts behaupten, was nicht belegt ist. Statt "monatlich" steht dort "6 Zahlungen, etwa monatlich". Schwankt der Betrag, wird die Spanne genannt. Ohne erkennbaren Abstand wird nicht hochgerechnet, sondern gezeigt, was geflossen ist. Dazu eine zweite Liste fuer einzelne groessere Zahlungen: Die Gruppierung braucht zwei Zahlungen, ein Jahresbeitrag erscheint bei einem importierten Jahr aber nur einmal - und ausgerechnet Versicherungen zahlt man oft jaehrlich. Geprueft: 37 Einheitstests, die Haelfte davon in der Gegenrichtung, plus vier End-to-End-Tests auf beiden Geraeteprofilen. Der Browsertest hat zwei Fehler gefunden, die Typpruefung und Einheitstests durchgelassen haben - darunter ein verlorener Backslash, der die Vorbelegung des Beitrags stillschweigend verschluckte. Nebenbei: .claude/ ins .gitignore. Dort legen parallel arbeitende Agenten ihre Worktrees ab; ein "git add -A" haette einen kompletten zweiten Checkout eingecheckt. Refs #3
1155 lines
35 KiB
Markdown
1155 lines
35 KiB
Markdown
# Installation Schritt für Schritt
|
|
|
|
Von einem vorbereiteten Server bis zur laufenden Anwendung. Rechne mit
|
|
**30 bis 45 Minuten**, wenn du es zum ersten Mal machst.
|
|
|
|
Diese Anleitung setzt einen Server voraus, auf dem **Docker bereits läuft**,
|
|
und einen **Reverse Proxy, der schon steht** — auch dann, wenn er auf einem
|
|
anderen Server liegt. Sie richtet nur das Betreuungs-Cockpit ein: kein
|
|
Docker, keine Betriebssystemhärtung, kein Proxy von Grund auf.
|
|
|
|
Was genau auf dem Proxy hinterlegt werden muss, steht vollständig in
|
|
[Schritt 7](#schritt-7--in-den-reverse-proxy-eintragen).
|
|
|
|
Nach jedem Schritt steht eine **Kontrolle**. Geh nicht weiter, bevor die
|
|
durchgeht — dann weißt du im Fehlerfall genau, wo es hakt.
|
|
|
|
> Wenn du die Anwendung nur ausprobieren willst, ohne Server: Der
|
|
> Abschnitt *Schnellstart* in der [README](../README.md) bringt dich in
|
|
> zehn Minuten auf dem eigenen Rechner zum Laufen.
|
|
|
|
---
|
|
|
|
## Inhalt
|
|
|
|
- [Schritt 0 — Voraussetzungen prüfen](#schritt-0--voraussetzungen-prüfen)
|
|
- [Schritt 1 — Anwendung holen](#schritt-1--anwendung-holen)
|
|
- [Schritt 2 — Schlüssel erzeugen](#schritt-2--schlüssel-erzeugen)
|
|
- [Schritt 3 — Konfiguration ausfüllen](#schritt-3--konfiguration-ausfüllen)
|
|
- [Schritt 4 — Starten](#schritt-4--starten)
|
|
- [Schritt 5 — Datenbank vorbereiten](#schritt-5--datenbank-vorbereiten)
|
|
- [Schritt 6 — Dateiablage einrichten](#schritt-6--dateiablage-einrichten)
|
|
- [Schritt 7 — In den Reverse Proxy eintragen](#schritt-7--in-den-reverse-proxy-eintragen)
|
|
- [Schritt 8 — Ersteinrichtung im Browser](#schritt-8--ersteinrichtung-im-browser)
|
|
- [Schritt 9 — Sicherung prüfen](#schritt-9--sicherung-prüfen)
|
|
- [Schritt 10 — Wartungslauf einrichten](#schritt-10--wartungslauf-einrichten)
|
|
- [Schritt 11 — Schlüssel in Sicherheit bringen](#schritt-11--schlüssel-in-sicherheit-bringen)
|
|
- [Aktualisieren](#aktualisieren)
|
|
- [Wenn etwas nicht klappt](#wenn-etwas-nicht-klappt)
|
|
- [Abnahmeliste](#abnahmeliste)
|
|
|
|
---
|
|
|
|
## Schritt 0 — Voraussetzungen prüfen
|
|
|
|
### Was der Server mitbringen muss
|
|
|
|
| | Minimum | Bequem |
|
|
|---|---|---|
|
|
| Arbeitsspeicher | 2 GB frei | 4 GB frei |
|
|
| Speicherplatz | 20 GB frei | 40 GB frei |
|
|
| Docker | 24 oder neuer | aktuell |
|
|
| Docker Compose | v2 (`docker compose`, nicht `docker-compose`) | aktuell |
|
|
| Reverse Proxy | vorhanden, mit TLS (auch auf einem anderen Server) | dasselbe |
|
|
| Netzweg zum Proxy | privates Netz, WireGuard oder Firewallregel | privates Netz |
|
|
| Standort | Deutschland oder mindestens EU | dasselbe |
|
|
|
|
Der erste Bau lädt Tesseract mit den deutschen Sprachdaten — plan etwa
|
|
**1 GB Netzverkehr** und fünf bis zehn Minuten ein.
|
|
|
|
### Kontrolle
|
|
|
|
Melde dich per SSH auf dem Server an und prüf:
|
|
|
|
```bash
|
|
docker --version # 24.x oder neuer
|
|
docker compose version # v2.x - mit Leerzeichen, nicht docker-compose
|
|
docker run --rm hello-world # "Hello from Docker!"
|
|
df -h / # genug Platz?
|
|
free -h # genug Speicher?
|
|
```
|
|
|
|
Kommt bei `docker run` ein *permission denied*, fehlt dir die
|
|
Docker-Gruppe:
|
|
|
|
```bash
|
|
sudo usermod -aG docker $USER
|
|
```
|
|
|
|
Danach **abmelden und neu anmelden**, sonst greift es nicht.
|
|
|
|
### Zeitzone
|
|
|
|
Die Behälter setzen `Europe/Berlin` selbst — der Host muss also nicht
|
|
umgestellt sein. Für die Logs und den cron-Eintrag aus Schritt 10 ist es
|
|
trotzdem angenehm:
|
|
|
|
```bash
|
|
timedatectl | grep "Time zone"
|
|
# nicht Europe/Berlin? dann:
|
|
sudo timedatectl set-timezone Europe/Berlin
|
|
```
|
|
|
|
### Domain
|
|
|
|
Du brauchst eine Adresse, unter der die Anwendung erreichbar sein soll,
|
|
zum Beispiel `cockpit.deine-domain.de`.
|
|
|
|
Der A-Record zeigt auf den **Proxy-Server**, nicht auf diesen hier — der
|
|
Proxy nimmt die Anfragen entgegen und reicht sie weiter. Setz ihn
|
|
**jetzt**, DNS-Änderungen brauchen manchmal eine Stunde.
|
|
|
|
```bash
|
|
dig +short cockpit.deine-domain.de # muss die IP des PROXY-Servers zeigen
|
|
```
|
|
|
|
### Und das Wichtigste
|
|
|
|
> Kläre den **Auftragsverarbeitungsvertrag nach Art. 28 DSGVO** mit deinem
|
|
> Hoster. Bei Hetzner im Kundenkonto unter *Verträge → AV-Vertrag*, bei
|
|
> anderen ähnlich. Ohne ihn ist der Betrieb formal rechtswidrig — und es
|
|
> dauert fünf Minuten.
|
|
>
|
|
> Dasselbe gilt für den Objektspeicher, falls du einen externen benutzt
|
|
> statt des mitgelieferten MinIO.
|
|
|
|
Halt außerdem etwas bereit, um Schlüssel zu notieren. Ein Passwortmanager
|
|
ist das Richtige — du erzeugst gleich Werte, die du nicht verlieren darfst.
|
|
|
|
---
|
|
|
|
## Schritt 1 — Anwendung holen
|
|
|
|
```bash
|
|
sudo mkdir -p /opt/betreuungs-cockpit
|
|
sudo chown $USER:$USER /opt/betreuungs-cockpit
|
|
|
|
git clone https://git.wilde-server.de/florian.wilde/betreuungs-cockpit.git \
|
|
/opt/betreuungs-cockpit
|
|
|
|
cd /opt/betreuungs-cockpit
|
|
|
|
# Auf die letzte veröffentlichte Fassung wechseln,
|
|
# nicht auf den laufenden Entwicklungsstand
|
|
git checkout v1.2.0
|
|
```
|
|
|
|
### Kontrolle
|
|
|
|
```bash
|
|
ls docker-compose.yml Dockerfile .env.example # alle drei da
|
|
git describe --tags # v1.2.0
|
|
```
|
|
|
|
---
|
|
|
|
## Schritt 2 — Schlüssel erzeugen
|
|
|
|
Die Anwendung braucht sieben Geheimnisse. Erzeuge sie **auf dem Server**,
|
|
nicht auf dem eigenen Rechner:
|
|
|
|
```bash
|
|
docker run --rm node:22-alpine node -e "
|
|
const c = require('crypto');
|
|
const b = n => c.randomBytes(n).toString('base64');
|
|
const w = n => b(n).replace(/[+/=]/g,'x');
|
|
console.log('AUTH_SECRET=' + b(32));
|
|
console.log('ENC_KEY_V1=' + b(32));
|
|
console.log('INDEX_KEY=' + b(32));
|
|
console.log('POSTGRES_PASSWORD=' + w(24));
|
|
console.log('SICHERUNG_PASSWORT=' + w(24));
|
|
console.log('S3_ACCESS_KEY_ID=' + w(12));
|
|
console.log('S3_SECRET_ACCESS_KEY=' + w(24));
|
|
"
|
|
```
|
|
|
|
Kopier die Ausgabe in einen Texteditor. Du brauchst sie im nächsten
|
|
Schritt.
|
|
|
|
> **`ENC_KEY_V1` ist der wichtigste Wert der ganzen Installation.** Mit ihm
|
|
> sind Diagnosen, IBANs, Versicherungsnummern und alle Notizen
|
|
> verschlüsselt. Geht er verloren, sind diese Felder
|
|
> **unwiederbringlich** weg — auch aus jedem Backup. Es gibt keine
|
|
> Hintertür.
|
|
>
|
|
> Schritt 11 sagt dir, wohin damit. Verlier ihn bis dahin nicht.
|
|
|
|
---
|
|
|
|
## Schritt 3 — Konfiguration ausfüllen
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
nano .env
|
|
```
|
|
|
|
Diese Werte **musst** du setzen:
|
|
|
|
```bash
|
|
# --- Datenbank ---
|
|
POSTGRES_PASSWORD=<aus Schritt 2>
|
|
|
|
# --- Anmeldung ---
|
|
AUTH_SECRET=<aus Schritt 2>
|
|
AUTH_URL=https://cockpit.deine-domain.de # exakt deine Adresse, mit https
|
|
AUTH_TRUST_HOST=true
|
|
|
|
# --- Verschlüsselung ---
|
|
ENC_KEY_V1=<aus Schritt 2>
|
|
ENC_KEY_AKTIV=1
|
|
INDEX_KEY=<aus Schritt 2>
|
|
|
|
# --- Objektspeicher ---
|
|
S3_ACCESS_KEY_ID=<aus Schritt 2>
|
|
S3_SECRET_ACCESS_KEY=<aus Schritt 2>
|
|
|
|
# --- Sicherung ---
|
|
SICHERUNG_PASSWORT=<aus Schritt 2>
|
|
|
|
# --- Erreichbarkeit ---
|
|
# Steht dein Proxy auf einem ANDEREN Server, muss hier die Adresse hinein,
|
|
# ueber die er diesen Server erreicht. Bleibt es bei 127.0.0.1, kommt er
|
|
# NICHT heran - und der Fehler sieht aus, als sei der Proxy schuld.
|
|
# Welcher Wert der richtige ist, steht in Schritt 7a.
|
|
APP_BIND=127.0.0.1
|
|
APP_PORT=3000
|
|
```
|
|
|
|
> **`AUTH_URL` muss exakt der Adresse entsprechen**, unter der du die
|
|
> Anwendung später aufrufst — mit `https://`, ohne Schrägstrich am Ende.
|
|
> Steht dort etwas anderes, wird das Sitzungs-Cookie für die falsche
|
|
> Herkunft gesetzt: Die Anmeldung landet stumm wieder auf dem Formular,
|
|
> ohne Fehlermeldung. Das ist der Fehler, den man am längsten sucht.
|
|
>
|
|
> Seit 1.1.0 hängt noch mehr daran: Aus dieser Adresse wird die Kennung
|
|
> abgeleitet, an die Passkeys gebunden werden. Steht sie falsch, lässt
|
|
> sich zwar ein Passkey einrichten, aber nie wieder verwenden. Und ein
|
|
> späterer Wechsel der Domain macht alle vorhandenen Passkeys
|
|
> unbrauchbar — das ist kein Fehler, sondern der Phishingschutz.
|
|
|
|
**Läuft auf dem Server schon etwas auf Port 3000?** Dann weich aus:
|
|
|
|
```bash
|
|
APP_PORT=3010
|
|
```
|
|
|
|
Auf welcher Adresse die Anwendung horcht, legst du in
|
|
[Schritt 7](#schritt-7--in-den-reverse-proxy-eintragen) fest — dort steht
|
|
auch, warum `127.0.0.1` nicht reicht, wenn der Proxy woanders läuft.
|
|
|
|
Optional, aber empfohlen — Fristerinnerungen per E-Mail:
|
|
|
|
```bash
|
|
SMTP_HOST=mail.deine-domain.de
|
|
SMTP_PORT=587
|
|
SMTP_USER=cockpit@deine-domain.de
|
|
SMTP_PASSWORD=<dein Mailpasswort>
|
|
SMTP_FROM="Betreuungs-Cockpit <cockpit@deine-domain.de>"
|
|
```
|
|
|
|
Speichern mit `Strg+O`, `Enter`, `Strg+X`. Dann die Datei absichern — sie
|
|
enthält alle Schlüssel:
|
|
|
|
```bash
|
|
chmod 600 .env
|
|
```
|
|
|
|
### Kontrolle
|
|
|
|
```bash
|
|
# Sind alle Pflichtwerte gefüllt? Zeigt nur die Namen, nicht die Werte.
|
|
grep -E '^(POSTGRES_PASSWORD|AUTH_SECRET|AUTH_URL|ENC_KEY_V1|INDEX_KEY|S3_ACCESS_KEY_ID|S3_SECRET_ACCESS_KEY|SICHERUNG_PASSWORT)=.+' .env \
|
|
| sed 's/=.*/ = gesetzt/'
|
|
```
|
|
|
|
Acht Zeilen müssen erscheinen. Fehlt eine, startet die Anwendung nicht —
|
|
und sagt dir beim Start auch, welche.
|
|
|
|
---
|
|
|
|
## Schritt 4 — Starten
|
|
|
|
```bash
|
|
cd /opt/betreuungs-cockpit
|
|
|
|
# Git-Stand mitgeben, damit später sichtbar ist, was läuft
|
|
GIT_COMMIT=$(git rev-parse --short HEAD) \
|
|
BUILD_ZEIT=$(date -Iseconds) \
|
|
docker compose up -d --build
|
|
```
|
|
|
|
Der erste Bau dauert **fünf bis zehn Minuten**. Danach geht es schnell.
|
|
|
|
### Kontrolle
|
|
|
|
```bash
|
|
docker compose ps
|
|
```
|
|
|
|
Vier Dienste, alle `running`, Datenbank und Anwendung `healthy`:
|
|
|
|
```
|
|
NAME STATUS
|
|
betreuungs-cockpit-app-1 Up (healthy)
|
|
betreuungs-cockpit-db-1 Up (healthy)
|
|
betreuungs-cockpit-minio-1 Up (healthy)
|
|
betreuungs-cockpit-sicherung-1 Up
|
|
```
|
|
|
|
```bash
|
|
# Antwortet die Anwendung? (Port anpassen, falls du APP_PORT gesetzt hast)
|
|
curl -s http://127.0.0.1:3000/api/gesundheit
|
|
# {"status":"bereit","version":"1.2.0","commit":"..."}
|
|
```
|
|
|
|
Kommt hier nichts, sieh in die Logs:
|
|
|
|
```bash
|
|
docker compose logs app --tail 50
|
|
```
|
|
|
|
---
|
|
|
|
## Schritt 5 — Datenbank vorbereiten
|
|
|
|
Die **Migrationen laufen beim Start automatisch mit**. Zwei SQL-Skripte
|
|
kommen einmalig von Hand dazu — sie richten die Volltextsuche ein und
|
|
machen das Audit-Log unveränderlich:
|
|
|
|
```bash
|
|
docker compose exec app node node_modules/prisma/build/index.js db execute \
|
|
--schema prisma/schema.prisma --file prisma/sql/volltextsuche.sql
|
|
|
|
docker compose exec app node node_modules/prisma/build/index.js db execute \
|
|
--schema prisma/schema.prisma --file prisma/sql/audit-schutz.sql
|
|
```
|
|
|
|
### Kontrolle
|
|
|
|
Prüf, dass der Schutz des Audit-Logs wirklich greift. Der folgende Befehl
|
|
**muss** mit einem Fehler enden:
|
|
|
|
```bash
|
|
docker compose exec db psql -U cockpit betreuungs_cockpit \
|
|
-c "UPDATE audit_log SET entitaet='x';"
|
|
```
|
|
|
|
Erwartete Ausgabe:
|
|
|
|
```
|
|
ERROR: Das Audit-Log ist unveraenderlich. UPDATE auf audit_log ist nicht zulaessig.
|
|
```
|
|
|
|
Kommt stattdessen `UPDATE 0`, ist das Skript nicht gelaufen. Führ es
|
|
erneut aus — beide Skripte sind so gebaut, dass mehrfaches Ausführen
|
|
nichts kaputtmacht.
|
|
|
|
---
|
|
|
|
## Schritt 6 — Dateiablage einrichten
|
|
|
|
Der Objektspeicher braucht einen Ablagebereich für die Dokumente:
|
|
|
|
```bash
|
|
set -a && source .env && set +a
|
|
|
|
docker compose exec minio mc alias set lokal \
|
|
http://localhost:9000 "$S3_ACCESS_KEY_ID" "$S3_SECRET_ACCESS_KEY"
|
|
|
|
docker compose exec minio mc mb lokal/betreuungs-cockpit
|
|
```
|
|
|
|
### Kontrolle
|
|
|
|
```bash
|
|
docker compose exec minio mc ls lokal
|
|
# [...] betreuungs-cockpit/
|
|
```
|
|
|
|
> **Externer Objektspeicher statt MinIO?** Wenn du einen S3-Dienst in der
|
|
> EU benutzt (z. B. Hetzner Object Storage, Falkenstein), streich den
|
|
> `minio`-Dienst aus der `docker-compose.yml`, entfern ihn aus
|
|
> `depends_on` beim `app`-Dienst und zeig mit `S3_ENDPOINT` dorthin. Den
|
|
> Bucket legst du dann beim Anbieter an. **Auch dafür brauchst du einen
|
|
> AV-Vertrag.**
|
|
|
|
---
|
|
|
|
## Schritt 7 — In den Reverse Proxy eintragen
|
|
|
|
Der Proxy steht auf einem **anderen Server**. Damit sind zwei Dinge zu
|
|
tun: die Anwendung für ihn erreichbar machen, und ihn richtig
|
|
konfigurieren.
|
|
|
|
### 7a — Erreichbar machen, aber nur für den Proxy
|
|
|
|
Standardmäßig horcht die Anwendung auf `127.0.0.1` — von außen kommt dann
|
|
niemand heran, auch der Proxy nicht. Das musst du ändern.
|
|
|
|
> **Die Anwendung beendet kein TLS.** Zwischen Proxy und Anwendung läuft
|
|
> unverschlüsseltes HTTP. Dieser Weg darf niemals durchs offene Netz
|
|
> gehen. Es gibt drei Möglichkeiten, in dieser Reihenfolge:
|
|
|
|
**Erste Wahl: privates Netz.** Hetzner, netcup und die meisten anderen
|
|
bieten ein privates Netz zwischen deinen Servern an. Trag in die `.env`
|
|
die Adresse ein, die dieser Server dort hat:
|
|
|
|
```bash
|
|
APP_BIND=10.0.0.5 # die private Adresse DIESES Servers
|
|
APP_PORT=3000
|
|
```
|
|
|
|
**Zweite Wahl: WireGuard.** Gibt es kein privates Netz, bau einen Tunnel
|
|
zwischen beiden Servern und trag die Tunneladresse ein:
|
|
|
|
```bash
|
|
APP_BIND=10.8.0.2
|
|
```
|
|
|
|
**Notlösung: öffentliche Adresse plus Firewall.** Nur, wenn beides nicht
|
|
geht — und dann zwingend mit einer Regel, die ausschließlich die IP des
|
|
Proxy-Servers durchlässt:
|
|
|
|
```bash
|
|
# in der .env
|
|
APP_BIND=0.0.0.0
|
|
|
|
# und auf DIESEM Server, vor dem Neustart:
|
|
sudo ufw allow from <IP-des-Proxy-Servers> to any port 3000 proto tcp
|
|
sudo ufw deny 3000/tcp
|
|
```
|
|
|
|
Prüf danach von einem dritten Rechner aus, dass der Port wirklich zu ist.
|
|
Vergiss die Regel, und deine Anwendung steht unverschlüsselt im Netz.
|
|
|
|
Nach der Änderung:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
docker compose ps # unter PORTS muss die neue Adresse stehen
|
|
```
|
|
|
|
### 7b — Was auf dem Proxy-Server hinterlegt wird
|
|
|
|
Das ist der Teil, den du auf dem **anderen** Server einträgst. Ersetz
|
|
`10.0.0.5` durch die Adresse aus 7a.
|
|
|
|
Vier Varianten, je nachdem was bei dir läuft: [Nginx Proxy Manager](#nginx-proxy-manager), [Caddy](#caddy), [nginx von Hand](#nginx) oder [Traefik](#traefik).
|
|
|
|
#### Nginx Proxy Manager
|
|
|
|
In der Oberfläche: **Hosts → Proxy Hosts → Add Proxy Host**.
|
|
|
|
**Reiter „Details"**
|
|
|
|
| Feld | Wert |
|
|
|---|---|
|
|
| Domain Names | `cockpit.deine-domain.de` |
|
|
| Scheme | `http` — nicht https. Die Anwendung beendet kein TLS |
|
|
| Forward Hostname / IP | die Adresse aus 7a, z. B. `10.0.0.5` |
|
|
| Forward Port | `3000` (oder dein `APP_PORT`) |
|
|
| Cache Assets | **aus** |
|
|
| Block Common Exploits | an |
|
|
| Websockets Support | an |
|
|
| Access List | leer lassen — die Anwendung hat eine eigene Anmeldung mit Pflicht-Zweitfaktor |
|
|
|
|
Zu **Cache Assets**: Next.js hängt an jede Skript- und Stildatei einen
|
|
Prüfsummen-Namen, ein Zwischenspeicher bringt hier also nichts. Aus bleibt
|
|
er, damit nach einem Update niemand rätselt, ob er noch eine alte Fassung
|
|
sieht.
|
|
|
|
**Reiter „SSL"**
|
|
|
|
| Feld | Wert |
|
|
|---|---|
|
|
| SSL Certificate | *Request a new SSL Certificate* (Let's Encrypt) |
|
|
| Force SSL | **an** |
|
|
| HTTP/2 Support | an |
|
|
| HSTS Enabled | **aus** |
|
|
| HSTS Subdomains | aus |
|
|
|
|
`HSTS Enabled` bleibt aus, weil die Anwendung den Kopf
|
|
`Strict-Transport-Security` bereits selbst setzt — sonst schickt der Server
|
|
ihn zweimal.
|
|
|
|
**Reiter „Advanced"**
|
|
|
|
Hier kommt der Teil hinein, den die Oberfläche nicht abbildet. Genau
|
|
dieser Block:
|
|
|
|
```nginx
|
|
# Fotos mehrseitiger Bescheide sind schnell 10 MB.
|
|
client_max_body_size 30M;
|
|
|
|
# Belegmappe, Rechnungslegung und Vollexport brauchen länger als die
|
|
# üblichen 60 Sekunden. Ohne diese Werte bricht der Download mittendrin ab.
|
|
proxy_connect_timeout 30s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
|
|
# Der Vollexport kommt als Strom. Puffert der Proxy, wartet der Browser,
|
|
# bis alles im Speicher des Proxys liegt - und läuft dabei in die Zeitgrenze.
|
|
proxy_buffering off;
|
|
proxy_request_buffering off;
|
|
```
|
|
|
|
Dann **Save**.
|
|
|
|
**Was du NICHT eintragen musst:** `proxy_set_header` für `Host`,
|
|
`X-Real-IP`, `X-Forwarded-For` und `X-Forwarded-Proto`. Nginx Proxy Manager
|
|
setzt diese vier bereits selbst, und zwar im `location`-Block — ein
|
|
zweiter Satz im Advanced-Feld läge auf `server`-Ebene und würde ohnehin
|
|
überschrieben. Trag sie nicht ein, sonst suchst du später an der falschen
|
|
Stelle.
|
|
|
|
**Wenn du eine `Access List` benutzt:** Sie sperrt auch `/api/wartung` aus.
|
|
Entweder du lässt sie weg, oder du trägst die IP des Servers ein, von dem
|
|
der cron-Eintrag aus Schritt 10 kommt.
|
|
|
|
#### Caddy
|
|
|
|
```caddyfile
|
|
cockpit.deine-domain.de {
|
|
encode zstd gzip
|
|
|
|
reverse_proxy 10.0.0.5:3000 {
|
|
# Berichte und der Vollexport brauchen Zeit. Ohne diese Werte
|
|
# bricht der Proxy mitten im Erzeugen ab.
|
|
transport http {
|
|
read_timeout 10m
|
|
write_timeout 10m
|
|
}
|
|
# Der Vollexport kommt als Strom - nicht puffern, sonst wartet der
|
|
# Browser, bis alles im Speicher des Proxys liegt.
|
|
flush_interval -1
|
|
}
|
|
|
|
# Ein Handyfoto eines mehrseitigen Bescheids ist schnell 10 MB.
|
|
request_body {
|
|
max_size 30MB
|
|
}
|
|
}
|
|
```
|
|
|
|
Caddy setzt `Host`, `X-Forwarded-For` und `X-Forwarded-Proto` von selbst
|
|
richtig — dafür musst du nichts tun.
|
|
|
|
#### nginx
|
|
|
|
```nginx
|
|
server {
|
|
listen 443 ssl;
|
|
http2 on;
|
|
server_name cockpit.deine-domain.de;
|
|
|
|
ssl_certificate /etc/letsencrypt/live/cockpit.deine-domain.de/fullchain.pem;
|
|
ssl_certificate_key /etc/letsencrypt/live/cockpit.deine-domain.de/privkey.pem;
|
|
|
|
# Uploads von Behördenschreiben
|
|
client_max_body_size 30M;
|
|
|
|
location / {
|
|
proxy_pass http://10.0.0.5:3000;
|
|
proxy_http_version 1.1;
|
|
|
|
# Ohne diese vier Zeilen funktioniert die Anwendung nicht richtig,
|
|
# siehe Tabelle unter 7c.
|
|
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 $scheme;
|
|
|
|
# PDF-Erzeugung und Vollexport dauern länger als die üblichen 60 s.
|
|
proxy_connect_timeout 30s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
|
|
# Der Vollexport kommt als Strom. Ohne diese beiden Zeilen sammelt
|
|
# nginx erst alles ein, und der Browser sieht minutenlang nichts.
|
|
proxy_buffering off;
|
|
proxy_request_buffering off;
|
|
}
|
|
}
|
|
|
|
# Alles Unverschlüsselte umleiten
|
|
server {
|
|
listen 80;
|
|
server_name cockpit.deine-domain.de;
|
|
return 301 https://$host$request_uri;
|
|
}
|
|
```
|
|
|
|
Aktivieren:
|
|
|
|
```bash
|
|
sudo ln -s /etc/nginx/sites-available/cockpit /etc/nginx/sites-enabled/
|
|
sudo certbot --nginx -d cockpit.deine-domain.de
|
|
sudo nginx -t && sudo systemctl reload nginx
|
|
```
|
|
|
|
#### Traefik
|
|
|
|
Weil die Anwendung auf einem anderen Server läuft, geht es **nicht** über
|
|
Docker-Labels, sondern über eine dynamische Konfigurationsdatei:
|
|
|
|
```yaml
|
|
http:
|
|
routers:
|
|
cockpit:
|
|
rule: "Host(`cockpit.deine-domain.de`)"
|
|
service: cockpit
|
|
entryPoints: [websecure]
|
|
tls:
|
|
certResolver: letsencrypt
|
|
|
|
services:
|
|
cockpit:
|
|
loadBalancer:
|
|
servers:
|
|
- url: "http://10.0.0.5:3000"
|
|
# Vollexport als Strom ausliefern, nicht sammeln
|
|
responseForwarding:
|
|
flushInterval: -1
|
|
```
|
|
|
|
Dazu in der statischen Konfiguration großzügige Zeitgrenzen:
|
|
|
|
```yaml
|
|
entryPoints:
|
|
websecure:
|
|
address: ":443"
|
|
transport:
|
|
respondingTimeouts:
|
|
readTimeout: 600s
|
|
writeTimeout: 600s
|
|
idleTimeout: 180s
|
|
```
|
|
|
|
### 7c — Was der Proxy leisten muss, und warum
|
|
|
|
| Anforderung | Was ohne sie passiert |
|
|
|---|---|
|
|
| **TLS beenden** | Der Browser lehnt Cookies mit `Secure`-Kennzeichen ab — die Anmeldung schlägt stumm fehl |
|
|
| **`Host` durchreichen** | Auth.js vergleicht den Host mit `AUTH_URL` und weist die Anmeldung ab |
|
|
| **`X-Forwarded-Proto: https`** | Die Anwendung baut Adressen mit `http` und setzt Cookies ohne `Secure` |
|
|
| **`X-Forwarded-For`** | Die Login-Sperre zählt alle Zugriffe als denselben Anschluss: Fünf Fehlversuche irgendwo sperren alle. Im Audit-Log fehlt die Herkunft |
|
|
| **Mindestens 30 MB Uploads** | Fotos mehrseitiger Bescheide werden abgeschnitten |
|
|
| **Zeitgrenze 10 Minuten** | Belegmappe, Rechnungslegung und Vollexport brechen mitten im Erzeugen ab |
|
|
| **Antwort nicht puffern** | Der Vollexport kommt als Strom. Puffert der Proxy, wartet der Browser, bis alles im Speicher des Proxys liegt — bei großen Beständen läuft er dabei in die Zeitgrenze |
|
|
|
|
**Keine eigenen Sicherheitskopfzeilen setzen.** CSP, `X-Frame-Options`,
|
|
`nosniff` und HSTS kommen aus der Anwendung. Besonders die CSP darf der
|
|
Proxy nicht überschreiben: Sie enthält eine Nonce, die pro Anfrage neu
|
|
ist. Eine feste CSP aus dem Proxy blockiert die eigenen Skripte — die
|
|
Seite lädt dann zwar, lässt sich aber nicht bedienen.
|
|
|
|
### Kontrolle
|
|
|
|
Erst **vom Proxy-Server aus** — kommt er überhaupt an?
|
|
|
|
```bash
|
|
curl -s http://10.0.0.5:3000/api/gesundheit
|
|
# {"status":"bereit","version":"1.2.0",...}
|
|
```
|
|
|
|
Kommt hier nichts, liegt es an 7a: falsche `APP_BIND`, Firewall, oder der
|
|
Behälter wurde nach der Änderung nicht neu gestartet.
|
|
|
|
Dann **von deinem eigenen Rechner aus**:
|
|
|
|
```bash
|
|
curl -sI https://cockpit.deine-domain.de/anmelden | head -8
|
|
```
|
|
|
|
Erwartet: `HTTP/2 200` und die Kopfzeilen `content-security-policy`,
|
|
`x-frame-options: DENY`, `x-content-type-options: nosniff`,
|
|
`strict-transport-security`.
|
|
|
|
Und die Gegenprobe — der Port darf von außen **nicht** offen sein:
|
|
|
|
```bash
|
|
curl -s --max-time 5 http://<oeffentliche-IP-des-App-Servers>:3000/api/gesundheit
|
|
# muss in eine Zeitüberschreitung laufen oder abgewiesen werden
|
|
```
|
|
|
|
Antwortet er, ist deine Firewallregel falsch. Das ist kein
|
|
Schönheitsfehler: Über diesen Weg käme man unverschlüsselt an
|
|
Gesundheitsdaten.
|
|
|
|
## Schritt 8 — Ersteinrichtung im Browser
|
|
|
|
Ruf `https://cockpit.deine-domain.de` auf. Ein Assistent führt dich durch
|
|
drei Schritte: Zugang anlegen, zweiten Faktor einrichten, betreute Person
|
|
erfassen.
|
|
|
|
Die Einzelheiten stehen in der [Bedienungsanleitung](ANLEITUNG.md),
|
|
Abschnitt *Der erste Tag*. Drei Dinge sind hier wichtig:
|
|
|
|
1. **Die Seite `/ersteinrichtung` sperrt sich selbst**, sobald ein Zugang
|
|
existiert. Niemand kann sich danach von außen einen zweiten
|
|
Administrator anlegen.
|
|
2. **Die Wiederherstellungscodes werden genau einmal angezeigt.** Druck
|
|
sie aus, bevor du weiterklickst.
|
|
3. **Das Bestellungsdatum** muss stimmen — daraus entstehen alle
|
|
gesetzlichen Fristen.
|
|
|
|
Zum Schluss: **Fristen → Pflichtfristen ableiten**.
|
|
|
|
### Kontrolle
|
|
|
|
- Du kommst mit E-Mail, Passwort und dem Code aus der App herein.
|
|
- Unter **Fristen** stehen Einträge mit Paragraphenbezug.
|
|
- Unter **Protokoll** siehst du deine eigene Anmeldung.
|
|
- Unten links in der Seitenleiste steht `Version 1.2.0 · <commit>`.
|
|
|
|
---
|
|
|
|
## Schritt 9 — Sicherung prüfen
|
|
|
|
Die tägliche Sicherung läuft ab jetzt um **03:15 Uhr**. Warte nicht bis
|
|
morgen — stoß sie einmal von Hand an:
|
|
|
|
```bash
|
|
cd /opt/betreuungs-cockpit
|
|
docker compose exec sicherung /bin/sh /sicherung.sh
|
|
```
|
|
|
|
Erwartete Ausgabe, in dieser Reihenfolge:
|
|
|
|
```
|
|
Sicherung beginnt: /backups/cockpit_2026-08-22_1430.dump.enc
|
|
Sicherung fertig (1.2M)
|
|
Selbstpruefung bestanden.
|
|
```
|
|
|
|
Die **Selbstprüfung** ist der eigentliche Punkt: Das Skript entschlüsselt
|
|
die eben geschriebene Datei sofort wieder. Schlägt das fehl, endet der
|
|
Lauf mit einem Fehler statt mit einem stillen Scheinerfolg.
|
|
|
|
### Und jetzt einmal zurückspielen
|
|
|
|
Ein Backup, das nie zurückgespielt wurde, ist kein Backup. Probier es
|
|
gleich aus, solange nichts drinsteht, was weh tut:
|
|
|
|
```bash
|
|
# Testdatenbank neben der echten anlegen
|
|
docker compose exec db createdb -U cockpit probe
|
|
|
|
# Zurückspielen (Dateinamen anpassen)
|
|
docker compose exec -e PGDATABASE=probe -e SICHERUNG_OHNE_RUECKFRAGE=ja \
|
|
sicherung /bin/sh /wiederherstellung.sh /backups/cockpit_<datum>.dump.enc
|
|
|
|
# Stichprobe
|
|
docker compose exec db psql -U cockpit probe \
|
|
-c "SELECT vorname, nachname FROM betreute;"
|
|
|
|
# Aufräumen
|
|
docker compose exec db dropdb -U cockpit probe
|
|
```
|
|
|
|
### Kontrolle
|
|
|
|
```bash
|
|
ls -lh backups/
|
|
# cockpit_....dump.enc und cockpit_....dump.enc.sha256
|
|
```
|
|
|
|
> **Was das Backup nicht enthält:** den Schlüssel `ENC_KEY_V1` und die
|
|
> Dateien im Objektspeicher. Für die Dateien:
|
|
>
|
|
> ```bash
|
|
> docker compose exec minio mc mirror lokal/betreuungs-cockpit /backups/dokumente
|
|
> ```
|
|
>
|
|
> Und kopier die Sicherungen regelmäßig **weg vom Server** — ein Backup,
|
|
> das neben dem Original liegt, hilft bei einem Serverausfall nicht.
|
|
|
|
---
|
|
|
|
## Schritt 10 — Wartungslauf einrichten
|
|
|
|
Einmal täglich: Fristerinnerungen verschicken, Papierkorb leeren, alte
|
|
Anmeldeversuche entfernen.
|
|
|
|
```bash
|
|
# Schlüssel für den Endpunkt ausrechnen
|
|
docker compose exec app node -e "
|
|
console.log(require('crypto').createHmac('sha256', process.env.AUTH_SECRET)
|
|
.update('wartung').digest('base64url'))
|
|
"
|
|
```
|
|
|
|
Den ausgegebenen Wert in einen cron-Eintrag:
|
|
|
|
```bash
|
|
crontab -e
|
|
```
|
|
|
|
Diese Zeile ans Ende:
|
|
|
|
```cron
|
|
30 4 * * * curl -fsS -H "X-Wartung-Schluessel: <ausgegebener Wert>" https://cockpit.deine-domain.de/api/wartung > /dev/null
|
|
```
|
|
|
|
### Kontrolle
|
|
|
|
```bash
|
|
curl -s -H "X-Wartung-Schluessel: <wert>" \
|
|
https://cockpit.deine-domain.de/api/wartung
|
|
# {"erinnerungen":0,"papierkorbGeleert":0,...,"fehler":[]}
|
|
```
|
|
|
|
Ohne gültigen Schlüssel antwortet die Route mit **404**, nicht mit 401 —
|
|
wer die Adresse errät, soll nicht einmal erfahren, dass es sie gibt.
|
|
|
|
---
|
|
|
|
## Schritt 11 — Schlüssel in Sicherheit bringen
|
|
|
|
Der letzte Schritt, und der wichtigste.
|
|
|
|
**`ENC_KEY_V1` gehört an zwei Orte, keiner davon ist dieser Server:**
|
|
|
|
1. In deinen **Passwortmanager**.
|
|
2. **Ausgedruckt** zu deinen Betreuungsunterlagen, im Ordner beim
|
|
Betreuerausweis.
|
|
|
|
Dasselbe gilt für `SICHERUNG_PASSWORT` — ohne das nützt dir die
|
|
verschlüsselte Sicherungsdatei nichts.
|
|
|
|
```bash
|
|
# Zum Abschreiben oder Kopieren
|
|
grep -E '^(ENC_KEY_V1|SICHERUNG_PASSWORT)=' .env
|
|
```
|
|
|
|
**Warum so umständlich?** Weil der Schlüssel bewusst nicht in der
|
|
Datenbank liegt. Ein gestohlenes Backup ist damit wertlos — und ein
|
|
verlorener Schlüssel macht auch dein eigenes Backup wertlos. Beides ist
|
|
dieselbe Eigenschaft, von zwei Seiten betrachtet.
|
|
|
|
Denk an die letzten Pflichten:
|
|
|
|
- **AV-Vertrag** mit Hoster und Objektspeicher abgeschlossen?
|
|
- [`verarbeitungsverzeichnis.md`](verarbeitungsverzeichnis.md) ausgefüllt?
|
|
- [`datenschutzhinweise.md`](datenschutzhinweise.md) ausgefüllt und der
|
|
betreuten Person ausgehändigt oder vorgelesen?
|
|
|
|
---
|
|
|
|
## Aktualisieren
|
|
|
|
```bash
|
|
cd /opt/betreuungs-cockpit
|
|
./scripts/update.sh
|
|
```
|
|
|
|
> **Kommst du von 1.1.0 oder älter?** Dort lag `update.sh` ohne
|
|
> Ausführungsrecht im Repository — `./scripts/update.sh` endet dann in
|
|
> „Permission denied". Einmalig hilft:
|
|
>
|
|
> ```bash
|
|
> bash scripts/update.sh
|
|
> ```
|
|
>
|
|
> Ab 1.1.1 ist das Recht gesetzt und der Punktschrägstrich funktioniert.
|
|
|
|
Das war es. Das Skript nimmt dir die Schritte ab, die man von Hand
|
|
vergisst:
|
|
|
|
1. Prüft Verzeichnis, git, docker und `.env`
|
|
2. Holt die Fassungen und zeigt, was zwischen deinem Stand und dem neuen
|
|
im Changelog steht
|
|
3. Fragt nach
|
|
4. **Stößt eine Sicherung an** — und bricht ab, wenn sie nicht klappt
|
|
5. Legt eigene Änderungen an versionierten Dateien beiseite, schaltet um,
|
|
baut neu
|
|
6. Wartet, bis die Anwendung antwortet
|
|
7. Führt die **beiden SQL-Skripte** aus (Volltextsuche, Audit-Schutz)
|
|
8. Prüft, ob wirklich die neue Fassung läuft
|
|
|
|
**Bis Schritt 3 wird nichts verändert.** Bricht etwas vorher ab, findest du
|
|
alles so vor wie zuvor.
|
|
|
|
### Aufrufmöglichkeiten
|
|
|
|
| Aufruf | Wirkung |
|
|
|---|---|
|
|
| `./scripts/update.sh` | auf die neueste veröffentlichte Fassung |
|
|
| `./scripts/update.sh v1.2.0` | auf eine bestimmte Fassung |
|
|
| `./scripts/update.sh --pruefen` | nur nachsehen, nichts ändern |
|
|
| `./scripts/update.sh --ja` | ohne Rückfrage (für cron) |
|
|
| `./scripts/update.sh --verwerfen` | eigene Änderungen wegwerfen statt beiseitelegen |
|
|
| `./scripts/update.sh --ohne-sicherung` | Sicherung überspringen — sehr gut überlegen |
|
|
|
|
### Eigene Änderungen an der docker-compose.yml
|
|
|
|
Das ist der Normalfall: Man passt etwas an, bis es läuft. Das Skript legt
|
|
solche Änderungen vor dem Umschalten mit `git stash` beiseite und sagt,
|
|
wie man sie zurückholt:
|
|
|
|
```bash
|
|
git stash list # was liegt da
|
|
git stash pop # zurückholen
|
|
git stash drop # wegwerfen
|
|
```
|
|
|
|
Sind deine Anpassungen inzwischen im Projekt eingearbeitet — wie
|
|
`APP_BIND` oder `MINIO_KONSOLE_PORT` —, brauchst du sie nicht
|
|
zurückzuholen. Dann setz die Werte einfach in der `.env`.
|
|
|
|
> **Die `.env` wird nie angefasst.** Sie ist nicht versioniert; weder
|
|
> `git checkout` noch das Skript rühren sie an.
|
|
|
|
### Von Hand, wenn du lieber selbst steuerst
|
|
|
|
```bash
|
|
cd /opt/betreuungs-cockpit
|
|
|
|
# 1. IMMER zuerst sichern - eine Migration lässt sich nicht zurückrollen
|
|
docker compose exec sicherung /bin/sh /sicherung.sh
|
|
|
|
# 2. Was ändert sich?
|
|
git fetch --tags
|
|
git log --oneline HEAD..origin/main
|
|
|
|
# 3. Umschalten
|
|
git checkout v1.2.0
|
|
|
|
# 4. Neu bauen und starten
|
|
GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_ZEIT=$(date -Iseconds) docker compose up -d --build
|
|
|
|
# 5. Die beiden SQL-Skripte - sie laufen NICHT mit den Migrationen mit
|
|
docker compose exec app node node_modules/prisma/build/index.js db execute --schema prisma/schema.prisma --file prisma/sql/volltextsuche.sql
|
|
docker compose exec app node node_modules/prisma/build/index.js db execute --schema prisma/schema.prisma --file prisma/sql/audit-schutz.sql
|
|
```
|
|
|
|
### Kontrolle
|
|
|
|
```bash
|
|
curl -s https://cockpit.deine-domain.de/api/gesundheit
|
|
# version muss die neue sein
|
|
```
|
|
|
|
Steht dort noch die alte Version, läuft der alte Behälter weiter — dann
|
|
hat der Bau nicht durchgezogen:
|
|
|
|
```bash
|
|
docker compose up -d --build --force-recreate app
|
|
```
|
|
|
|
### Zurück auf den vorherigen Stand
|
|
|
|
```bash
|
|
git checkout v1.0.4
|
|
docker compose up -d --build
|
|
```
|
|
|
|
Hat die neue Fassung die Datenbank migriert, reicht das nicht — dann muss
|
|
auch die Sicherung zurück (siehe Schritt 9).
|
|
|
|
---
|
|
|
|
## Wenn etwas nicht klappt
|
|
|
|
### Ein Dienst startet nicht
|
|
|
|
```bash
|
|
docker compose ps
|
|
docker compose logs app --tail 100
|
|
docker compose logs db --tail 50
|
|
```
|
|
|
|
Fehlt eine Umgebungsvariable, sagt die Anwendung beim Start, welche.
|
|
|
|
### „Umgebungsvariable ENC_KEY_V1 fehlt"
|
|
|
|
Die `.env` liegt nicht neben der `docker-compose.yml`, oder die Zeile ist
|
|
leer. Prüf mit dem Befehl aus Schritt 3.
|
|
|
|
### Der Proxy erreicht die Anwendung nicht („502", „Connection refused")
|
|
|
|
Fast immer steht in der `.env` noch `APP_BIND=127.0.0.1`, während der
|
|
Proxy auf einem anderen Server läuft. Dann horcht die Anwendung nur auf
|
|
der Loopback-Adresse — erreichbar ausschließlich vom Anwendungsserver
|
|
selbst.
|
|
|
|
Prüf, worauf sie tatsächlich horcht:
|
|
|
|
```bash
|
|
docker compose ps
|
|
# unter PORTS muss die Adresse stehen, die der Proxy erreicht,
|
|
# NICHT 127.0.0.1
|
|
```
|
|
|
|
Und ob sie überhaupt antwortet — erst lokal, dann vom Proxy-Server aus:
|
|
|
|
```bash
|
|
# auf dem Anwendungsserver
|
|
curl -s http://127.0.0.1:3000/api/gesundheit
|
|
|
|
# auf dem Proxy-Server
|
|
curl -s http://<APP_BIND-Adresse>:3000/api/gesundheit
|
|
```
|
|
|
|
Antwortet der erste, aber nicht der zweite, ist `APP_BIND` falsch oder
|
|
eine Firewall dazwischen. Setz den richtigen Wert (Schritt 7a) und
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
Ein Neubau des Abbilds ist dafür **nicht** nötig — es ist nur eine
|
|
Umgebungsänderung, der Behälter wird neu erzeugt.
|
|
|
|
### Port 3000 ist schon belegt
|
|
|
|
```bash
|
|
sudo ss -tlnp | grep 3000
|
|
```
|
|
|
|
Setz `APP_PORT=3010` in der `.env`, dann `docker compose up -d`, und zeig
|
|
mit dem Proxy auf den neuen Port.
|
|
|
|
### Die Anmeldung landet wieder auf dem Formular, ohne Fehlermeldung
|
|
|
|
`AUTH_URL` stimmt nicht mit der aufgerufenen Adresse überein. Häufig:
|
|
`http` statt `https`, oder ein Schrägstrich am Ende. Nach dem Ändern:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
### „Zu viele Fehlversuche"
|
|
|
|
Nach fünf Fehlversuchen wird für eine Viertelstunde gesperrt — auch dich.
|
|
Warte ab, oder setz die Zähler zurück:
|
|
|
|
```bash
|
|
docker compose exec db psql -U cockpit betreuungs_cockpit \
|
|
-c "DELETE FROM login_versuch WHERE erfolg = false;"
|
|
```
|
|
|
|
### Der Code aus der App wird nicht angenommen
|
|
|
|
Die Uhr des Telefons geht falsch. TOTP-Codes hängen an der Uhrzeit. Stell
|
|
die automatische Zeitsynchronisierung ein und prüf, dass auch der Server
|
|
richtig geht:
|
|
|
|
```bash
|
|
timedatectl
|
|
docker compose exec app date
|
|
```
|
|
|
|
### Der Knopf „Mit Passkey anmelden" erscheint nicht
|
|
|
|
Passkeys brauchen einen sicheren Kontext. Die Anwendung bietet sie nur an,
|
|
wenn `AUTH_URL` auf `https://` steht (oder auf `localhost`) — sonst
|
|
erscheint der Knopf bewusst gar nicht erst, statt beim Klick mit einer
|
|
kryptischen Browsermeldung abzubrechen.
|
|
|
|
```bash
|
|
docker compose exec app printenv AUTH_URL # muss mit https:// beginnen
|
|
```
|
|
|
|
Steht dort `http://`, korrigiere die `.env` und starte neu:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
Erscheint der Knopf weiterhin nicht, kann der Browser kein WebAuthn — das
|
|
betrifft praktisch nur sehr alte Fassungen.
|
|
|
|
### Ein Passkey lässt sich einrichten, aber nicht zum Anmelden benutzen
|
|
|
|
Fast immer stimmt die Adresse nicht überein. Der Schlüssel ist an den
|
|
Hostnamen aus `AUTH_URL` gebunden; der Browser vergleicht ihn mit dem, was
|
|
in der Adresszeile steht.
|
|
|
|
```bash
|
|
docker compose exec app printenv AUTH_URL
|
|
# muss exakt die Adresse sein, die du im Browser eintippst
|
|
```
|
|
|
|
Typische Ursachen: `AUTH_URL` zeigt auf `cockpit.example.de`, aufgerufen
|
|
wird aber `www.cockpit.example.de` — oder umgekehrt. Ein Passkey für die
|
|
eine Adresse gilt auf der anderen nicht.
|
|
|
|
**Nach einem Wechsel der Domain** müssen alle Passkeys neu eingerichtet
|
|
werden. Die alten Einträge lassen sich unter *Mein Konto → Passkeys*
|
|
entfernen. Die Anmeldung mit E-Mail, Passwort und Code funktioniert in der
|
|
Zwischenzeit unverändert — man sperrt sich damit also nicht aus.
|
|
|
|
### Die Texterkennung findet nichts
|
|
|
|
```bash
|
|
docker compose exec app tesseract --list-langs
|
|
# deu muss dabei sein
|
|
```
|
|
|
|
Fehlt sie, ist das Abbild nicht vollständig gebaut:
|
|
`docker compose build --no-cache app`.
|
|
|
|
### Uploads schlagen bei großen Dateien fehl
|
|
|
|
Der Proxy schneidet ab. Bei Caddy `request_body max_size`, bei nginx
|
|
`client_max_body_size` — beide auf mindestens 30 MB.
|
|
|
|
### Die Seite lädt, aber ohne Gestaltung, und nichts lässt sich anklicken
|
|
|
|
Dein Proxy überschreibt die Content-Security-Policy der Anwendung. Sie
|
|
enthält eine Nonce, die pro Anfrage neu ist — eine feste CSP aus dem Proxy
|
|
blockiert dann die eigenen Skripte. Nimm die Kopfzeile aus dem Proxy
|
|
heraus.
|
|
|
|
### Der Speicherplatz wird knapp
|
|
|
|
```bash
|
|
df -h
|
|
docker system prune -a # entfernt ungenutzte Abbilder
|
|
```
|
|
|
|
**Ohne `--volumes`**, sonst löschst du die Datenbank.
|
|
|
|
### Ganz von vorn
|
|
|
|
```bash
|
|
docker compose down -v # ACHTUNG: löscht ALLE Daten
|
|
docker compose up -d --build
|
|
```
|
|
|
|
Das `-v` entfernt die Datenbank. Nur, wenn du das wirklich willst.
|
|
|
|
---
|
|
|
|
## Abnahmeliste
|
|
|
|
Hak das durch, bevor du echte Daten einträgst:
|
|
|
|
- [ ] AV-Vertrag mit dem Hoster abgeschlossen
|
|
- [ ] AV-Vertrag für den Objektspeicher abgeschlossen (falls extern)
|
|
- [ ] Server steht in Deutschland oder mindestens in der EU
|
|
- [ ] `.env` hat Rechte `600`
|
|
- [ ] Alle vier Dienste laufen, Datenbank und Anwendung `healthy`
|
|
- [ ] `/api/gesundheit` antwortet mit der erwarteten Version
|
|
- [ ] **Audit-Schutz greift** — `UPDATE audit_log` endet mit Fehler
|
|
- [ ] Bucket im Objektspeicher angelegt
|
|
- [ ] `https://` funktioniert, Zertifikat gültig
|
|
- [ ] Sicherheitskopfzeilen kommen durch den Proxy an
|
|
- [ ] **Der Anwendungsport ist von außen nicht erreichbar** (Gegenprobe aus Schritt 7)
|
|
- [ ] Der Weg Proxy → Anwendung läuft über ein privates Netz oder einen Tunnel
|
|
- [ ] Zweiter Faktor eingerichtet
|
|
- [ ] **Wiederherstellungscodes ausgedruckt** und weggelegt
|
|
- [ ] Bestellungsdatum eingetragen, Pflichtfristen abgeleitet
|
|
- [ ] Sicherung von Hand angestoßen, Selbstprüfung bestanden
|
|
- [ ] Sicherung **einmal zurückgespielt** und geprüft
|
|
- [ ] Sicherungen werden vom Server weg kopiert
|
|
- [ ] Wartungslauf im cron eingetragen und einmal getestet
|
|
- [ ] **`ENC_KEY_V1` im Passwortmanager und ausgedruckt**
|
|
- [ ] `SICHERUNG_PASSWORT` ebenso
|
|
- [ ] Verarbeitungsverzeichnis ausgefüllt
|
|
- [ ] Datenschutzhinweise ausgefüllt und ausgehändigt
|
|
|
|
Ist alles abgehakt, kannst du anfangen. Die
|
|
[Bedienungsanleitung](ANLEITUNG.md) sagt dir, wie.
|