Die Anleitung sagt "./scripts/update.sh" - auf Linux kam dort "Permission denied". Die drei .sh-Dateien lagen mit Modus 644 statt 755 im Index. Ursache ist der Entwicklungsrechner: Unter Windows steht core.filemode=false, git nimmt ein fehlendes Ausfuehrungsrecht dort gar nicht wahr. Gemeldet hat es der Betrieb, nicht der Bau. Wer von 1.1.0 oder aelter kommt, braucht einmalig "bash scripts/update.sh" - das alte Skript ist ja noch das nicht ausfuehrbare. Steht jetzt in README und Installationsanleitung. Zusaetzlich setzt update.sh nach dem Umschalten selbst ein chmod +x auf scripts/*.sh. Das faengt den Fall ab, dass jemand Dateien von Hand kopiert und das Recht dabei verliert - genau so kam man bisher an dieses Skript, bevor es in der laufenden Fassung existierte. Neu: tests/skripte.test.ts prueft nicht Verhalten, sondern Verpackung - Ausfuehrungsrecht im Index, Shebang, scripts/ im Docker-Abbild, die Sicherungsskripte in der Compose-Datei. Es ist der zweite Fehler dieser Art (vorher fehlte scripts/ ganz im Abbild, 1.0.1); beide Male war der Code richtig und kein Test sah hin, wie die Dateien beim Server ankommen. Gegenprobe gemacht: mit zurueckgesetztem Recht schlaegt er fehl und nennt die Datei. Nebenbei aufgefallen: scripts/version.mjs datierte in UTC. Diese Fassung waere um 00:08 deutscher Zeit auf den Vortag datiert worden. Rechnet jetzt in Europe/Berlin.
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.1.1
|
|
```
|
|
|
|
### Kontrolle
|
|
|
|
```bash
|
|
ls docker-compose.yml Dockerfile .env.example # alle drei da
|
|
git describe --tags # v1.1.1
|
|
```
|
|
|
|
---
|
|
|
|
## 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.1.1","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.1.1",...}
|
|
```
|
|
|
|
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.1.1 · <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.1.1
|
|
|
|
# 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.
|