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_TMPDIRexistiert 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 —
unpaperals 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 /tmpim 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_TMPDIRNICHT als Fix verwenden — stiller No-Op, keine Wirkung -
TMPDIRauf einen persistenten Pfad (nicht tmpfs) setzen, z. B./usr/src/paperless/scratch -
PAPERLESS_OCR_CLEAN=nonesetzen, wenn Scans ohnehin sauber eingezogen werden — schaltet unpaper ab -
PAPERLESS_WORKER_TIMEOUThochsetzen (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