# Installation Schritt für Schritt Von einem leeren Server bis zur laufenden Anwendung. Rechne mit **60 bis 90 Minuten**, wenn du es zum ersten Mal machst. Diese Anleitung setzt voraus, dass du dich per SSH auf einem Linux-Server anmelden und Befehle eintippen kannst. Mehr nicht — jeder Schritt steht ausgeschrieben da, und nach jedem Abschnitt steht eine **Kontrolle**, mit der du prüfst, ob es geklappt hat, bevor du weitermachst. > 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. Diese Anleitung hier > ist für den echten Betrieb. --- ## Inhalt - [Schritt 0 — Was du vorher brauchst](#schritt-0--was-du-vorher-brauchst) - [Schritt 1 — Server vorbereiten](#schritt-1--server-vorbereiten) - [Schritt 2 — Docker installieren](#schritt-2--docker-installieren) - [Schritt 3 — Anwendung holen](#schritt-3--anwendung-holen) - [Schritt 4 — Schlüssel erzeugen](#schritt-4--schlüssel-erzeugen) - [Schritt 5 — Konfiguration ausfüllen](#schritt-5--konfiguration-ausfüllen) - [Schritt 6 — Starten](#schritt-6--starten) - [Schritt 7 — Datenbank vorbereiten](#schritt-7--datenbank-vorbereiten) - [Schritt 8 — Dateiablage einrichten](#schritt-8--dateiablage-einrichten) - [Schritt 9 — Reverse Proxy und TLS](#schritt-9--reverse-proxy-und-tls) - [Schritt 10 — Ersteinrichtung im Browser](#schritt-10--ersteinrichtung-im-browser) - [Schritt 11 — Sicherung prüfen](#schritt-11--sicherung-prüfen) - [Schritt 12 — Wartungslauf einrichten](#schritt-12--wartungslauf-einrichten) - [Schritt 13 — Schlüssel in Sicherheit bringen](#schritt-13--schlüssel-in-sicherheit-bringen) - [Aktualisieren](#aktualisieren) - [Wenn etwas nicht klappt](#wenn-etwas-nicht-klappt) - [Abnahmeliste](#abnahmeliste) --- ## Schritt 0 — Was du vorher brauchst **Ein Server in Deutschland oder mindestens in der EU.** Für eine Betreuung reicht die kleinste Größe bequem: | | Minimum | Bequem | |---|---|---| | Arbeitsspeicher | 2 GB | 4 GB | | Speicherplatz | 20 GB | 40 GB | | Kerne | 2 | 2 | | Betriebssystem | Debian 12 oder Ubuntu 24.04 | dasselbe | Anbieter mit Rechenzentren in Deutschland: Hetzner (Falkenstein, Nürnberg), netcup (Nürnberg), IONOS (Karlsruhe). Ein Server für rund fünf Euro im Monat genügt. > **Bevor du bestellst:** Kläre den **Auftragsverarbeitungsvertrag nach > Art. 28 DSGVO**. Bei Hetzner findest du ihn im Kundenkonto unter > *Verträge → AV-Vertrag*, bei anderen ähnlich. Ohne ihn ist der Betrieb > formal rechtswidrig — und es dauert fünf Minuten. **Eine Domain oder Subdomain**, zum Beispiel `cockpit.deine-domain.de`. Setz einen A-Record auf die IP-Adresse des Servers, bevor du anfängst; DNS-Änderungen brauchen manchmal eine Stunde. **Etwas zum Aufschreiben.** Du erzeugst gleich Schlüssel, die du nicht verlieren darfst. Ein Passwortmanager ist das Richtige. ### Kontrolle ```bash # Von deinem Rechner aus - zeigt die IP deines Servers? dig +short cockpit.deine-domain.de # Kommst du auf den Server? ssh root@cockpit.deine-domain.de ``` --- ## Schritt 1 — Server vorbereiten Alles Folgende läuft **auf dem Server**. ```bash # System aktuell bringen apt update && apt upgrade -y # Werkzeuge, die wir brauchen apt install -y git curl ca-certificates ufw # Zeitzone setzen - sonst rechnet der Server in UTC und # verschiebt Fristen um einen Tag timedatectl set-timezone Europe/Berlin ``` **Einen eigenen Benutzer anlegen.** Als `root` zu arbeiten ist unnötig riskant: ```bash adduser --gecos "" cockpit usermod -aG sudo cockpit # SSH-Schlüssel übernehmen, damit du dich weiter anmelden kannst rsync --archive --chown=cockpit:cockpit ~/.ssh /home/cockpit/ ``` **Die Firewall einschalten.** Nur SSH, HTTP und HTTPS von außen: ```bash ufw allow OpenSSH ufw allow 80/tcp ufw allow 443/tcp ufw --force enable ``` **SSH absichern** — Anmeldung nur mit Schlüssel, nicht mit Passwort. Nur machen, wenn du dich sicher mit Schlüssel anmelden kannst: ```bash sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config sed -i 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config systemctl restart ssh ``` ### Kontrolle ```bash timedatectl | grep "Time zone" # Europe/Berlin ufw status # active, drei Regeln ``` Melde dich jetzt neu an, als `cockpit`: ```bash ssh cockpit@cockpit.deine-domain.de ``` --- ## Schritt 2 — Docker installieren ```bash # Offizielles Paketverzeichnis von Docker eintragen sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin # Ohne sudo arbeiten dürfen sudo usermod -aG docker $USER ``` > Bei **Ubuntu** ersetzt du in den beiden URLs `debian` durch `ubuntu`. **Melde dich ab und wieder an**, damit die Gruppenzugehörigkeit greift. ### Kontrolle ```bash docker --version # Docker version 27.x oder neuer docker compose version # Docker Compose version v2.x docker run --rm hello-world # "Hello from Docker!" ``` Kommt bei `docker run` ein *permission denied*, hast du das Ab- und Anmelden vergessen. --- ## Schritt 3 — 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.0.0 ``` ### Kontrolle ```bash ls docker-compose.yml Dockerfile .env.example # alle drei da git describe --tags # v1.0.0 ``` --- ## Schritt 4 — Schlüssel erzeugen Die Anwendung braucht vier 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'); console.log('AUTH_SECRET=' + b(32)); console.log('ENC_KEY_V1=' + b(32)); console.log('INDEX_KEY=' + b(32)); console.log('POSTGRES_PASSWORD=' + b(24).replace(/[+/=]/g,'x')); console.log('SICHERUNG_PASSWORT=' + b(24).replace(/[+/=]/g,'x')); console.log('S3_ACCESS_KEY_ID=' + b(12).replace(/[+/=]/g,'x')); console.log('S3_SECRET_ACCESS_KEY=' + b(24).replace(/[+/=]/g,'x')); " ``` Kopier die Ausgabe in einen Texteditor. Du brauchst sie gleich. > **`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 13 sagt dir, wohin damit. Verlier ihn bis dahin nicht. --- ## Schritt 5 — Konfiguration ausfüllen ```bash cp .env.example .env nano .env ``` Diese Werte **musst** du setzen: ```bash # --- Datenbank --- POSTGRES_PASSWORD= # --- Anmeldung --- AUTH_SECRET= AUTH_URL=https://cockpit.deine-domain.de # exakt deine Adresse, mit https AUTH_TRUST_HOST=true # --- Verschlüsselung --- ENC_KEY_V1= ENC_KEY_AKTIV=1 INDEX_KEY= # --- Objektspeicher --- S3_ACCESS_KEY_ID= S3_SECRET_ACCESS_KEY= # --- Sicherung --- SICHERUNG_PASSWORT= ``` > **`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. 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= SMTP_FROM="Betreuungs-Cockpit " ``` Speichern mit `Strg+O`, `Enter`, `Strg+X`. **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 6 — 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** — Tesseract und die deutschen Sprachdaten wollen heruntergeladen werden. Danach geht es schnell. ### Kontrolle ```bash docker compose ps ``` Vier Dienste, alle `running`, die Datenbank `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? curl -s http://127.0.0.1:3000/api/gesundheit # {"status":"bereit","version":"1.0.0","commit":"..."} ``` Kommt hier nichts, sieh in die Logs: ```bash docker compose logs app --tail 50 ``` --- ## Schritt 7 — 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. --- ## Schritt 8 — Dateiablage einrichten Der Objektspeicher braucht einen Ablagebereich für die Dokumente: ```bash source .env 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/ ``` > Wenn du statt MinIO einen externen Objektspeicher in der EU benutzt > (z. B. Hetzner Object Storage), lass den `minio`-Dienst in der > `docker-compose.yml` weg und zeig mit `S3_ENDPOINT` dorthin. **Auch > dafür brauchst du einen AV-Vertrag.** --- ## Schritt 9 — Reverse Proxy und TLS Die Anwendung horcht bewusst nur auf `127.0.0.1:3000` und bringt **keinen eigenen Proxy** mit. Von außen erreichbar wird sie über den Proxy auf dem Server, der auch das Zertifikat besorgt. ### Wenn noch kein Proxy da ist: Caddy Caddy holt Let's-Encrypt-Zertifikate von selbst und erneuert sie: ```bash sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ | sudo tee /etc/apt/sources.list.d/caddy-stable.list sudo apt update && sudo apt install -y caddy ``` Dann `/etc/caddy/Caddyfile` ersetzen: ```bash sudo tee /etc/caddy/Caddyfile > /dev/null <<'ENDE' cockpit.deine-domain.de { encode zstd gzip reverse_proxy 127.0.0.1:3000 # Fotos von Behördenschreiben können groß sein request_body { max_size 30MB } } ENDE sudo systemctl reload caddy ``` ### Wenn schon nginx läuft ```nginx server { listen 443 ssl http2; 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; client_max_body_size 30M; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; 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; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } ``` Das Zertifikat holst du mit `sudo certbot --nginx -d cockpit.deine-domain.de`. > Der Proxy **muss** `X-Forwarded-For` weiterreichen. Die Anwendung > braucht die echte Adresse für die Login-Sperre und für das Audit-Log > (dort nur als Hash). Ohne den Kopf zählt sie alle Zugriffe als denselben > Anschluss. ### Kontrolle Von deinem eigenen Rechner aus: ```bash curl -sI https://cockpit.deine-domain.de/anmelden | head -5 ``` Erwartet: `HTTP/2 200`, dazu die Kopfzeilen `content-security-policy`, `x-frame-options: DENY` und `strict-transport-security`. --- ## Schritt 10 — 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 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.0.0 · `. --- ## Schritt 11 — 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 docker compose exec -e PGDATABASE=probe -e SICHERUNG_OHNE_RUECKFRAGE=ja \ sicherung /bin/sh /wiederherstellung.sh /backups/cockpit_.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 12 — Wartungslauf einrichten Einmal täglich: Fristerinnerungen verschicken, Papierkorb leeren, alte Anmeldeversuche entfernen. ```bash # Schlüssel für den Endpunkt ausrechnen source .env 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: " https://cockpit.deine-domain.de/api/wartung > /dev/null ``` ### Kontrolle ```bash curl -s -H "X-Wartung-Schluessel: " \ 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 13 — 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 # 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 7 erneut aus** — sie sind so gebaut, dass mehrfaches Ausführen nichts kaputtmacht. ### 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. Sieh in `docker compose logs app`. --- ## 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 5. ### 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 ``` ### 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 Datenbank ist voll ```bash df -h docker system prune -a --volumes # ACHTUNG: entfernt ungenutzte Volumes ``` Vorher prüfen, was da ist. `docker system prune` ohne `--volumes` ist der sichere Anfang. ### 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 - [ ] Zeitzone auf `Europe/Berlin` - [ ] Firewall aktiv, nur 22, 80 und 443 offen - [ ] SSH-Anmeldung nur mit Schlüssel - [ ] `https://` funktioniert, Zertifikat gültig - [ ] `.env` hat Rechte `600` - [ ] Alle vier Dienste laufen, Datenbank `healthy` - [ ] `/api/gesundheit` antwortet mit der erwarteten Version - [ ] **Audit-Schutz greift** — `UPDATE audit_log` endet mit Fehler - [ ] Bucket im Objektspeicher angelegt - [ ] 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.