Anleitung: wo die Daten liegen, und drei Fallen aus dem Betrieb

Aus einer Fehlersuche am 31.08.2026 sind vier Luecken in der Anleitung
uebriggeblieben. Keine davon war falsch formuliert - alle vier waren
schlicht nicht durchdacht.

1. Es stand nirgends, wo die Daten liegen. Der neue Abschnitt nennt die
   drei Orte, zeigt den Pfad auf der Platte, sagt ausdruecklich, dass dort
   keine anklickbaren PDFs liegen, und beschreibt den Abgleich zwischen
   Akte und Speicher: Zu jedem Eintrag muss eine Datei gehoeren, sonst
   belegt eine Pruefsumme ein Dokument, das es nicht mehr gibt.

2. Der mc-Alias wurde einmalig gesetzt. Er liegt in /root/.mc IM
   Behaelter und ist nach jedem "docker compose up -d minio" weg -
   "mc ls lokal" liefert dann kommentarlos nichts, und man haelt seine
   Dokumente fuer verschwunden. Genau das ist passiert. Jeder Aufruf setzt
   den Alias jetzt selbst, aus den Variablen des Behaelters.

3. Der Bucket-Name stand fest im Befehl, die Anwendung liest ihn aus
   S3_BUCKET. Weichen sie ab, zeigen beide aneinander vorbei.

4. "set -a && source .env" wurde empfohlen, ohne zu sagen, dass die
   exportierten Werte danach jedes "docker compose up" ueberstimmen. Wer
   spaeter die .env aendert und neu startet, sieht keine Wirkung und sucht
   den Fehler ueberall, nur nicht in der eigenen Shell. Der Kniff ist
   nicht mehr noetig - der neue Befehl kommt ohne ihn aus - und die Falle
   steht als Erklaerung daneben.

Dazu docker-compose.override.yml in der .gitignore und in der Anleitung
als vorgesehener Ort fuer Anpassungen, die ein Update ueberleben sollen:
update.sh prueft nur versionierte Dateien und laesst sie in Ruhe.

Die Aussage zur Sicherung ist mitgezogen - sie stimmt seit #41 nicht mehr,
die Dokumente sind jetzt enthalten.

Neun Pruefungen in tests/anleitung.test.ts, gegen vier Mutationen
geprueft: Name fest verdrahtet, Alias nicht gesetzt, source .env zurueck,
Abschnitt entfernt - jede wird rot.

