Drei Scanner-Scans kommen per IMAP rein, 48 + 14 + 34 Seiten, zusammen 81 MB PDF. Alle drei crashen beim Import. Nicht beim zweiten Versuch, nicht nur der große — alle drei, reproduzierbar. Der Consumer-Job stirbt mitten in der OCR-Verarbeitung, ohne verwertbare Fehlermeldung im Paperless-Log.

Der naheliegende Verdächtige: die Datei ist zu groß, OCR braucht zu lange, Timeout. Falsch. Die Wahrheit liegt tiefer — in einem 512-MB-tmpfs, das von einem Tool gesprengt wird, das die meisten Paperless-Nutzer nie bewusst konfigurieren: unpaper.

Das Problem

Der Paperless-Container doc-vader auf darkside.local (192.168.66.70) hat /tmp als tmpfs mit 512 MB gemountet — Standard-Setup, nichts Exotisches. Jeder einzelne der drei Scans schlägt beim Import fehl.

Scans dieser Art laufen durch die volle OCR-Pipeline: OCRmyPDF ruft unpaper auf, um Seiten zu entrauschen und gerade zu rücken (Deskew/Clean). unpaper arbeitet dabei nicht auf dem komprimierten PDF, sondern konvertiert jede Seite in ein unkomprimiertes PPM-Bitmap — und das landet in /tmp.

Beim größten der drei Scans mit 48 Seiten reicht die Rechnung nicht mehr: ~26 MB pro Seite unkomprimiertes PPM, mal 48 Seiten, sind rund 1,2 GB an Temp-Daten — gegen ein 512-MB-Limit. Der Worker läuft mitten in der OCR voll, crasht, und der Consume-Job hinterlässt ein halb verarbeitetes Dokument.

Ehrlichkeitshalber: Der kleinste Scan (14 Seiten) hätte mit rechnerisch ~364 MB sogar unter das tmpfs-Limit gepasst. Bei ihm kam die zusätzlich randvolle Host-Platte (7 GB verwaiste OCR-Temp, siehe Diagnose) als zweite Ursache hinzu. Für den großen Scan war das tmpfs allein schon zu knapp — der Fix unten adressiert beide Baustellen.

Die Diagnose

Erster Schritt: Logs. Die zeigen keinen klaren Fehler, nur einen abgebrochenen Worker-Prozess.

docker logs paperless_webserver --since 1h | grep -i error

Kein brauchbarer Treffer. Also direkt auf die Container-Disk schauen, während der Import läuft:

docker exec paperless_webserver df -h /tmp

Bad (während des Imports, kurz vor dem Crash):

Filesystem      Size  Used Avail Use% Mounted on
tmpfs           512M  510M     0 100% /tmp

Volle 512 MB tmpfs mitten im OCR-Lauf — bei drei Scans mit zusammen 81 MB PDF. Das Verhältnis passt nicht, wenn man nur an die reine PDF-Größe denkt. Es passt, sobald man weiß, dass unpaper jede Seite unkomprimiert zwischenspeichert.

Zweiter Fund, unabhängig vom aktuellen Import: Die Host-Disk selbst stand bei 100 %. Grund waren 7 GB verwaister ocrmypdf.io.*-Temp-Dateien aus vorangegangenen, abgebrochenen OCR-Läufen — Reste, die nie aufgeräumt wurden, weil die Jobs mitten im Crash starben statt sauber zu terminieren.

du -sh /usr/src/paperless/scratch/ocrmypdf.io.* 2>/dev/null | sort -rh | head

Bad:

7.1G    total (ocrmypdf.io.* Verzeichnisse, teils Wochen alt)

Dritter Fund: Ein naheliegender Reflex ist, einfach PAPERLESS_OCR_TMPDIR zu setzen und das tmpfs größer zu machen. Beides greift nicht — und genau das ist der eigentliche Fallstrick.

Die Ursache

