[[Uebersicht|← Zurück zur Homepage]]
# PaperSafe – Umsetzungsprotokoll
> **Diese Notiz wird von Claude Code während der Umsetzung geführt.** Sie ist das chronologische Ist-Protokoll. Die Soll-Vorgabe steht in [[05-Vorgabe-ClaudeCode]] und wird **nicht** verändert.
## Regeln für die Protokollführung
1. **Chronologisch anhängen, nichts überschreiben.** Ein späterer Eintrag korrigiert einen früheren, er löscht ihn nicht
2. **Jeder Eintrag mit Datum und Uhrzeit**
3. **Fehler und Sackgassen gehören hier hinein.** Ein Protokoll, in dem alles glattging, ist beim nächsten Problem wertlos. Was nicht funktioniert hat, ist oft die nützlichste Information
4. **Abweichungen von [[05-Vorgabe-ClaudeCode]] immer mit Begründung** – und als Abweichung gekennzeichnet, nicht stillschweigend
5. **Keine Secrets.** Keine Passwörter, keine Keys, keine Tokens. Stattdessen: „generiert, separat übermittelt"
6. **Befehle im Wortlaut**, wenn sie für eine Wiederholung oder Fehlersuche relevant sind
7. Dauerhafte Ergebnisse (finale Werte, Konfiguration) zusätzlich in [[02-Installation]], [[03-Konfiguration]], [[04-Betrieb-Backup]] eintragen – dieses Protokoll ist die Chronik, jene Notizen sind der Stand
## Statusübersicht
| Phase | Inhalt | Status | Datum |
|---|---|---|---|
| 1 | Bestandsaufnahme | abgeschlossen | 18.07.2026 |
| 2 | Container anlegen | abgeschlossen | 18.07.2026 |
| 3 | Paperless-ngx installieren | abgeschlossen | 18.07.2026 |
| 4 | Funktionsprüfung | abgeschlossen | 18.07.2026 |
| 5 | Backup einrichten | abgeschlossen | 18.07.2026 |
| 6 | Restore-Test | **abgeschlossen, erfolgreich** | 18.07.2026 |
| 7 | Dokumentation zurückschreiben | abgeschlossen | 18.07.2026 |
## Ermittelte Eckdaten
Wird in Phase 1 gefüllt und danach als Referenz verwendet:
| Wert | Ergebnis |
|---|---|
| CTID | **110** (bestätigt, angelegt) |
| MAC-Adresse net0 | **BC:24:11:E0:81:A8** (unverändert, Router-Reservierung passt) |
| IP-Adresse | **192.168.2.35** (per Router-Reservierung, verifiziert im Container) |
| IPv6 | deaktiviert – `ip6=manual` in Proxmox + `net.ipv6.conf.all/default/lo.disable_ipv6=1` im Gast (siehe Phase 2b, Abweichung von „ip6=none") |
| Gateway / Bridge | 192.168.2.1 / vmbr0 (bestätigt, bestehende Bridge) |
| Storage Container-Disk | **local-lvm** (bestätigt, Abweichung von Phase-1-Vorschlag `nvme-thin`, siehe unten) |
| Storage NAS (Name/Typ) | DSOBE203_V2, Typ CIFS/SMB, Server `dsobe2023`, Pfad `/mnt/pve/DSOBE203_V2` (bestätigt, bestehend) |
| NAS-Freigabe Export | `//dsobe2023/papersafeexport`, gemountet auf `/mnt/papersafe-export` (fstab, `_netdev,nofail`), Zugangsdaten in `/root/.smbcred-papersafeexport` (`600`) |
| Docker-Version | 29.6.2 (Engine, CLI, containerd.io), Compose-Plugin v5.3.1, Buildx-Plugin 0.35.0 |
| Mount Point mp0 | `local-lvm:vm-110-disk-1`, 200 GB, `/opt/paperless/data`, `backup=1` |
| Paperless-Image-Version | `ghcr.io/paperless-ngx/paperless-ngx:2.20.15` |
| Weitere Image-Versionen | postgres:18.4-bookworm, redis:8.8.0, gotenberg/gotenberg:8.25, apache/tika:3.3.1.0 |
| Installationspfad Stack | `/opt/paperless/` (Compose-Dateien), Daten unter `/opt/paperless/data/{data,media,consume,export,pgdata,redisdata}` (Bind-Mounts) |
| PAPERLESS_URL | `http://192.168.2.35:8000` |
| Secrets | `PAPERLESS_SECRET_KEY`, `POSTGRES_PASSWORD`, `PAPERLESS_ADMIN_PASSWORD` – generiert, separat übermittelt (Chat) |
---
## Protokolleinträge
### Phase 1 – Bestandsaufnahme
**18.07.2026, 15:15 Uhr.** Rein lesende Bestandsaufnahme per SSH (Alias `proxmox`, Key `~/.ssh/id_ed25519_proxmox`). Keine Änderungen vorgenommen.
Ausgeführte Befehle: `pveversion -v`, `pct list`, `qm list`, `pvesm status`, `cat /etc/pve/storage.cfg`, `nproc`, `free -h`, `cat /etc/pve/jobs.cfg`, `cat /etc/vzdump.conf`, `ip a`, `cat /etc/network/interfaces`, `pct config 104/105/108`, `qm config 103/109`, `ip neighbor show`, Ping-Test auf 192.168.2.200–205, `pvesh get /cluster/nextid`.
**Proxmox-Version:** proxmox-ve 9.2.0, pve-manager 9.2.4, Kernel 7.0.14-4-pve.
**Belegte IDs:**
| ID | Name | Typ | Status | Kerne | RAM | Disk |
|---|---|---|---|---|---|---|
| 103 | w11 | VM | gestoppt | 4 | 4096 MB | 60 GB (local-lvm) |
| 104 | tor-snowflake | LXC | läuft | 2 | 1024 MB | 8 GB (local-lvm) |
| 105 | zigbee2mqtt | LXC | läuft | 2 | 2048 MB | 5 GB (local-lvm) – **tabu** |
| 108 | ghost-blog | LXC | gestoppt (onboot 0) | 1 | 512 MB | 10 GB (local-lvm) |
| 109 | haproxmox | VM | läuft | 4 | 4096 MB (balloon 2048) | 150 GB (nvme-thin) |
**Wichtiger Befund, kein VM 100 vorhanden:** Laut `05-Vorgabe-ClaudeCode.md` Abschnitt 0/1/Anhang A ist „Home Assistant (VM 100)" tabu. Auf dem Host existiert keine ID 100. Home Assistant läuft tatsächlich als **VM 109 „haproxmox"** – die VM-Beschreibung lautet wörtlich „Homeassistant OS VM". `pvesh get /cluster/nextid` liefert dementsprechend `100` als nächste freie ID. Diese Abweichung wurde noch während der Bestandsaufnahme in `CLAUDE.md` durch den Nutzer korrigiert (VM 109 statt VM 100). Die Vorgabe 05 selbst bleibt unverändert (schreibgeschützt) und nennt weiterhin „VM 100" – das ist gemäß Grundregel 10 dort zu korrigieren, nicht von mir.
**Storages (`pvesm status` + `/etc/pve/storage.cfg`):**
| Name | Typ | Größe gesamt | Belegt | Frei | Pfad/Details |
|---|---|---|---|---|---|
| local | dir | 98,5 GB | 21,1 GB (21,45 %) | 72,3 GB | `/var/lib/vz`, Content iso/backup/vztmpl |
| local-lvm | lvmthin | 832,9 GB | 32,7 GB (3,93 %) | 800,2 GB | VG `pve`, Content images/rootdir |
| nvme-thin | lvmthin | 488,1 GB | 60,8 GB (12,45 %) | 427,4 GB | VG `vg-nvme`, Content images/rootdir |
| DSOBE203_V2 | cifs | 7,48 TB | 4,38 TB (58,50 %) | 3,10 TB | Server `dsobe2023`, Share `public`, Pfad `/mnt/pve/DSOBE203_V2`, Subdir `/backup/proxmox`, Content backup+images, `prune-backups keep-all=1` |
Der Storage-Name im System lautet `DSOBE203_V2` (Server dahinter heißt `dsobe2023`) – keine Abweichung, nur Klarstellung. Einbindung ist bestehend, wurde nicht verändert.
**CPU/RAM (`nproc`, `free -h`):** 8 physische Kerne. Laufende Gäste beanspruchen zusammen bereits 8 vCPU (CT104: 2 + CT105: 2 + VM109: 4) – das entspricht 1:1-Auslastung der physischen Kerne, Proxmox erlaubt CPU-Overcommit, unkritisch bei den bisherigen Lasten. Ein PaperSafe-Container mit 2 Kernen würde auf 10 vCPU/8 pCPU führen. RAM: 15 GiB gesamt, 6,0 GiB belegt, 9,1 GiB frei, 9,4 GiB verfügbar – 4096 MB für PaperSafe passen komfortabel.
**Backup-Jobs (`/etc/pve/jobs.cfg`):** Ein Job `backup-de908e8d-9301`, Zeitplan `06,12,18:00`, **`all 1`** (erfasst automatisch alle Gäste, keine explizite ID-Liste), compress zstd, `prune-backups keep-daily=5`, Storage `DSOBE203_V2`, Modus snapshot. Relevanz für Phase 5: Ein neuer Container würde automatisch erfasst, keine Ergänzung einer ID-Liste nötig.
**Netzwerk (`ip a`, `/etc/network/interfaces`, `ip neighbor show`):** Eine Bridge `vmbr0`, statisch `192.168.2.146/24`, Gateway `192.168.2.1`, gebunden an physisches NIC `enxc8a362f22277`. **Auffällig:** Alle bestehenden Container/VMs nutzen `ip=dhcp` – keiner hat eine im LXC-Config fest eingetragene statische IP. Die Vorgabe „statische IP, Bridge wie bestehende Container" (Phase 2) passt bezüglich Bridge, aber bezüglich „statisch" weicht das Muster der bestehenden Container ab – das ist ein Klärungspunkt für Phase 2, keine Änderung wurde vorgenommen.
IP-Freiprüfung: ARP-Cache (`ip neighbor show`) zeigt nur wenige aktive IPv4-Hosts (.1 Gateway, .87, .97, .154, .191) – das Cache ist unvollständig, da nur kürzlich aktive Geräte erfasst sind. Zusätzlicher Ping-Test auf 192.168.2.200–205: keine Antwort auf allen sechs Adressen. Kein Zugriff auf den DHCP-Server/Router möglich, daher keine 100%ige Garantie – Empfehlung ist ein Vorschlag, keine Bestätigung.
**Empfehlung:**
- **CTID:** 110 (nach höchster aktuell belegter ID 109 – vermeidet auch jede Verwechslung mit der oben beschriebenen ID-100-Verwirrung). Alternativen: 106, 111.
- **Storage Container-Disk:** `nvme-thin` (NVMe-basiert, 427 GB frei) – schneller als `local-lvm`, welches ebenfalls ausreichend Platz hätte (800 GB frei) und als Alternative in Frage kommt.
- **IP:** 192.168.2.200 (per Ping-Test unbeantwortet). Alternativen: .201, .210.
- **Ressourcen:** 2 Kerne / 4 GB RAM / 20 GB Disk passen nach aktuellem Stand (RAM unkritisch, CPU mit leichtem Overcommit, Storage unkritisch).
**Was dagegen spricht / unklar ist:**
- CTID 100 wäre laut Proxmox-API technisch frei, wird aber wegen der Home-Assistant-Verwechslung in der Vorgabe **nicht** empfohlen.
- Statische IP vs. DHCP-Muster der bestehenden Container – bitte bestätigen, welches Verfahren für PaperSafe gewünscht ist.
- Freie IP ist nur per ARP-Cache + Ping-Sweep geprüft, nicht per DHCP-Server-Abfrage – Router/DHCP-Konfiguration war nicht zugänglich.
- Ob `nvme-thin` gegenüber `local-lvm` tatsächlich schneller ist (physisches Medium dahinter), wurde nicht am Host verifiziert – nur aus dem Namen abgeleitet.
---
### Phase 2 – Container anlegen (erster Teil)
**18.07.2026, 16:00 Uhr.** Freigegebene Werte aus `05-Vorgabe-ClaudeCode.md` Version 1.5 umgesetzt. Container angelegt, **nicht gestartet** (wie vorgegeben).
**Vorbereitung (rein lesend):**
- `man -P cat pct.conf` auf dem Host geprüft: Parametername `cpuunits` existiert unverändert in Proxmox 9 (kein Umbenennung gegenüber der Vorgabe).
- `pveam update && pveam available --section system | grep debian-12`: aktuellstes verfügbares Debian-12-Template ist `debian-12-standard_12.12-1_amd64.tar.zst`.
- `pveam list local`: Template war bereits lokal vorhanden (`local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst`) – kein Download nötig.
**Ausgeführter Befehl:**
```
pct create 110 local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst \
--hostname papersafe --unprivileged 1 --ostype debian \
--cores 2 --cpuunits 50 --memory 4096 --swap 512 \
--rootfs local-lvm:20 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto,type=veth \
--features nesting=1,keyctl=1 --onboot 1
```
Lief ohne Fehler durch (SSH-Hostkeys des Containers wurden beim Anlegen erzeugt).
**Prüfung `pct config 110`:**
```
arch: amd64
cores: 2
cpuunits: 50
features: nesting=1,keyctl=1
hostname: papersafe
memory: 4096
net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:E0:81:A8,ip=dhcp,ip6=auto,type=veth
onboot: 1
ostype: debian
rootfs: local-lvm:vm-110-disk-0,size=20G
swap: 512
unprivileged: 1
```
Alle Werte stimmen mit der Vorgabe überein – Kerne, cpuunits, RAM, Swap, Disk-Größe/Storage, Bridge, Features, Onboot. `pct status 110` bestätigt `stopped`. Container wurde **nicht gestartet**.
**MAC-Adresse (net0): `BC:24:11:E0:81:A8`** – wird für die DHCP-Reservierung im Router benötigt.
**Abweichungen von der ursprünglichen Phase-1-Empfehlung (in 05 v1.5 bereits als bewusste Entscheidung dokumentiert, hier nur zur Vollständigkeit im Ist-Protokoll wiederholt):**
- Storage `local-lvm` statt `nvme-thin` – Entkopplung der Container-I/O von Home Assistant (VM 109), das auf `nvme-thin` liegt
- `ip=dhcp` statt statischer IP im Container – konsistent mit allen bestehenden Gästen, feste Adresse wird als Router-Reservierung anhand der MAC vergeben
- `cpuunits 50` (Standard 100) neu ergänzt – Home Assistant soll bei CPU-Konflikten (z. B. OCR-Lastspitzen) Vorrang behalten
Keine unerwarteten Probleme aufgetreten.
---
### Phase 2b – Grundsystem und Docker
**18.07.2026, 16:45 Uhr.** DHCP-Reservierung durch Nutzer eingerichtet (MAC `BC:24:11:E0:81:A8` → `192.168.2.35`). Container gestartet, Grundsystem eingerichtet, Docker installiert und funktionsgeprüft.
**1. IPv6 entfernen – Abweichung von der Vorgabe:**
`pct set 110 --net0 ...,ip6=none,...` schlug fehl:
```
400 Parameter verification failed.
net0.ip6: invalid format - value does not look like a valid ipv6 network configuration
```
Gegen `man pct.conf` (Abschnitt `net[n]`) geprüft: gültige Werte für `ip6` sind `IPv6/CIDR|auto|dhcp|manual` – `none` existiert nicht. Gemäß Grundregel 4 (Doku gewinnt) stattdessen `ip6=manual` gesetzt (MAC dabei unverändert gelassen):
```
pct set 110 --net0 name=eth0,bridge=vmbr0,hwaddr=BC:24:11:E0:81:A8,ip=dhcp,ip6=manual,type=veth
```
Nach dem Start zeigte sich, dass `ip6=manual` nur verhindert, dass **Proxmox** eine IPv6-Adresse verwaltet – der Debian-Kernel bezog trotzdem per SLAAC eine öffentliche IPv6-Adresse (`2001:16b8:...`). Das widerspricht der erklärten Absicht („für ein LAN-internes Archiv nicht nötig"). Daher zusätzlich im Gast selbst deaktiviert:
```
cat > /etc/sysctl.d/99-disable-ipv6.conf <<EOF
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
EOF
sysctl --system
```
Danach nur noch `192.168.2.35` (IPv4) aktiv, verifiziert per `hostname -I`. Die `permission denied`-Meldungen bei `sysctl --system` für `kernel.pid_max` und `fs.protected_*` sind unabhängig davon und normales Verhalten unprivilegierter LXC-Container (non-namespaced Kernel-Parameter) – keine Fehlfunktion.
**2. Container starten, IP prüfen, Duplikatsprüfung:**
Vor dem Start: `ping -c3 -W1 192.168.2.35` (100% Verlust) und `nmap -sn 192.168.2.35` (Host down) vom Proxmox-Host aus – keine Antwort, Adresse war frei. `arping` ist auf dem Host nicht installiert, daher nur ICMP-basierte Prüfung möglich (keine 100%ige Garantie bei Geräten, die ICMP blockieren, aber in diesem Adressbereich plausibel).
`pct start 110` → `pct exec 110 -- ip -4 -o addr show eth0` bestätigt `192.168.2.35/24`, exakt wie reserviert. Keine Abweichung, kein Stopp nötig.
**3. Grundsystem:**
```
apt-get update
apt-get -y full-upgrade
apt-get -y autoremove && apt-get clean
sed -i 's/^# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen
locale-gen
update-locale LANG=de_DE.UTF-8
timedatectl set-timezone Europe/Berlin
apt-get -y install curl ca-certificates gnupg unattended-upgrades
```
`/etc/default/locale` zeigt `LANG=de_DE.UTF-8`, `locale -a` listet `de_DE.utf8`. **Beobachtung, kein Fehler:** `pct exec` (lxc-attach) durchläuft PAM nicht und vererbt teils Umgebungsvariablen vom aufrufenden Host-Prozess – deshalb zeigten `locale`-Aufrufe über `pct exec` weiterhin `en_US.UTF-8`/`POSIX` und die Warnung „Cannot set LC_CTYPE to default locale". Bei einem echten Login (SSH, Konsole) greift PAM und liest `/etc/default/locale` korrekt. `timedatectl show --property=Timezone` bestätigt `Europe/Berlin`.
`unattended-upgrades` installiert, Service aktiv (`systemctl is-active` → `active`). `/etc/apt/apt.conf.d/20auto-upgrades`: `Update-Package-Lists "1"`, `Unattended-Upgrade "1"`. `/etc/apt/apt.conf.d/50unattended-upgrades`: Debian-Security-Origin-Zeilen sind in der Standardkonfiguration bereits aktiv (unkommentiert).
**4. Docker – aus dem offiziellen Repository:**
Vor der Installation `docs.docker.com/engine/install/debian/` per WebFetch geprüft (Grundregel 4/7 aus 05: keine Befehle aus dem Gedächtnis). Die aktuelle Doku nutzt das **Deb822-Quellenformat** (`docker.sources`), nicht mehr die klassische `.list`-Datei mit `deb [signed-by=...]`-Zeile – das ist neuer als meine letzte bekannte Version der Doku und wurde entsprechend umgesetzt:
```
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
cat > /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: bookworm
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
EOF
apt-get update
apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
Kein `get.docker.com`-Skript verwendet (Grundregel 6).
**Installierte Versionen:**
| Komponente | Version |
|---|---|
| Docker Engine (Client+Server) | 29.6.2 |
| containerd | v2.2.6 |
| runc | 1.3.6 |
| Docker Compose Plugin | v5.3.1 |
| Docker Buildx Plugin | 0.35.0 |
`systemctl is-active docker` → `active`.
**5. Funktionsprüfung:**
```
docker run --rm hello-world
```
Lief **auf Anhieb durch** – Image wurde gezogen, Container lief, Ausgabe „Hello from Docker!" erschien. Keine Probleme mit `nesting`/`keyctl`, kein Ausweichen auf einen privilegierten Container nötig.
**Zusammenfassung der Abweichungen von 05 in Phase 2b:**
- `ip6=none` (05, Erledigt-Tabelle Phase 2) existiert nicht in Proxmox 9 → `ip6=manual` verwendet, zusätzlich IPv6 im Gast per sysctl deaktiviert, um die erklärte Absicht („kein IPv6 nötig") tatsächlich zu erfüllen
- Docker-Repository-Format ist Deb822 (`docker.sources`) statt der klassischen `.list`-Datei – aktueller Stand laut `docs.docker.com`, keine inhaltliche Abweichung von der Vorgabe („offizielles Repository, kein Convenience-Skript")
Keine Sackgassen, keine unerwarteten Fehler außer den beiden oben dokumentierten und gelösten Punkten.
---
### Phase 3 – Paperless-ngx installieren
**18.07.2026, 17:40 Uhr.** Daten-Volume angelegt, Compose-Stack (PostgreSQL, Redis, Gotenberg, Tika, Webserver) aus der aktuellen Doku erstellt, gestartet und funktionsgeprüft. **Noch keine Testdokumente eingespielt** (Phase 4).
**1. Daten-Volume `mp0`:**
```
pct stop 110
pct set 110 --mp0 local-lvm:200,mp=/opt/paperless/data,backup=1
pct start 110
```
`pct config 110` zeigt `mp0: local-lvm:vm-110-disk-1,mp=/opt/paperless/data,backup=1,size=200G`. Syntax gegen `man pct.conf` (Abschnitt `mp[n]`) geprüft, passte wie erwartet. Nach dem Start: `df -h /opt/paperless/data` zeigt 196G Größe, 186G frei; Schreibtest (`touch`/`rm` einer Testdatei) erfolgreich – Volume ist gemountet und beschreibbar.
**2. Doku-Recherche (Grundregel 4):**
`docs.paperless-ngx.com` lieferte durchgehend **HTTP 403** (sowohl von hier als auch per `curl` direkt aus dem Container, auch mit Browser-User-Agent) – vermutlich ein Bot-Schutz. Ausgewichen auf das GitHub-Release, wie in Grundregel 4 als Alternative vorgesehen. Aktuellstes Release laut GitHub-API: **v2.20.15** (veröffentlicht 27.04.2026).
Referenzdatei: `docker/compose/docker-compose.postgres-tika.yml` aus dem Tag `v2.20.15`. Dabei zwei Abweichungen von der offiziellen Vorlage festgestellt und korrigiert:
- Die offizielle Datei nutzt `ghcr.io/paperless-ngx/paperless-ngx:latest` und `docker.io/apache/tika:latest` – beide **verstoßen gegen Grundregel 5** (keine `latest`-Tags). Gepinnt auf `paperless-ngx:2.20.15` (passend zum Release) und `apache/tika:3.3.1.0` (aktuellste stabile Tika-Version laut Docker Hub, Stand 27.05.2026).
- `postgres:18` und `redis:8` sind Major-Version-Floating-Tags (kein `latest`, aber auch nicht voll reproduzierbar). Im Sinne von Grundregel 5 („Rollback und Nachvollziehbarkeit") auf konkrete Patch-Versionen gepinnt: `postgres:18.4-bookworm`, `redis:8.8.0`.
- `gotenberg/gotenberg:8.25` war bereits explizit gepinnt, unverändert übernommen.
**`PAPERLESS_FILENAME_FORMAT`-Syntax – Abweichung von 05:** Die Vorgabe nennt `{created_year}/{correspondent}/{title}` (einfache Klammern). Laut `docs/advanced_usage.md` (Tag v2.20.15) verwendet das Filename-Handling **Jinja2-Templates mit doppelten Klammern**: „The filename formatting uses Jinja templates to build the filename." Gemäß Grundregel 4 (Doku gewinnt) verwendet: `PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ correspondent }}/{{ title }}`. Alle drei Platzhalternamen (`created_year`, `correspondent`, `title`) existieren unverändert, nur die Klammer-Syntax war veraltet.
**3. Verzeichnisstruktur:**
```
mkdir -p /opt/paperless/data/{data,media,consume,export,pgdata,redisdata}
chown -R 1000:1000 /opt/paperless/data/data /opt/paperless/data/media /opt/paperless/data/consume /opt/paperless/data/export
```
`pgdata` und `redisdata` bewusst nicht vorab gechownt – die offiziellen Postgres-/Redis-Images regeln die Berechtigung ihres Datenverzeichnisses beim ersten Start selbst. Funktionierte ohne Probleme (siehe unten).
**4. Secrets generiert (im Container, `openssl rand -base64`):** `PAPERLESS_SECRET_KEY` (64 Zeichen), `POSTGRES_PASSWORD` (32 Zeichen), `PAPERLESS_ADMIN_PASSWORD` (24 Zeichen). Nicht in dieser Notiz oder im Vault gespeichert – **generiert, separat im Chat übermittelt** (Grundregel 7). Die temporäre Klartext-Datei auf dem Container wurde nach Auslesen sofort gelöscht (`rm /root/.papersafe_secrets_tmp`, Löschung verifiziert).
**5. Dateien unter `/opt/paperless/` (Rechte `600`, außer `docker-compose.yml`):**
- `.env`: `COMPOSE_PROJECT_NAME=paperless`, `POSTGRES_PASSWORD=<generiert>` (wird sowohl im `db`- als auch im `webserver`-Service über `${POSTGRES_PASSWORD}` referenziert, damit beide Seiten synchron bleiben)
- `docker-compose.env`: `USERMAP_UID=1000`, `USERMAP_GID=1000`, `PAPERLESS_URL=http://192.168.2.35:8000`, `PAPERLESS_SECRET_KEY=<generiert>`, `PAPERLESS_TIME_ZONE=Europe/Berlin`, `PAPERLESS_OCR_LANGUAGE=deu+eng`, `PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ correspondent }}/{{ title }}`, `PAPERLESS_ADMIN_USER=admin`, `PAPERLESS_ADMIN_PASSWORD=<generiert>`, `PAPERLESS_ADMIN_MAIL=root@localhost`
- `docker-compose.yml`: fünf Services (`broker`, `db`, `webserver`, `gotenberg`, `tika`), alle Datenverzeichnisse als **Bind-Mounts** unter `./data/…` (relativ zu `/opt/paperless/`, landet damit auf dem 200-GB-Volume) statt der in der Vorlage verwendeten benannten Docker-Volumes – wie in 05 gefordert. Kein `volumes:`-Top-Level-Abschnitt mehr nötig.
Anmerkung, nicht Teil des expliziten Auftrags: `PAPERLESS_ADMIN_USER=admin` und `PAPERLESS_ADMIN_MAIL=root@localhost` als naheliegende Standardwerte gewählt, da nicht vorgegeben – bei Bedarf änderbar (Passwort-Reset über `docker compose exec webserver python manage.py changepassword admin`).
**6. Build/Start:**
```
cd /opt/paperless
docker compose config --quiet # Syntaxprüfung, OK
docker compose pull # alle 5 Images exakt in den gepinnten Versionen geladen
docker compose up -d
```
Alle fünf Container liefen sofort hoch, keine Restart-Schleifen. `docker compose ps` nach Anlaufphase:
| Service | Image | Status |
|---|---|---|
| broker | redis:8.8.0 | Up |
| db | postgres:18.4-bookworm | Up |
| gotenberg | gotenberg:8.25 | Up |
| tika | apache/tika:3.3.1.0 | Up |
| webserver | paperless-ngx:2.20.15 | Up (healthy) |
Webserver-Logs: Migrationen liefen vollständig durch, `Created superuser "admin" with provided password.`, keine Fehler oder Warnungen. `du -sh /opt/paperless/data/*` bestätigt, dass Postgres (`pgdata`, 68 M) und der Such-Index (`data`, 56 K) tatsächlich auf dem Bind-Mount landen; `media`/`consume`/`export` noch leer (erwartungsgemäß, keine Dokumente eingespielt).
**7. Prüfung:**
- `curl -L http://192.168.2.35:8000/` → folgt Redirect zu `/accounts/login/?next=/`, finaler Status `200` – Weboberfläche erreichbar
- Login-Test über die API (`POST /api/token/` mit `admin`+generiertem Passwort): **erste Prüfung schlug mit 400 fehl** – Sackgasse, aber schnell gelöst: Ursache war fehlendes URL-Encoding des base64-Passworts in `curl -d` (enthielt `+`/`/`, wurde als Leerzeichen fehlinterpretiert). Mit `--data-urlencode` wiederholt: **`200`, Token erhalten** – Login funktioniert einwandfrei. Kein Problem der Installation, nur des Testbefehls.
**Zusammenfassung der Abweichungen von 05 in Phase 3:**
- `docs.paperless-ngx.com` nicht erreichbar (403) → GitHub-Release als Quelle verwendet (in Grundregel 4 als Alternative vorgesehen)
- `paperless-ngx:latest` und `tika:latest` in der offiziellen Vorlage → auf konkrete Versionen gepinnt (`2.20.15` / `3.3.1.0`)
- `postgres:18`/`redis:8` (Major-Version-Tags) → auf Patch-Versionen gepinnt (`18.4-bookworm` / `8.8.0`) im Sinne der Nachvollziehbarkeits-Begründung aus Grundregel 5
- `PAPERLESS_FILENAME_FORMAT`-Syntax von einfachen auf doppelte Klammern (Jinja2) korrigiert, Platzhalternamen unverändert
- `PAPERLESS_ADMIN_USER`/`_MAIL` waren nicht vorgegeben, sinnvolle Standardwerte gewählt (änderbar)
Keine offenen technischen Probleme. Container 110 läuft stabil mit allen fünf Diensten.
---
### Phase 4 – Funktionsprüfung
**18.07.2026, 16:20–16:30 Uhr.** Zwei Testdokumente über den Consume-Ordner eingespielt, Ablagestruktur verifiziert, Lastverhalten beobachtet, Testdokumente wieder entfernt, anschließend Härtung (Admin-Secrets aus `docker-compose.env`) durchgeführt.
**1. Bildbasiertes PDF ohne Textebene – Erzeugung und Nachweis:**
Erzeugt **im `webserver`-Container** (dort sind ImageMagick, Ghostscript und Poppler-Utils ohnehin vorhanden, keine zusätzliche Installation nötig):
```
convert -size 1700x2200 xc:white -font /usr/share/fonts/opentype/urw-base35/NimbusSans-Regular.otf \
-pointsize 46 -fill black -gravity NorthWest \
-annotate +100+150 'PaperSafe Testbeleg' \
-annotate +100+260 'Rechnungsnummer 4711' \
-annotate +100+330 'Prüfsumme: Größe und Qualität' \
-annotate +100+400 'Straße: Königsallee, München' \
-annotate +100+470 'Betrag: 123,45 Euro' \
-annotate +100+540 'Stichwort: Wärmepumpe' \
-annotate +100+680 'Sonderzeichen: ÄÖÜ äöü ß' \
/tmp/ocrtest/page.png
convert /tmp/ocrtest/page.png -density 300 /tmp/ocrtest/ocrtest-scan.pdf
```
Text wurde direkt als Rastergrafik (PNG, Graustufen 1700×2200) gerendert und daraus das PDF gebaut – kein PDF-Erzeuger mit Textausgabe beteiligt. **Nachweis der fehlenden Textebene vor dem Einspielen:**
- `pdffonts ocrtest-scan.pdf` → leere Ergebnisliste (keine eingebetteten Fonts)
- `pdftotext ocrtest-scan.pdf -` → keine Textausgabe (nur Seitenumbruchzeichen)
Visuelle Kontrolle des gerenderten Bildes bestätigt korrekte Darstellung inkl. Umlaute vor dem Konsum.
**Sackgasse:** Erster Versuch, den Text aus einer Datei per `@textdatei` an `convert -annotate` zu übergeben, scheiterte an der ImageMagick-Sicherheitsrichtlinie (`not allowed by the security policy`). Zusätzlich wurde beim ersten Durchlauf aus Vorsicht vor Quoting-Problemen über mehrere Shell-Ebenen (SSH → `pct exec` → `docker compose exec`) mit ASCII-Ersatzschreibweise (`ss`/`oe`/`ae`) gearbeitet – das verfehlte den eigentlichen Zweck des Tests. Gelöst durch: Text direkt als Kommandozeilenargumente ohne `@`-Datei-Referenz, und das Skript einmal lokal geschrieben und unverändert per `pct push` in den Container übertragen (keine verschachtelten Shell-Quotings mehr) – danach mit echten Umlauten in allen Wörtern.
**Verarbeitung im Consume-Ordner:**
- Datei um **16:26:04 Uhr** nach `/opt/paperless/data/consume/ocrtest-scan.pdf` kopiert
- Automatisch eingelesen, in der API sichtbar (`added`) um **16:26:10 Uhr** → **Verarbeitungsdauer ca. 6 Sekunden**
- **Erkannter Text (OCR, Tesseract `deu+eng`), Wortlaut aus der API:**
```
PaperSafe Testbeleg
Rechnungsnummer 4711
Prüfsumme: Größe und Qualität
Straße: Königsallee, München
Betrag: 123,45 Euro
Stichwort: Wärmepumpe
Sonderzeichen: AOU aöü ß
```
Alle Fließtext-Wörter mit Umlauten (Prüfsumme, Größe, Qualität, Straße, Königsallee, München, Wärmepumpe) **korrekt erkannt**, ebenso die Kleinbuchstaben-Umlaute und ß in der letzten Zeile. Einzige Abweichung: Die drei **großen** Umlaute ÄÖÜ in der Testzeile „Sonderzeichen" wurden als „AOU" ohne Pünktchen erkannt – eine bekannte Tesseract-Schwäche bei großen Umlauten, trat aber bei keinem einzigen Umlaut in normalen Wörtern auf. Für den praktischen Einsatz (private Belege, i. d. R. keine durchgängigen Großbuchstaben) unkritisch.
- **Volltextsuche:** `GET /api/documents/?query=Wärmepumpe` → 1 Treffer, korrektes Dokument, Highlighting bestätigt Treffer im Wort „Wärmepumpe"
**2. Office-Dokument (.docx):**
Da im Container kein LibreOffice/`soffice` installiert ist (Konvertierung läuft über den `gotenberg`-Dienst, wie in Phase 3 vorgesehen), wurde ein minimales gültiges `.docx` (OOXML-Zip mit `[Content_Types].xml`, `_rels/.rels`, `word/document.xml`) per kurzem Python-Skript (`zipfile`-Modul der Standardbibliothek, keine Zusatzpakete) direkt im `webserver`-Container erzeugt, mit deutschem Text inkl. Umlauten (ÄÖÜ äöü ß). Gültigkeit vorab geprüft: `file` erkennt es korrekt als „Microsoft Word 2007+", `zipfile` listet die drei erwarteten Teile.
- Datei um 16:26:04 Uhr zusammen mit dem PDF nach `/opt/paperless/data/consume/testbeleg.docx` kopiert
- Automatisch eingelesen (Gotenberg-Konvertierung zu PDF, danach Textextraktion), sichtbar in der API um **16:26:13 Uhr** → **Verarbeitungsdauer ca. 9 Sekunden**
- Extrahierter Text vollständig und **fehlerfrei**, inklusive `ÄÖÜ äöü ß` (hier keine OCR nötig, da Text direkt aus dem Office-Format extrahiert wird – kein Nachweis einer OCR-Schwäche wie bei Punkt 1)
- Volltextsuche nach „Dokumentenpruefung" → 1 Treffer, korrektes Dokument
**3. Ablagestruktur:**
Ohne zugewiesenen Korrespondenten landeten beide Dateien zunächst unter `{{ created_year }}/none/{{ title }}` (Paperless-Fallback „none", wenn das Template-Feld leer ist) – erwartungsgemäß, da keinem der Testdokumente ein Korrespondent zugeordnet war:
```
/opt/paperless/data/media/documents/archive/2026/none/ocrtest-scan.pdf
/opt/paperless/data/media/documents/originals/2026/none/ocrtest-scan.pdf
/opt/paperless/data/media/documents/archive/2026/none/testbeleg.pdf
/opt/paperless/data/media/documents/originals/2026/none/testbeleg.docx
```
Um die Korrespondent-Ebene tatsächlich zu verifizieren, wurde testweise ein Korrespondent „Testfirma GmbH" angelegt und dem PDF-Dokument zugewiesen. Paperless verschob die Datei daraufhin automatisch:
```
/opt/paperless/data/media/documents/archive/2026/Testfirma GmbH/ocrtest-scan.pdf
/opt/paperless/data/media/documents/originals/2026/Testfirma GmbH/ocrtest-scan.pdf
```
**Struktur `Jahr/Korrespondent/Titel` damit vollständig bestätigt.**
**4. Lastverhalten (Proxmox-Host, während der Verarbeitung 16:25:56–16:28:07 Uhr, Messung alle 2s):**
| Zeitpunkt | Load Average (1 Min) | HA-Antwortzeit (`curl` auf `192.168.2.154:8123`) |
|---|---|---|
| Baseline (vor dem Ablegen) | 0,19–0,21 | 0,7–1,0 ms |
| Peak (ca. 16 Sek. nach Ablegen) | **0,57** | 0,7 ms – keine Auffälligkeit |
| Danach abklingend | zurück auf 0,10 nach ~2 Min | durchgehend 0,6–2,4 ms (Rauschen, kein Trend) |
Host hat 8 physische Kerne – ein Peak von 0,57 ist eine sehr geringe Auslastung (< 10 % eines Kerns rechnerisch). Die HA-Antwortzeit zeigte **keine erkennbare Verzögerung** während der Verarbeitung; alle Werte blieben im Millisekundenbereich ohne Zusammenhang mit dem Lastpeak. **Einschränkung:** Beide Testdokumente waren klein (1 Seite PDF, kurzer Text) und die Verarbeitung war mit 6–9 Sekunden sehr schnell abgeschlossen – der Test bestätigt, dass `cpuunits 50` bei diesem kleinen Umfang keinerlei spürbare Auswirkung auf Home Assistant hat, sagt aber nichts über das Verhalten bei großen Batches (z. B. gleichzeitiger Import vieler mehrseitiger Scans) aus. Das wäre erst beim späteren Massenimport (ausdrücklich nicht Teil dieser Phase) zu beobachten.
**5. Verarbeitungsdauer (Ablegen im Consume-Ordner bis Verfügbarkeit in der API):**
| Dokument | Ablegen | Verfügbar | Dauer |
|---|---|---|---|
| `ocrtest-scan.pdf` (OCR) | 16:26:04 | 16:26:10 | ~6 s |
| `testbeleg.docx` (Gotenberg/Tika) | 16:26:04 | 16:26:13 | ~9 s |
**6. Aufräumen:**
Beide Dokumente über die API in den Papierkorb verschoben (`DELETE /api/documents/{id}/`, je HTTP 204) und anschließend endgültig gelöscht (`POST /api/trash/` mit `action=empty`, HTTP 200). Ergebnis geprüft:
- `GET /api/documents/` → `count: 0`
- `find /opt/paperless/data/media/documents -type f` → nur noch `media.lock`, keine Dokumentdateien, keine Thumbnails
- Consume-Ordner leer (Dateien wurden von Paperless nach erfolgreichem Import automatisch entfernt)
- Testweise angelegter Korrespondent „Testfirma GmbH" ebenfalls gelöscht
- Alle temporären Test- und Hilfsdateien (Skripte, generierte Bilder/PDFs im Container, Zwischenstände auf dem Proxmox-Host) entfernt
**7. Härtung – Admin-Secrets aus `docker-compose.env` entfernt:**
```
cp docker-compose.env docker-compose.env.bak-vor-haertung # temporär, s.u.
sed -i '/^PAPERLESS_ADMIN_USER=/d;/^PAPERLESS_ADMIN_PASSWORD=/d;/^PAPERLESS_ADMIN_MAIL=/d' docker-compose.env
docker compose up -d
```
Nur der `webserver`-Dienst wurde neu erstellt (einziger Dienst, der `docker-compose.env` referenziert) – Postgres, Redis, Gotenberg und Tika blieben unangetastet und liefen durchgehend weiter. Nach Neustart (`health: healthy` nach ca. 30 Sekunden): Login-Test über `/api/token/` mit dem bereits bestehenden Superuser **erfolgreich (HTTP 200)** – der Account existiert unabhängig von den Umgebungsvariablen in der Datenbank fort, wie erwartet. Die Sicherungsdatei `docker-compose.env.bak-vor-haertung` (enthielt das Passwort im Klartext) wurde danach sofort wieder gelöscht, da sie den eigentlichen Zweck der Härtung unterlaufen hätte. `docker-compose.env` steht weiterhin mit Rechten `600` root:root, jetzt ohne die drei Admin-Variablen.
**Zusammenfassung der Abweichungen von 05 in Phase 4:**
- Testwerkzeuge (ImageMagick, Ghostscript, Poppler-Utils, Python) waren bereits im `webserver`-Container vorhanden – keine zusätzliche Installation auf dem LXC-Host oder im Container nötig
- Office-Testdokument wurde mangels LibreOffice im Container nicht mit einer Office-Suite erzeugt, sondern als minimales gültiges OOXML-`.docx` per Python `zipfile` gebaut – inhaltlich gleichwertig für den Test des Gotenberg/Tika-Pfads, da Gotenberg unabhängig vom Ursprungswerkzeug jedes gültige `.docx` konvertiert
Keine unerwarteten Fehler in der Kernfunktion. Einzige Auffälligkeit: OCR-Schwäche bei großen Umlauten (ÄÖÜ) in Versalien-Kontext, siehe oben – kein Blocker.
---
### Phase 5 – Backup einrichten
**18.07.2026, 16:45–17:05 Uhr.** Ausgeführt **nicht** über Claude Code, sondern direkt aus der Obsidian-Sitzung per SSH (`proxmox`-Alias), da die Claude-Code-Credits aufgebraucht waren. Gleiche Regeln, gleiche Protokollpflicht.
> **Hinweis zur Chronologie:** Die Zeitstempel der Phasen 3 und 4 stehen in umgekehrter Reihenfolge (Phase 3 mit 17:40, Phase 4 mit 16:20–16:30). Der Eintrag zu Phase 4 dürfte die falsche Uhrzeit tragen. Gemäß Protokollregel 1 wird das hier korrigierend vermerkt, nicht überschrieben. Ab Phase 5 sind die Zeiten gegen die Host-Uhr geprüft.
**1. NAS-Freigabe eingebunden – zwei Anläufe:**
Erster Versuch mit dem separaten Benutzer `papersafeadmin` scheiterte:
```
CIFS: Status code returned 0xc000006d STATUS_LOGON_FAILURE
```
Die Zugangsdaten kamen korrekt an (Passwortlänge stimmte, Sonderzeichen `^`/`*` waren nicht die Ursache) – das NAS wies den Benutzer ab. Weitere Versuche wurden **bewusst gestoppt**, da DSM eine Auto-Block-Funktion hat und die Proxmox-IP über dieselbe Adresse die regulären Backups schreibt. Nach Rückfrage stellte der Nutzer auf den bestehenden Benutzer `dsadmin` um, damit funktionierte der Mount sofort.
> **Abweichung von der Empfehlung, bewusst:** In [[04-Betrieb-Backup]] war ein **eigener** DSM-Benutzer je Freigabe empfohlen, damit ein kompromittierter Zugang nicht beide Sicherungsebenen gleichzeitig erreicht. Genutzt wird nun für beide Freigaben derselbe Benutzer `dsadmin`. Die Trennung der Ebenen besteht damit nur noch auf Verzeichnisebene, nicht auf Berechtigungsebene.
Dauerhafte Einbindung über `/etc/fstab`:
```
//dsobe2023/papersafeexport /mnt/papersafe-export cifs credentials=/root/.smbcred-papersafeexport,uid=0,gid=0,file_mode=0600,dir_mode=0700,vers=3.1.1,soft,_netdev,nofail 0 0
```
Remount-Test über `umount` + `mount /mnt/papersafe-export` erfolgreich, Schreib- und Löschtest bestanden. Freigabe: 7,0 TB gesamt, 3,2 TB frei.
**2. Bestehender Backup-Job – keine Änderung nötig:**
`/etc/pve/jobs.cfg` zeigt `all 1` – der Job erfasst alle Gäste automatisch, CT 110 also ohne Ergänzung einer ID-Liste. Zeitplan `06,12,18:00`, `prune-backups keep-daily=5`, Storage `DSOBE203_V2`, Modus snapshot, Kompression zstd. **Am bestehenden Job wurde nichts verändert.**
Manueller Testlauf:
```
vzdump 110 --storage DSOBE203_V2 --mode snapshot --compress zstd
```
Ergebnis: `vzdump-lxc-110-2026_07_18-16_56_27.tar.zst`, 6,9 GiB geschrieben, **Archivgröße 3,43 GB**, Dauer 49 Sekunden, Container war 1 Sekunde eingefroren. Erfolgreich abgeschlossen.
**3. `document_exporter` – Optionen vorab geprüft:**
`document_exporter --help` im Container ausgelesen statt aus dem Gedächtnis zu arbeiten. Verwendet werden `-z` (Zip), `-zn` (Name), `-nt` (keine Thumbnails – regenerierbar, spart Platz).
**Sackgasse:** Der erste Skriptlauf brach ab, weil die Datei nicht gefunden wurde. Ursache: `document_exporter` hängt die Endung `.zip` **selbst** an – aus `-zn name.zip` wird `name.zip.zip`. Skript korrigiert, sodass `-zn` den Namen ohne Endung erhält und die erwartete Datei `${NAME}.zip` heißt. Die beiden fehlerhaften Testdateien wurden entfernt.
**4. Export-Skript `/usr/local/sbin/papersafe-export.sh` (Rechte `700`):**
Ablauf: prüft, ob der Mount steht (bricht sonst ab, statt ins Leere zu sichern) → erzeugt das Zip im Container → prüft, dass die Datei existiert → kopiert per `pct pull` auf die Freigabe → **vergleicht die Dateigröße auf beiden Seiten** → löscht die Arbeitskopie im Container → entfernt Exporte älter als 60 Tage.
Testlauf erfolgreich: `papersafe-export-2026-07-18_1656.zip`, 4.107 Bytes. Inhalt geprüft: `manifest.json` und `metadata.json` – korrekt, da noch keine Dokumente im Bestand sind.
**5. Zeitplan (Nutzerentscheidung: wöchentlich):**
| Unit | Zeitplan |
|---|---|
| `papersafe-export.timer` | Sonntag 03:00, `RandomizedDelaySec=15min`, `Persistent=true` |
| `thinpool-check.timer` | täglich 07:00 |
`Persistent=true` sorgt dafür, dass ein verpasster Lauf (Host aus) nachgeholt wird. Bei Fehlschlag verschickt `
[email protected]` eine Mail an root. Testlauf über `systemctl start papersafe-export.service` erfolgreich, zweites Zip auf der Freigabe erzeugt.
**6. Befund Thin Pool – neu, nicht Teil des Auftrags:**
Das `vzdump` gab aus:
```
WARNING: You have not turned on protection against thin pools running out of space.
WARNING: Sum of all thin volume sizes (<826.02 GiB) exceeds the size of thin pool pve/data
and the amount of free space in volume group (16.00 GiB).
```
Ist-Stand: Pool `pve/data` 794,30 GB, **5,40 % belegt** (Metadaten 0,42 %); Pool `vg-nvme/data` 465,52 GB, 12,50 % belegt (Metadaten 19,84 %). Volume Group `pve` hat nur noch **16 GB frei**, `vg-nvme` ist voll ausgelegt.
Akut unkritisch, aber: Läuft ein Thin Pool voll, schalten **alle** darin liegenden Volumes auf read-only – das träfe auch CT 104, CT 105 (Zigbee2MQTT), CT 108 und VM 103. Das in Phase 3 angelegte 200-GB-Volume erhöht die Überbuchung (real belegt davon: 2,10 %).
Nach Rücksprache eingerichtet: `/usr/local/sbin/thinpool-check.sh` prüft täglich alle Thin Pools und meldet per Mail an root ab 75 % (Warnung) bzw. 85 % (kritisch), ebenso ab 75 % Metadaten-Belegung. **Kein Eingriff in bestehende Volumes.**
Der Alarmpfad wurde aktiv getestet (Schwellwert temporär auf 1 % gesetzt): Mail wurde erzeugt und laut `mail.log` über den Gmail-Relay an `
[email protected]` zugestellt (`status=sent, 250 2.0.0 OK`). Der Normalfall (alle Pools unterhalb der Schwelle) liefert Exit-Code 0 und einen Syslog-Eintrag ohne Mail.
**7. Weiterer Befund:** Es existiert ein Snapshot `snap_vm-110-disk-1_Basis20260718` über das 200-GB-Volume sowie ältere `_Stand`-Snapshots von CT 103/104/105/108. Snapshots halten in Thin Pools alte Datenblöcke fest und tragen zur Belegung bei. Nicht angefasst – Aufräumen wäre eine Nutzerentscheidung.
**Zusammenfassung der Abweichungen von 05 in Phase 5:**
- Ausführung direkt per SSH aus der Obsidian-Sitzung statt über Claude Code (Credits aufgebraucht) – Nutzerentscheidung
- Gemeinsamer DSM-Benutzer `dsadmin` für beide Freigaben statt getrennter Benutzer je Freigabe – Nutzerentscheidung nach fehlgeschlagener Anmeldung mit `papersafeadmin`
- Zusätzlich eingerichtet, in 05 nicht vorgesehen: Thin-Pool-Frühwarnung (auf Basis eines Befunds aus dem Backup-Testlauf, nach Rückfrage)
**Noch offen:** Das NAS wurde nicht auf Btrfs/Snapshot Replication geprüft – Zugriff auf DSM war nicht Teil dieser Phase und ist ausdrücklich Nutzersache.
---
### Phase 6 – Restore-Test
**18.07.2026, 17:15–17:25 Uhr.** Vollständiger Wiederherstellungstest auf einer separaten Instanz. Ergebnis: **erfolgreich**, alle geprüften Ebenen wiederhergestellt.
**1. Aussagekräftige Ausgangslage geschaffen:**
Der Bestand war nach Phase 4 leer. Ein Restore mit null Dokumenten hätte nur belegt, dass das *System* zurückkommt – nicht, dass **Dokumente und Metadaten** überleben. Daher zuvor ein Testdokument erzeugt (bildbasiertes PDF, eindeutiger Suchbegriff `Restoretest9182`, Umlaute in „Straße"/„Größe"). OCR erkannte den Text korrekt.
**2. Sicherungen mit Dokument erstellt:**
| Sicherung | Ergebnis |
|---|---|
| `vzdump` CT 110 | `vzdump-lxc-110-2026_07_18-17_19_18.tar.zst`, 3,43 GB, 49 s |
| Logischer Export | `papersafe-export-2026-07-18_1720.zip`, 51.799 Bytes |
Der Zip-Inhalt wurde geprüft und enthält neben `manifest.json`/`metadata.json` **beide Dateiversionen**: `2026-07-18 restoretest.pdf` (Original) und `2026-07-18 restoretest-archive.pdf` (OCR-Version). Damit ist belegt, dass der Export nicht nur Metadaten sichert.
**3. Restore auf neue CTID 111:**
```
pct restore 111 <backup> --storage local-lvm --hostname papersafe-restoretest
```
**Wichtiger Befund:** Der Restore übernahm die **Original-MAC** `BC:24:11:E0:81:A8`. Wäre die Instanz so gestartet worden, hätte sie über die Router-Reservierung dieselbe IP `192.168.2.35` wie das Produktivsystem angefordert – IP-Konflikt mit dem laufenden Betrieb. Vor dem Start daher neue MAC vergeben (`BC:24:11:E1:FB:8D`) und `onboot 0` gesetzt, damit die Testinstanz nach einem Host-Neustart nicht von selbst wiederkommt. Zugewiesene IP: `192.168.2.48`.
> **Für künftige Restores merken:** Bei einer Wiederherstellung auf eine andere ID immer erst die MAC ändern, dann starten. Bei einem echten Notfall-Restore auf die *ursprüngliche* ID ist die alte MAC dagegen genau richtig – dann stimmt die Reservierung wieder.
**4. Prüfergebnisse der wiederhergestellten Instanz:**
| Prüfung | Ergebnis |
|---|---|
| Alle 5 Dienste | `broker`, `db`, `gotenberg`, `tika`, `webserver` – Up, webserver `healthy` |
| Weboberfläche | `http://192.168.2.48:8000` → HTTP 200 |
| Dokumentenbestand | 1 Dokument, Titel `restoretest` |
| Inhalt/OCR-Text | vollständig, inkl. `Straße` und `Größe` |
| Volltextsuche | Suche nach `Restoretest9182` → 1 Treffer, korrektes Dokument |
| Dateien auf dem Volume | Original und Archivversion unter `.../2026/none/restoretest.pdf` |
**5. Aufräumen:**
- CT 111 gestoppt und entfernt (vorher Hostname beider Container gegengeprüft, um Verwechslung mit dem Produktivsystem auszuschließen); LVM-Volumes `vm-111-disk-0/1` sauber entfernt
- Testdokument im Produktivsystem gelöscht
- **Sackgasse:** Das Löschen über `Document.global_objects.hard_delete()` entfernte den Datenbankeintrag, **nicht** aber die Dateien – dieser Weg umgeht die Aufräumroutine, die beim normalen Löschweg (Oberfläche/API) greift. Die drei verwaisten Dateien (Original, Archivversion, Thumbnail) wurden manuell entfernt, leere Verzeichnisse aufgeräumt
- `document_sanity_checker`: **keine Beanstandungen**, Bestand 0 Dokumente, unter `media/` nur noch `media.lock`
- Hilfsskripte auf Host und Container entfernt
- Die drei Test-Exportdateien auf der NAS-Freigabe entfernt, damit der erste reguläre Lauf am Sonntag einen sauberen Stand erzeugt
**Hinweis:** Die `vzdump`-Archive vom 18.07. enthalten das Testdokument. Sie laufen über die Aufbewahrungsregel `keep-daily=5` von selbst aus.
**Bewertung:** Beide Sicherungswege sind nachweislich funktionsfähig – das vzdump stellt das komplette System inklusive Datenbank und Suchindex wieder her, der logische Export enthält die Dokumentdateien plattformunabhängig. **Damit ist die Voraussetzung erfüllt, produktiv Dokumente einzuspielen.**
---
## Phase 8 – Automatischer Import aus lokalen Ordnern (18.07.2026, 18:35–18:45)
Ausgeführt per SSH aus der Obsidian-Sitzung. Grundlage: [[07-Automatischer-Import]].
**Ausgangspunkt:** Nutzerhinweis, die SWAP-Auslastung von CT 110 liege bei fast 100 %. Befund: 510 von 512 MB Swap bei nur 12 % RAM-Nutzung und 3 h Laufzeit – **kein Speichermangel**, sondern Nachwirkung der Test- und Restore-Läufe bei `vm.swappiness=60`. Der ohnehin nötige Neustart hat den Swap vollständig geräumt (danach 0 MB); `--swap 0` war nicht erforderlich und wurde **nicht** gesetzt.
**Umgesetzt:**
| Schritt | Ergebnis |
|---|---|
| DSM-Freigabe `papersafeconsume` | vom Nutzer angelegt, Papierkorb deaktiviert |
| CIFS-Mount Host | `/mnt/papersafe-consume`, `uid=101000,gid=101000` (UID-Verschiebung des unprivilegierten Containers), fstab mit `_netdev,nofail` |
| Bind-Mount Container | `mp1 → /opt/paperless/consume-net`, `backup=0` (Netzfreigabe gehört nicht in vzdump) |
| Compose | Consume-Volume von `./data/consume` auf `/opt/paperless/consume-net` umgestellt; Sicherungskopien `*.bak-20260718` |
| Variablen | `CONSUMER_RECURSIVE`, `SUBDIRS_AS_TAGS`, `POLLING=60`, `POLLING_DELAY=5`, `POLLING_RETRY_COUNT=10`, `DELETE_DUPLICATES`, `FILENAME_FORMAT_REMOVE_NONE` – alle im laufenden Container als aktiv verifiziert |
**Warum Polling:** Über CIFS/SMB feuert `inotify` nicht. Ohne `PAPERLESS_CONSUMER_POLLING` würde der Ordner unbemerkt volllaufen. `POLLING_DELAY` und `RETRY_COUNT` verhindern, dass eine über SMB noch unvollständig übertragene Datei zu früh eingelesen wird.
**Verifikation (End-to-End, nicht nur Konfigurationsabgleich):**
1. Schreib- und **Löschtest** unter UID 1000 im Container erfolgreich – Löschen ist zwingend, da Paperless die Datei nach der Verarbeitung entfernt
2. Testdatei in Unterordner `Testfirma/` abgelegt → nach einem Polling-Zyklus (~60 s) automatisch eingelesen
3. **Tag `Testfirma` automatisch aus dem Ordnernamen erzeugt** – der Kernmechanismus des Konzepts ist praktisch belegt
4. Ablage unter `2026/importtest.txt` **ohne** `none`-Ordner → `REMOVE_NONE` wirkt
5. Kein `#recycle` auf der Freigabe nach dem Abräumen → DSM-Papierkorb tatsächlich deaktiviert
6. Testdokument, Tag und Dateien entfernt, Sanity-Check ohne Befund
**Befund (bestätigt einen Punkt aus Phase 6):** `hard_delete()` über die Django-Shell entfernt den Datenbankeintrag, **aber nicht Original und Thumbnail auf der Platte**. Auch das Leeren des Papierkorbs per Shell ändert daran nichts. Die Aufräumroutine greift nur über Oberfläche bzw. API. **Konsequenz: Dokumente nicht über die Shell löschen.**
## Offene Punkte aus der Umsetzung
- [x] ~~Home Assistant VM-ID~~ – in `05` als VM 100 geführt, existiert nicht; tatsächlich VM 109 „haproxmox". In `CLAUDE.md` bereits korrigiert; `05` bleibt laut Grundregel 10 unverändert, Korrektur dort ist Sache des Nutzers
- [x] ~~Bestätigung CTID 110~~ – erledigt, Container angelegt
- [x] ~~Bestätigung Storage~~ – erledigt, `local-lvm` statt `nvme-thin` (bewusste Entscheidung, siehe Phase 2)
- [x] ~~`05-Vorgabe-ClaudeCode.md` VM-100/109-Korrektur~~ – vom Nutzer bereits in v1.4 nachgezogen
- [x] ~~DHCP-Reservierung im Router~~ – erledigt, `192.168.2.35`
- [x] ~~Container starten, Grundsystem, Docker~~ – erledigt, siehe Phase 2b
- [ ] `05-Vorgabe-ClaudeCode.md` bei Nutzer melden: `ip6=none` in Phase 2 ist kein gültiger Proxmox-9-Wert (gültig: `IPv6/CIDR|auto|dhcp|manual`); außerdem verhindert `ip6=manual` allein kein SLAAC im Gast – die zusätzliche sysctl-Deaktivierung war nötig, um „kein IPv6" tatsächlich zu erreichen
- [x] ~~Phase 3 (Paperless-ngx)~~ – erledigt, Stack läuft, Login geprüft
- [ ] `05-Vorgabe-ClaudeCode.md` bei Nutzer melden: `PAPERLESS_FILENAME_FORMAT`-Syntax in Phase 3 ist veraltet (einfache statt doppelte Klammern/Jinja2)
- [ ] `docs.paperless-ngx.com` lieferte durchgehend HTTP 403 (Bot-Schutz?) – falls das dauerhaft so bleibt, für künftige Phasen einplanen, direkt auf GitHub-Releases/-Docs auszuweichen
- [ ] `PAPERLESS_ADMIN_USER=admin` / `PAPERLESS_ADMIN_MAIL=root@localhost` waren nicht vorgegeben – bei Bedarf ändern (Passwort-Reset: `docker compose exec webserver python manage.py changepassword admin`)
- [x] ~~Phase 4 (Funktionsprüfung mit echten Testdokumenten)~~ – erledigt, siehe Phase 4. OCR, Office-Konvertierung, Ablagestruktur, Volltextsuche und Lastverhalten geprüft, Testdokumente entfernt, Admin-Secrets aus `docker-compose.env` entfernt
- [ ] OCR erkennt große Umlaute (ÄÖÜ) in Versalien-Kontext nicht zuverlässig (wurden als „AOU" ohne Pünktchen gelesen) – bei normalen Wörtern mit kleinen Umlauten trat das nicht auf. Kein Blocker, aber im Hinterkopf behalten, falls künftig Dokumente mit durchgängigen Großbuchstaben-Umlauten (z. B. Fensterbriefe in Blockschrift) durchsucht werden sollen
- [ ] Lastverhalten wurde nur mit zwei kleinen, schnell verarbeiteten Testdokumenten (6–9 Sekunden) geprüft. Aussage zu `cpuunits 50` gilt für diesen Umfang; das Verhalten bei einem größeren Batch (viele mehrseitige Scans gleichzeitig) ist damit nicht abgedeckt und wäre erst beim späteren Massenimport zu beobachten
- [x] ~~Phase 5 (Backup einrichten)~~ – erledigt, siehe Phase 5
- [ ] Zuleitung ist eingerichtet, aber noch **kein echtes Dokument** importiert – Testlauf mit 10–20 realen Dokumenten steht aus, danach erst der Gesamtbestand
- [ ] Lastverhalten beim Massenimport weiterhin unbeobachtet (siehe Punkt oben zu `cpuunits 50`); der Testlauf mit einer einzelnen Textdatei sagt darüber nichts aus
- [ ] `vm.swappiness=60` auf dem Host unverändert – der Swap wird sich beim Massenimport voraussichtlich erneut füllen. Kein Fehler, aber die rote Anzeige in der Oberfläche kommt dann wieder
- [ ] Für die Consume-Freigabe wird **derselbe DSM-Benutzer `dsadmin`** verwendet wie für den Export (siehe Phase 5) – die empfohlene Trennung je Freigabe steht weiterhin aus
- [ ] E-Mail-Import (Postfächer und Regeln) noch nicht eingerichtet
---
**Status:** Phasen 1–8 abgeschlossen. **Automatischer Ordner-Import steht und ist end-to-end verifiziert** – Freigabe eingebunden, Polling aktiv, Ordnername wird nachweislich zum Tag. Offen: Testlauf mit realen Dokumenten, danach Gesamtbestand; E-Mail-Import
**Version:** 1.8
**Letzte Aktualisierung:** 18.07.2026 18:45 Uhr
### Änderungshistorie
| Version | Datum | Uhrzeit | Änderung |
|---|---|---|---|
| 1.0 | 18.07.2026 | 14:30 | Erstfassung – Protokollgerüst mit Regeln, Statusübersicht und Eckdaten-Tabelle |
| 1.1 | 18.07.2026 | 15:15 | Phase 1 (Bestandsaufnahme) abgeschlossen: Statusübersicht, Eckdaten und Protokolleintrag gefüllt. Wichtiger Befund: VM 100 existiert nicht, Home Assistant ist VM 109 – als offener Punkt vermerkt |
| 1.2 | 18.07.2026 | 16:00 | Phase 2 erster Teil: Container 110 „papersafe" angelegt (lt. 05 v1.5 mit `local-lvm`, `ip=dhcp`, `cpuunits 50`), Konfiguration verifiziert, **nicht gestartet**. MAC-Adresse `BC:24:11:E0:81:A8` ermittelt und in Eckdaten eingetragen. Offene Punkte auf DHCP-Reservierung durch Nutzer umgestellt |
| 1.3 | 18.07.2026 | 16:45 | Phase 2b abgeschlossen: IPv6 entfernt (`ip6=none` war ungültig, `ip6=manual` + sysctl-Deaktivierung im Gast verwendet), Container gestartet und IP `192.168.2.35` verifiziert, Grundsystem (Update, Locale, Zeitzone, unattended-upgrades) eingerichtet, Docker 29.6.2 aus dem offiziellen Repository (Deb822-Format) installiert, `docker run --rm hello-world` erfolgreich. Statusübersicht Phase 2 auf „abgeschlossen" |
| 1.4 | 18.07.2026 | 17:40 | Phase 3 abgeschlossen: Mount Point `mp0` (200 GB, `/opt/paperless/data`) angelegt. Compose-Stack (Postgres 18.4, Redis 8.8.0, Gotenberg 8.25, Tika 3.3.1.0, Paperless-ngx 2.20.15 – alle Images gepinnt, `latest`-Tags aus der offiziellen Vorlage korrigiert) unter `/opt/paperless/` mit Bind-Mounts statt benannter Volumes erstellt. `docs.paperless-ngx.com` war nicht erreichbar (403), auf GitHub-Release v2.20.15 ausgewichen. `PAPERLESS_FILENAME_FORMAT`-Syntax auf Jinja2-Doppelklammern korrigiert. Secrets generiert und separat übermittelt. Stack gestartet, Weboberfläche und Login erfolgreich geprüft |
| 1.5 | 18.07.2026 | 16:35 | Phase 4 abgeschlossen: Bildbasiertes Test-PDF ohne Textebene erzeugt und Fehlen der Textebene per `pdffonts`/`pdftotext` nachgewiesen, OCR erkannte deutschen Fließtext inkl. Umlaute korrekt (Schwäche nur bei großen ÄÖÜ in Versalien-Kontext), Volltextsuche erfolgreich. Office-Testdokument (.docx) über Gotenberg/Tika korrekt konvertiert und durchsuchbar. Ablagestruktur `Jahr/Korrespondent/Titel` am Beispiel verifiziert. Lastverhalten beobachtet: Peak-Load 0,57 (von 8 Kernen), keine erkennbare Verzögerung bei Home Assistant. Verarbeitungsdauer 6–9 Sekunden je Dokument. Testdokumente vollständig entfernt (API, Papierkorb, Volume, Consume-Ordner geprüft). Härtung: `PAPERLESS_ADMIN_USER`/`_PASSWORD`/`_MAIL` aus `docker-compose.env` entfernt, Webserver neu gestartet, Login mit bestehendem Superuser weiterhin erfolgreich |
| 1.6 | 18.07.2026 | 17:05 | Phase 5 abgeschlossen (ausgeführt per SSH aus der Obsidian-Sitzung, da Claude-Code-Credits aufgebraucht): NAS-Freigabe `//dsobe2023/papersafeexport` auf `/mnt/papersafe-export` eingebunden (erst nach Wechsel des DSM-Benutzers von `papersafeadmin` auf `dsadmin`, siehe Phase 5). `vzdump` für CT 110 getestet – 3,43 GB in 49 s, bestehender Job erfasst den Container automatisch (`all 1`), unverändert gelassen. Export-Skript `papersafe-export.sh` angelegt (Sackgasse: `document_exporter` hängt `.zip` selbst an), systemd-Timer wöchentlich Sonntag 03:00 mit 60 Tagen Aufbewahrung. **Neuer Befund:** Thin-Pool-Warnung aus dem vzdump – Frühwarnung `thinpool-check.sh` täglich 07:00 eingerichtet, Alarmpfad per Testmail verifiziert. Chronologie-Hinweis zu den Zeitstempeln der Phasen 3/4 ergänzt |
| 1.7 | 18.07.2026 | 17:25 | **Phase 6 (Restore-Test) abgeschlossen und erfolgreich.** Vor dem Test ein Testdokument angelegt, damit der Restore auch Dokumente und Metadaten belegt und nicht nur das System. Backup und Export mit Dokument erstellt, Zip-Inhalt geprüft (enthält Original und Archivversion). Restore auf CT 111 – **Befund: Restore übernimmt die Original-MAC**, vor dem Start neue MAC vergeben, sonst IP-Konflikt mit dem Produktivsystem. Wiederhergestellte Instanz vollständig verifiziert (5 Dienste, Weboberfläche, Dokumentinhalt inkl. Umlaute, Volltextsuche, Dateien auf dem Volume). Anschließend CT 111 entfernt, Testdaten gelöscht (Sackgasse: `hard_delete()` lässt Dateien liegen, manuell nachgeräumt), Sanity-Check ohne Beanstandung |
| 1.8 | 18.07.2026 | 18:45 | **Phase 8 (automatischer Ordner-Import) abgeschlossen.** Anlass war der Nutzerhinweis zur SWAP-Auslastung – Befund: kein Speichermangel, Nachwirkung der Testläufe, durch den ohnehin nötigen Neustart geräumt. CIFS-Freigabe `papersafeconsume` mit `uid=101000` eingebunden (UID-Verschiebung des unprivilegierten Containers), als `mp1` mit `backup=0` durchgereicht, Compose-Consume-Pfad umgestellt, sieben Consumer-Variablen gesetzt. **Polling statt inotify, weil `inotify` über SMB nicht feuert.** End-to-End verifiziert: Löschrecht unter UID 1000, automatischer Einzug nach einem Polling-Zyklus, **Tag aus Ordnername erzeugt**, Ablage ohne `none`-Ordner, kein `#recycle`. Bestätigter Befund: Löschen über die Django-Shell lässt Dateien liegen – künftig nur über Oberfläche/API löschen |
---
[[Impressum|Impressum]] | [[Datenschutzerklärung|Datenschutz]]