Refs #46
This commit is contained in:
Florian Wilde
2026-09-01 16:36:48 +02:00
parent 53dde70f26
commit 042e63812b
3 changed files with 245 additions and 12 deletions
+8
View File
@@ -11,6 +11,14 @@ dist/
next-env.d.ts next-env.d.ts
*.tsbuildinfo *.tsbuildinfo
# Eigene Anpassungen der Installation.
#
# Docker Compose liest diese Datei automatisch zusaetzlich zur
# docker-compose.yml. Sie ist der vorgesehene Ort fuer Aenderungen, die ein
# Update ueberleben sollen: update.sh prueft nur versionierte Dateien
# (git status --untracked-files=no) und laesst sie deshalb in Ruhe.
docker-compose.override.yml
# Umgebung - enthaelt Schluessel, gehoert NIE ins Repo # Umgebung - enthaelt Schluessel, gehoert NIE ins Repo
.env .env
.env.local .env.local
+127 -12
View File
@@ -34,6 +34,7 @@ durchgeht — dann weißt du im Fehlerfall genau, wo es hakt.
- [Schritt 9 — Sicherung prüfen](#schritt-9--sicherung-prüfen) - [Schritt 9 — Sicherung prüfen](#schritt-9--sicherung-prüfen)
- [Schritt 10 — Wartungslauf einrichten](#schritt-10--wartungslauf-einrichten) - [Schritt 10 — Wartungslauf einrichten](#schritt-10--wartungslauf-einrichten)
- [Schritt 11 — Schlüssel in Sicherheit bringen](#schritt-11--schlüssel-in-sicherheit-bringen) - [Schritt 11 — Schlüssel in Sicherheit bringen](#schritt-11--schlüssel-in-sicherheit-bringen)
- [Wo deine Daten liegen](#wo-deine-daten-liegen)
- [Aktualisieren](#aktualisieren) - [Aktualisieren](#aktualisieren)
- [Wenn etwas nicht klappt](#wenn-etwas-nicht-klappt) - [Wenn etwas nicht klappt](#wenn-etwas-nicht-klappt)
- [Abnahmeliste](#abnahmeliste) - [Abnahmeliste](#abnahmeliste)
@@ -375,22 +376,37 @@ nichts kaputtmacht.
Der Objektspeicher braucht einen Ablagebereich für die Dokumente: Der Objektspeicher braucht einen Ablagebereich für die Dokumente:
```bash ```bash
set -a && source .env && set +a docker compose exec minio sh -c '
mc alias set lokal http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null &&
docker compose exec minio mc alias set lokal \ mc mb "lokal/$S3_BUCKET" 2>/dev/null || true
http://localhost:9000 "$S3_ACCESS_KEY_ID" "$S3_SECRET_ACCESS_KEY" mc ls lokal
'
docker compose exec minio mc mb lokal/betreuungs-cockpit
``` ```
### Kontrolle Es muss eine Zeile mit dem Namen deines Ablagebereichs erscheinen —
standardmäßig `betreuungs-cockpit/`.
```bash > **Warum der Befehl so umständlich aussieht.** Er holt die Zugangsdaten
docker compose exec minio mc ls lokal > aus den Variablen, die im MinIO-Behälter ohnehin stehen. Das hat drei
# [...] betreuungs-cockpit/ > Gründe, und jeder davon hat schon einmal Zeit gekostet:
``` >
> 1. **Kein Passwort in der Befehlszeile** — es stünde sonst in der
> Verlaufsdatei deiner Shell.
> 2. **Kein `source .env`** — exportierte Werte haben Vorrang vor der
> `.env`-Datei und überstimmen danach *jedes* `docker compose up`. Wer
> später die `.env` ändert und neu startet, sieht keine Wirkung und
> sucht den Fehler überall, nur nicht in der eigenen Shell.
> 3. **Der Alias überlebt kein Neuerstellen.** `mc alias set` schreibt
> nach `/root/.mc` **im Behälter**. Nach jedem `docker compose up -d
> minio` ist er weg, und `mc ls lokal` liefert dann kommentarlos nichts
> — man hält seine Dokumente für verschwunden. Deshalb setzt jeder
> Aufruf den Alias selbst.
>
> Der Bucket-Name kommt aus `$S3_BUCKET` und ist nicht fest verdrahtet:
> Sonst zeigen Anwendung und Ablagebereich aneinander vorbei, sobald
> jemand den Namen ändert.
> **Serverseitige Verschluesselung?** Brauchst du nicht. Die Anwendung > **Serverseitige Verschlüsselung?** Brauchst du nicht. Die Anwendung
> verschlüsselt jede Datei selbst, bevor sie den Server verlässt — mit > verschlüsselt jede Datei selbst, bevor sie den Server verlässt — mit
> `ENC_KEY_V1`, der den Anbieter nie erreicht. Das mitgelieferte MinIO > `ENC_KEY_V1`, der den Anbieter nie erreicht. Das mitgelieferte MinIO
> läuft ohne KMS und würde jede Ablage ablehnen, sobald `S3_SSE` in der > läuft ohne KMS und würde jede Ablage ablehnen, sobald `S3_SSE` in der
@@ -913,6 +929,105 @@ Denk an die letzten Pflichten:
--- ---
## Wo deine Daten liegen
Drei Orte, und keiner davon ist ein Ordner, den du einfach öffnest.
| Was | Wo | In der täglichen Sicherung? |
|---|---|---|
| **Dokumente** (Bescheide, Schreiben, Belege) | Docker-Volume `minio-daten` | ja |
| **Datenbank** (Akte, Fristen, Buchungen) | Docker-Volume `postgres-daten` | ja |
| **Sicherungen** | `./backups` neben der `docker-compose.yml` | — |
### Den Pfad auf der Platte finden
```bash
docker volume inspect betreuungs-cockpit_minio-daten --format '{{.Mountpoint}}'
# /var/lib/docker/volumes/betreuungs-cockpit_minio-daten/_data
```
Heißt dein Projektverzeichnis anders, heißen auch die Volumes anders —
Docker stellt den Verzeichnisnamen voran:
```bash
docker volume ls | grep -E "minio-daten|postgres-daten"
```
> **Dort hineinzusehen bringt nichts.** MinIO legt jedes Dokument als
> eigenes Verzeichnis mit Metadatei und Datenteilen ab, und der Inhalt ist
> verschlüsselt. Du fändest dort keine anklickbaren PDFs, sondern
> Fragmente. Wer die Dokumente sehen will, nimmt die Anwendung.
### Nachsehen, was abgelegt ist
```bash
docker compose exec minio sh -c '
mc alias set lokal http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null &&
mc ls --recursive "lokal/$S3_BUCKET"
'
```
Je Dokument eine Zeile unter `betreute/<id>/dokumente/<uuid>.pdf`.
### Prüfen, ob Akte und Speicher übereinstimmen
Die wichtigste Kontrolle: Zu **jedem** Eintrag in der Akte muss eine Datei
gehören. Fehlt sie, belegt eine Prüfsumme ein Dokument, das es nicht mehr
gibt — gegenüber dem Betreuungsgericht der schlechteste Zustand.
```bash
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At -c "SELECT \"objektSchluessel\" FROM dokument WHERE \"geloeschtAm\" IS NULL ORDER BY 1;"' | sort > /tmp/erwartet.txt
docker compose exec minio sh -c '
mc alias set lokal http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null &&
mc ls --recursive "lokal/$S3_BUCKET"
' | awk '{print $NF}' | sort > /tmp/vorhanden.txt
echo "In der Akte: $(wc -l < /tmp/erwartet.txt) Im Speicher: $(wc -l < /tmp/vorhanden.txt)"
echo "--- Fehlt im Speicher (Handlungsbedarf): ---"
comm -23 /tmp/erwartet.txt /tmp/vorhanden.txt
echo "--- Verwaist im Speicher (harmlos): ---"
comm -13 /tmp/erwartet.txt /tmp/vorhanden.txt
```
Beide Zahlen gleich und beide Listen leer — dann passt es.
Ein Eintrag unter *„Fehlt im Speicher"* ist ein Akteneintrag ohne Datei.
Er entsteht, wenn eine Ablage scheitert. Solche Einträge löschen und das
Dokument neu anlegen.
### Ohne welche Schlüssel nichts davon lesbar ist
| Schlüssel | Ohne ihn |
|---|---|
| `ENC_KEY_V1` | Diagnosen, IBANs, Notizen und alle Dokumente bleiben unlesbar — auch aus jeder Sicherung |
| `SICHERUNG_PASSWORT` | keine Sicherung lässt sich öffnen |
| `MINIO_KMS_SECRET_KEY` | nur falls gesetzt: MinIO gibt die Objekte nicht mehr heraus |
Alle gehören an die zwei Orte aus Schritt 11, und keiner davon ist dieser
Server.
### Eigene Anpassungen, die ein Update überleben
Änderungen an der `docker-compose.yml` legt `scripts/update.sh` bei jedem
Update per `git stash` beiseite — sie sind versioniert. Wer etwas dauerhaft
ändern will, legt stattdessen eine **`docker-compose.override.yml`**
daneben:
```yaml
services:
app:
environment:
EIGENER_WERT: ${EIGENER_WERT:-}
```
Docker Compose liest sie automatisch mit. `update.sh` prüft nur
versionierte Dateien (`git status --untracked-files=no`) und lässt sie
deshalb unangetastet.
---
## Aktualisieren ## Aktualisieren
```bash ```bash
+110
View File
@@ -0,0 +1,110 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
/**
* Die Installationsanleitung muss ausführbar sein, nicht nur lesbar.
*
* Anlass ist eine Fehlersuche vom 31.08.2026. Drei Stellen der Anleitung
* haben dabei Zeit gekostet — keine davon war falsch formuliert, alle drei
* waren schlicht nicht durchdacht:
*
* 1. `mc alias set` wurde einmalig gesetzt. Der Alias liegt in `/root/.mc`
* **im Behälter** und ist nach jedem `docker compose up -d minio` weg.
* `mc ls lokal` liefert dann kommentarlos nichts — man hält seine
* Dokumente für verschwunden.
* 2. Der Bucket-Name stand fest im Befehl, die Anwendung liest ihn aus
* `S3_BUCKET`. Weichen sie ab, zeigen beide aneinander vorbei.
* 3. `set -a && source .env` exportiert alle Werte in die Shell — und die
* haben danach Vorrang vor der `.env`-Datei. Wer später etwas ändert
* und neu startet, sieht keine Wirkung.
*
* Diese Prüfungen lesen den Text. Sie können die Anleitung nicht
* ausführen, aber sie schlagen an, sobald jemand in eine der Fallen
* zurückfällt.
*/
const anleitung = readFileSync('docs/INSTALLATION.md', 'utf8')
/** Alle Zeilen mit einem mc-Aufruf, ohne Fließtext und Zitate. */
function mcZeilen(): string[] {
return anleitung
.split('\n')
.filter((zeile) => /\bmc (ls|mb|mirror|cp|rm|stat)\b/.test(zeile))
.filter((zeile) => !zeile.trim().startsWith('>'))
}
describe('Installationsanleitung', () => {
it('setzt bei jedem mc-Aufruf den Alias selbst', () => {
// Sonst funktioniert der Befehl nur, solange der Behaelter nicht neu
// erstellt wurde - und das sagt einem niemand.
const abschnitte = anleitung.split('```')
const ohneAlias = abschnitte
.filter((block) => /\bmc (ls|mb|mirror|cp|rm|stat)\b/.test(block))
.filter((block) => !block.includes('mc alias set'))
expect(
ohneAlias.length,
'Ein mc-Befehl ohne "mc alias set" im selben Block läuft nur, solange ' +
'der MinIO-Behälter nicht neu erstellt wurde.',
).toBe(0)
})
it('verdrahtet den Namen des Ablagebereichs nicht fest', () => {
const festVerdrahtet = mcZeilen().filter((zeile) => /lokal\/betreuungs-cockpit/.test(zeile))
expect(
festVerdrahtet,
'Der Name gehört aus $S3_BUCKET - sonst zeigen Anwendung und ' +
'Ablagebereich aneinander vorbei, sobald jemand ihn ändert.',
).toEqual([])
})
it('empfiehlt kein "source .env" mehr', () => {
// Exportierte Werte ueberstimmen danach jedes "docker compose up".
const zeilen = anleitung
.split('\n')
.filter((zeile) => !zeile.trim().startsWith('>'))
.filter((zeile) => !zeile.trim().startsWith('#'))
.filter((zeile) => /source \.env/.test(zeile))
expect(zeilen).toEqual([])
})
it('erklärt trotzdem, warum es nicht empfohlen wird', () => {
// Weglassen allein hilft nicht: Wer den Kniff von woanders kennt,
// benutzt ihn weiter.
expect(anleitung).toMatch(/Vorrang vor der/)
})
it('sagt, wo die Daten liegen', () => {
expect(anleitung).toMatch(/## Wo deine Daten liegen/)
expect(anleitung).toMatch(/docker volume inspect/)
expect(anleitung).toMatch(/minio-daten/)
expect(anleitung).toMatch(/postgres-daten/)
})
it('zeigt, wie man Akte und Speicher abgleicht', () => {
// Die wichtigste Kontrolle: Zu jedem Akteneintrag muss eine Datei
// gehoeren, sonst belegt eine Pruefsumme ein Dokument, das es nicht
// mehr gibt.
expect(anleitung).toMatch(/objektSchluessel/)
expect(anleitung).toMatch(/comm -23/)
})
it('nennt die Schlüssel, ohne die nichts mehr lesbar ist', () => {
const abschnitt = anleitung.slice(anleitung.indexOf('## Wo deine Daten liegen'))
for (const schluessel of ['ENC_KEY_V1', 'SICHERUNG_PASSWORT', 'MINIO_KMS_SECRET_KEY']) {
expect(abschnitt, `${schluessel} fehlt in der Aufzählung`).toContain(schluessel)
}
})
it('weist docker-compose.override.yml als Ort für eigene Anpassungen aus', () => {
expect(anleitung).toMatch(/docker-compose\.override\.yml/)
expect(readFileSync('.gitignore', 'utf8')).toMatch(/docker-compose\.override\.yml/)
})
it('behauptet nicht mehr, die Dokumente seien ungesichert', () => {
// Stand bis v1.11.1 so drin und stimmt seit der Sicherung nicht mehr.
expect(anleitung).not.toMatch(/Was das Backup nicht enthält:.*Dateien im Objektspeicher/s)
})
})