Das Perfide: Die Umgebungsvariable, die man bei OCR-Temp-Speicher reflexhaft zuerst setzt — PAPERLESS_OCR_TMPDIR — existiert in der laufenden Version schlicht nicht. Sie wird gesetzt und dann ignoriert. Ein stiller No-Op. Du denkst, du hast das Problem behoben. Der nächste Crash beweist das Gegenteil.

Der zweite Reflex — das tmpfs einfach größer ziehen, z. B. auf 2 GB — funktioniert in vielen Setups ebenfalls nicht, weil der Container-Cgroup selbst auf ein Memory-Limit von 2 GB gedeckelt ist. tmpfs zählt gegen den RAM-Cgroup. Ein 2-GB-tmpfs in einem 2-GB-Cgroup bedeutet: sobald unpaper tatsächlich 2 GB an Temp-Daten produziert, killt der OOM-Killer den Prozess, bevor das tmpfs überhaupt voll ist. Mehr tmpfs löst das strukturelle Problem nicht, es verschiebt nur den Punkt, an dem es crasht.

Die eigentliche Ursache: unpaper ist der Haupt-Temp-Verbraucher in der gesamten OCR-Pipeline — nicht OCRmyPDF selbst, nicht Tesseract. Und unpaper läuft bei jedem Scan automatisch mit, sofern man es nicht explizit abschaltet.

Die Lösung

Fünf Stellschrauben, zusammen in docker-compose.env:

# docker-compose.env auf doc-vader

# Persistenten Scratch-Pfad statt tmpfs verwenden
TMPDIR=/usr/src/paperless/scratch

# unpaper deaktivieren — das war der Haupt-Temp-Verbraucher
PAPERLESS_OCR_CLEAN=none

# PDF-Optimierung (zusätzlicher Temp-Verbrauch) abschalten
PAPERLESS_OCR_USER_ARGS={"optimize": 0}

# Parallele Worker-Threads begrenzen — mehr Seiten gleichzeitig = mehr Temp-Spitzenlast
PAPERLESS_THREADS_PER_WORKER=4

# Großzügigeres Timeout für die jetzt langsameren, aber stabilen Jobs
PAPERLESS_WORKER_TIMEOUT=7200

TMPDIR ersetzt das tmpfs-Verhalten durch einen persistenten Pfad auf der Container-Disk (/usr/src/paperless/scratch) — begrenzt nur durch den tatsächlichen Plattenplatz, nicht durch ein künstlich kleines RAM-Limit. PAPERLESS_OCR_CLEAN=none schaltet unpaper komplett ab, womit der Hauptverursacher der PPM-Temp-Dateien verschwindet. Der Trade-off: keine automatische Entrauschung/Begradigung mehr — für Scanner-Scans, die ohnehin meist sauber eingezogen werden, ein akzeptabler Tausch gegen Stabilität.

docker compose up -d --no-deps webserver

Danach den verwaisten Temp-Müll beseitigen und dafür sorgen, dass er nicht wiederkommt:

# Einmalig: alte ocrmypdf-Reste löschen
rm -rf /usr/src/paperless/scratch/ocrmypdf.io.*

# Cron: Temp-Reste älter als 24h regelmäßig wegräumen
# (crontab -e auf doc-vader)
0 3 * * * find /usr/src/paperless/scratch -maxdepth 1 -name 'ocrmypdf.io.*' -mtime +1 -exec rm -rf {} \;

Pfad-Hinweis: /usr/src/paperless/scratch ist ein Pfad im Container. Der Host-Cron trifft ihn nur, wenn das Verzeichnis als Volume/Bind-Mount 1:1 nach außen zeigt. Sonst per docker exec arbeiten (und scratch als Volume anlegen, damit es persistent ist):

docker exec -u paperless paperless_webserver \
  find /usr/src/paperless/scratch -maxdepth 1 -name 'ocrmypdf.io.*' -mtime +1 -exec rm -rf {} \;

