Die Anleitung setzt jetzt voraus, was ohnehin da ist: ein Server mit laufendem Docker und ein Reverse Proxy, der schon steht - auch auf einem anderen Rechner. Docker-Installation und Betriebssystemhaertung sind raus, dafuer ist der Proxy-Abschnitt der ausfuehrlichste geworden. Damit ein externer Proxy die Anwendung ueberhaupt erreicht, war eine Aenderung noetig: Die Bindeadresse stand fest auf 127.0.0.1. Jetzt gibt es APP_BIND, Standard bleibt die Loopback-Adresse. Der Weg dorthin ist nach Vorzug sortiert - privates Netz, dann WireGuard, dann als Notloesung oeffentliche Adresse mit Firewallregel. Zwischen Proxy und Anwendung laeuft unverschluesseltes HTTP; das gehoert nicht ins offene Netz. Deshalb steht in der Kontrolle eine Gegenprobe, die fehlschlagen MUSS: Antwortet der Anwendungsport von aussen, ist die Firewallregel falsch. Fertige Konfigurationen fuer Caddy, nginx und Traefik, jeweils mit den Werten, die sonst uebersehen werden: - Zeitgrenze 10 Minuten - sonst brechen Belegmappe, Rechnungslegung und Vollexport mitten im Erzeugen ab. Die Standardwerte von nginx liegen bei 60 Sekunden. - Antwort nicht puffern - der Vollexport kommt als Strom. Puffert der Proxy, wartet der Browser, bis alles im Speicher des Proxys liegt, und laeuft dabei in die Zeitgrenze. - Und der Hinweis, dass der Proxy KEINE eigene CSP setzen darf: Sie enthaelt eine Nonce, die pro Anfrage neu ist. Eine feste CSP blockiert die eigenen Skripte - die Seite laedt dann, laesst sich aber nicht bedienen. Der A-Record zeigt auf den Proxy-Server, nicht auf den Anwendungsserver. Stand vorher falsch da. Etappe: 8 Phase: Haertung
27 KiB
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.
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 bringt dich in zehn Minuten auf dem eigenen Rechner zum Laufen.
Inhalt
- Schritt 0 — Voraussetzungen prüfen
- Schritt 1 — Anwendung holen
- Schritt 2 — Schlüssel erzeugen
- Schritt 3 — Konfiguration ausfüllen
- Schritt 4 — Starten
- Schritt 5 — Datenbank vorbereiten
- Schritt 6 — Dateiablage einrichten
- Schritt 7 — In den Reverse Proxy eintragen
- Schritt 8 — Ersteinrichtung im Browser
- Schritt 9 — Sicherung prüfen
- Schritt 10 — Wartungslauf einrichten
- Schritt 11 — Schlüssel in Sicherheit bringen
- Aktualisieren
- Wenn etwas nicht klappt
- 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:
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:
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:
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.
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
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.0.0
Kontrolle
ls docker-compose.yml Dockerfile .env.example # alle drei da
git describe --tags # v1.0.0
Schritt 2 — Schlüssel erzeugen
Die Anwendung braucht sieben Geheimnisse. Erzeuge sie auf dem Server, nicht auf dem eigenen Rechner:
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_V1ist 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
cp .env.example .env
nano .env
Diese Werte musst du setzen:
# --- 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>
AUTH_URLmuss exakt der Adresse entsprechen, unter der du die Anwendung später aufrufst — mithttps://, 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.
Läuft auf dem Server schon etwas auf Port 3000? Dann weich aus:
APP_PORT=3010
Auf welcher Adresse die Anwendung horcht, legst du in
Schritt 7 fest — dort steht
auch, warum 127.0.0.1 nicht reicht, wenn der Proxy woanders läuft.
Optional, aber empfohlen — Fristerinnerungen per E-Mail:
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:
chmod 600 .env
Kontrolle
# 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
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
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
# 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.0.0","commit":"..."}
Kommt hier nichts, sieh in die Logs:
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:
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:
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:
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
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 derdocker-compose.yml, entfern ihn ausdepends_onbeimapp-Dienst und zeig mitS3_ENDPOINTdorthin. 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:
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:
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:
# 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:
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.
Caddy
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
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:
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:
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:
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?
curl -s http://10.0.0.5:3000/api/gesundheit
# {"status":"bereit","version":"1.0.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:
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:
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, Abschnitt Der erste Tag. Drei Dinge sind hier wichtig:
- Die Seite
/ersteinrichtungsperrt sich selbst, sobald ein Zugang existiert. Niemand kann sich danach von außen einen zweiten Administrator anlegen. - Die Wiederherstellungscodes werden genau einmal angezeigt. Druck sie aus, bevor du weiterklickst.
- 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.0.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:
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:
# 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
ls -lh backups/
# cockpit_....dump.enc und cockpit_....dump.enc.sha256
Was das Backup nicht enthält: den Schlüssel
ENC_KEY_V1und die Dateien im Objektspeicher. Für die Dateien:docker compose exec minio mc mirror lokal/betreuungs-cockpit /backups/dokumenteUnd 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.
# 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:
crontab -e
Diese Zeile ans Ende:
30 4 * * * curl -fsS -H "X-Wartung-Schluessel: <ausgegebener Wert>" https://cockpit.deine-domain.de/api/wartung > /dev/null
Kontrolle
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:
- In deinen Passwortmanager.
- Ausgedruckt zu deinen Betreuungsunterlagen, im Ordner beim Betreuerausweis.
Dasselbe gilt für SICHERUNG_PASSWORT — ohne das nützt dir die
verschlüsselte Sicherungsdatei nichts.
# 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.mdausgefüllt?datenschutzhinweise.mdausgefüllt und der betreuten Person ausgehändigt oder vorgelesen?
Aktualisieren
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? Lies den Changelog, bevor du ziehst
git fetch --tags
git log --oneline HEAD..origin/main
# 3. Auf die neue Fassung wechseln
git checkout v1.1.0
# 4. Neu bauen und starten
GIT_COMMIT=$(git rev-parse --short HEAD) \
BUILD_ZEIT=$(date -Iseconds) \
docker compose up -d --build
Die Migrationen laufen beim Start mit. Sagt der Changelog, dass sich ein SQL-Skript geändert hat, führ die beiden Befehle aus Schritt 5 erneut aus.
Kontrolle
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. Sieh in docker compose logs app.
Wenn etwas nicht klappt
Ein Dienst startet nicht
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.
Port 3000 ist schon belegt
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:
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:
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:
timedatectl
docker compose exec app date
Die Texterkennung findet nichts
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
df -h
docker system prune -a # entfernt ungenutzte Abbilder
Ohne --volumes, sonst löschst du die Datenbank.
Ganz von vorn
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
.envhat Rechte600- Alle vier Dienste laufen, Datenbank und Anwendung
healthy /api/gesundheitantwortet mit der erwarteten Version- Audit-Schutz greift —
UPDATE audit_logendet 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_V1im Passwortmanager und ausgedrucktSICHERUNG_PASSWORTebenso- Verarbeitungsverzeichnis ausgefüllt
- Datenschutzhinweise ausgefüllt und ausgehändigt
Ist alles abgehakt, kannst du anfangen. Die Bedienungsanleitung sagt dir, wie.