Fix #46: Wo die Daten liegen, und drei Fallen aus dem Betrieb #53
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user