Good (gleicher Scan, nach dem Fix):

docker exec paperless_webserver df -h /tmp
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           512M   12M   500M   3% /tmp

/tmp bleibt fast leer, weil unpaper gar nicht mehr läuft — der verbleibende Temp-Verbrauch von OCRmyPDF/Tesseract selbst ist marginal gegen das, was unpaper vorher produziert hat.

Die drei defekten Scans ließen sich mit aktivem Fix nicht automatisch neu mergen — die Paperless-Merge-API war zu dem Zeitpunkt selbst defekt. Workaround: manuelles Ghostscript-Merge der drei Einzel-PDFs, Upload über den normalen Consume-Ordner. Ergebnis: ein zusammengeführtes 96-Seiten-Dokument, sauber durch die (jetzt unpaper-freie) OCR-Pipeline gelaufen.

Lessons Learned

  • Dokumentierte Env-Variablen sind nicht automatisch wirksame Env-Variablen. PAPERLESS_OCR_TMPDIR existiert als Name, aber nicht als Funktion in der eingesetzten Version — bevor du eine Variable als Fix einsetzt, verifizier, dass sie tatsächlich etwas bewirkt (Log-Ausgabe prüfen, nicht nur Doku glauben).
  • “tmpfs größer machen” ist kein Fix, wenn der Cgroup-Memory-Limit genauso knapp ist. tmpfs frisst RAM. Ein größeres tmpfs in einem gedeckelten Container verschiebt den Crash-Punkt nur von “tmpfs voll” zu “OOM-Kill” — beide enden mit einem toten Worker.
  • Der eigentliche Temp-Fresser in OCR-Pipelines ist selten das Haupttool. Nicht OCRmyPDF, nicht Tesseract — unpaper als Vorverarbeitungsschritt produziert die großen unkomprimierten Zwischendateien. Bei Speicherproblemen in der OCR-Kette zuerst die Vorverarbeitungsschritte prüfen, nicht die Hauptengine.
  • Verwaiste Temp-Dateien aus gecrashten Jobs räumen sich nicht von selbst auf. 7 GB alte ocrmypdf.io.*-Reste füllten die Host-Disk komplett, unabhängig vom eigentlichen tmpfs-Problem. Ohne Cleanup-Cron sammelt sich das bei jedem weiteren Crash erneut an.
  • Container-Recreate kann manuelle Code-Patches zurücksetzen. Ein unabhängiger bulk=100-Patch in der Mail-Komponente musste nach dem Neustart des Containers erneut angewendet werden — ein Seiteneffekt, der mit dem eigentlichen OCR-Fix nichts zu tun hat, aber bei jedem Recreate wiederkehrt, wenn der Patch nicht im Image selbst liegt.

Checkliste

  • df -h /tmp im Container während eines laufenden OCR-Jobs prüfen — läuft das tmpfs während der Verarbeitung voll?
  • Seitenzahl des Scans gegen tmpfs-Größe abschätzen: ~26 MB/Seite (unpaper-PPM) × Seitenzahl vs. verfügbares tmpfs
  • PAPERLESS_OCR_TMPDIR NICHT als Fix verwenden — stiller No-Op, keine Wirkung
  • TMPDIR auf einen persistenten Pfad (nicht tmpfs) setzen, z. B. /usr/src/paperless/scratch
  • PAPERLESS_OCR_CLEAN=none setzen, wenn Scans ohnehin sauber eingezogen werden — schaltet unpaper ab
  • PAPERLESS_WORKER_TIMEOUT hochsetzen (z. B. 7200), damit große Jobs auf dem persistenten Temp-Pfad nicht ins Timeout laufen
  • Alte ocrmypdf.io.*-Verzeichnisse im Scratch-Pfad per Cron aufräumen (z. B. älter als 24h)
  • Nach jedem Container-Recreate: manuelle Code-Patches (falls vorhanden) erneut prüfen und anwenden