Vor dem Rollout durchgesehen und die verbliebenen Stellen geschlossen, an denen etwas schiefgehen konnte, ohne dass es irgendwo sichtbar wurde. Datenverlust: - veraPDF: das in [verapdf].binary konfigurierte Programm wird im Preflight geprueft. Bisher galt bei falschem Pfad JEDE Datei als "nicht konform" — Ergebnis nach error/, Original geloescht (Default delete). run_verapdf() trennt jetzt ausserdem ein echtes FAIL-Urteil von einer Stoerung (VeraPdfUnavailable: nicht startbar, abgestuerzt, kein PASS/FAIL in der Ausgabe). Bei Stoerung wandern Original UND Ergebnis nach error/, das Original wird nicht entsorgt. - Gleichnamige Dateien wurden in outgoing/, error/ und beim Ordner-Upload mit abweichendem target kommentarlos ueberschrieben. Jetzt Zeitstempel daneben, mit Warnung; ProcessResult.output traegt den echten Pfad. Robustheit: - Kaputtes oder nicht lesbares TOML beim Start: Exit 2 statt Traceback. - RestartPreventExitStatus=2 in der Unit — Exit 2 (Config/Preflight) laeuft nicht mehr endlos neu, die Instanz bleibt sichtbar failed stehen. - Toter watchdog-Observer wird erkannt: Exit 3, systemd setzt den Watch neu auf. Vorher blieb die Unit "active" und verarbeitete nichts mehr. - Relative Pfade in [paths]/archive_dir/target sind ein Config-Fehler statt still unter /opt zu landen. - Fehler beim Archivieren entwertet den Durchlauf nicht mehr: Upload und Mail laufen, Sichtbarkeit ueber log.error + "OK mit Warnung"-Mail. - Nicht-PDFs in incoming/ werden beim Start-Scan gesammelt gemeldet. - Logging explizit nach stdout (die Doku versprach das schon). Struktur: - Neue lib/common.sh, von install.sh und update.sh gesourct. Die doppelte venv_is_healthy() gibt es nur noch einmal, in der gruendlichen Fassung — die schlanke in install.sh haette eine nach einem Distro-Sprung kaputte venv als gesund durchgewunken (nachgewiesen). - install.sh warnt in Containern, wenn systemd-journald nicht laeuft. Doku: Dateisystem-Festlegung (ext4/xfs/zfs, kein CIFS/NFS wegen inotify), Debian 13 in LXC auf Proxmox scheitert an journald (243/CREDENTIALS, AppArmor blockiert sd-mkdcreds) inkl. Abhilfe, echte Speicher-Messwerte, Exit-Code-Tabelle. 254 Tests gruen (vorher 152). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
37 KiB
Installation
Erstinstallation und Anlage von Hotfolder-Instanzen mit install.sh.
Verwandte Dokumente: README · Update · Debian-Major-Upgrade
Voraussetzungen
| Punkt | Anforderung |
|---|---|
| Betriebssystem | Debian 12 (bookworm) oder Debian 13 — systemd wird vorausgesetzt |
| Python | 3.11+ (wegen tomllib aus der stdlib); kommt aus der Distribution |
| Arbeitsspeicher | mindestens 2 GB für den produktiven Betrieb — siehe Systemanforderungen |
| Dateisystem | ext4, xfs oder zfs; incoming/ lokal, kein CIFS/NFS — siehe Dateisystem |
| Rechte | root (sudo ./install.sh) |
| Netz | apt-Zugriff für die System-Pakete, PyPI-Zugriff für die venv |
| Repo | muss dauerhaft liegen bleiben — update.sh kopiert daraus (s. UPDATE.md) |
Die System-Pakete installiert der Installer selbst. Die Liste steht als
einzige Quelle in lib/common.sh (Funktion pdf_ocr_apt_packages(), zwischen
den Marken # --- BEGIN apt-packages / # --- END apt-packages); install.sh
sourct die Datei, und update.sh schneidet den Block zusätzlich noch einmal
aus der Repo-Fassung heraus, damit beim Update die neue Liste gilt und
nicht die eventuell ältere Kopie unter /opt/pdf-ocr-hotfolder/lib/:
python3 python3-venv python3-pip
tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng
ghostscript qpdf unpaper pngquant icc-profiles-free
ca-certificates curl
Weitere Tesseract-Sprachpakete installiert der Installer bei Bedarf pro Instanz nach (siehe OCR-Sprachen).
Die Python-Abhängigkeiten stehen fest gepinnt in requirements.txt
(ocrmypdf, watchdog, requests, paramiko). Warum das so ist und wie man die Pins
anhebt: OS-UPGRADE.md.
Systemanforderungen
CPU und Platte sind unkritisch — der Arbeitsspeicher ist es nicht. OCR rastert jede Seite in voller Auflösung ins RAM; der Spitzenbedarf hängt an der Seitengröße, nicht an der Dateigröße der PDF.
Empfehlung: mindestens 2 GB RAM. Für Mehr-Instanz-Betrieb oder
max_workers > 2 entsprechend mehr.
Dateisystem: ext4, xfs oder zfs
Der Dienst wird ausschließlich auf ext4, xfs oder zfs betrieben. Andere Dateisysteme sind nicht vorgesehen und werden nicht getestet.
incoming/ gehört auf ein lokales, inotify-fähiges Dateisystem — kein
CIFS/NFS-Mount. Der Hotfolder hängt vollständig an inotify (watchdog
meldet created, moved, closed). inotify ist ein Mechanismus des lokalen
Kernels: er sieht nur Änderungen, die dieser Kernel selbst ausführt. Schreibt
ein anderer Rechner über SMB oder NFS in ein gemountetes Verzeichnis, geht das
am lokalen VFS vorbei und es entsteht gar kein Event — nicht verzögert,
nicht unzuverlässig, sondern grundsätzlich keines.
Die Folge ist heimtückisch, weil nichts kaputt aussieht: Der Dienst startet,
meldet active (running), arbeitet den Bestand beim Start-Scan sauber ab — und
bemerkt danach keine einzige neue Datei mehr. Es gibt keinen Fehler, keine
Meldung, keine Mail. Erst wenn jemand die Instanz neu startet, wird der
inzwischen angesammelte Stapel auf einmal verarbeitet.
Richtiger Aufbau: der Scanner schreibt über SMB/NFS auf den Rechner, auf dem
der Dienst läuft, und incoming/ liegt dort auf der lokalen Platte. Der
Netz-Export zeigt auf dieses lokale Verzeichnis, nicht umgekehrt. Für die
Ausgabe gilt die Einschränkung nicht — outgoing/ und
[upload.folder].target dürfen auf einem Netz-Share liegen, dorthin wird nur
geschrieben.
Was 2 GB tatsächlich tragen
Gemessen auf einem LXC-Container mit 2 GB RAM, Debian 13 — eine
A4-Seite in 300 dpi mit deskew = true, oversample = 300, jobs = 4:
| Messwert | Ergebnis |
|---|---|
| Laufzeit | 19 s |
MemoryPeak des Dienstes (systemctl show -p MemoryPeak) |
380 MB |
memory.peak des ganzen Containers |
503 MB |
oom_kill in /sys/fs/cgroup/memory.events |
0 |
Dieselbe Seite auf derselben Maschine mit 512 MB war genau der OOM-Kill
unten. Der Spitzenbedarf liegt also bei rund einem halben Gigabyte für
eine Seite bei einem Worker — 2 GB lassen damit Luft für den
Default max_workers = 2, für das Betriebssystem und für einen zweiten
Hotfolder, sind aber keine üppige Reserve.
Warum 512 MB nachweislich nicht reichen
Gemessen auf einem LXC-Container mit 512 MB RAM + 512 MB Swap, Debian 13, Ghostscript 10.05.1:
| Vorgang | Ergebnis |
|---|---|
eine einzelne A4-Seite in 300 dpi, deskew = true |
cgroup-Limit gerissen, Dienst vom OOM-Killer beendet |
| dieselbe Verarbeitung mit einer kleineren Seite (850 × 1100 px) | läuft sauber durch, ca. 19 s |
Beleg aus dem dmesg des LXC-Hosts:
oom-kill:constraint=CONSTRAINT_MEMCG, oom_memcg=/lxc/201, task=python
total-vm:1168764kB, anon-rss:489676kB
Eine Seite, ein Worker — und schon knapp 500 MB anonymer Speicher. 512 MB sind damit für 300-dpi-Scans keine knappe, sondern eine unzureichende Dimensionierung.
RAM ↔ max_workers × jobs × Auflösung
Der Spitzenbedarf multipliziert sich über drei Config-Werte aus
[ocr]:
| Key | Default | Wirkung auf den Speicher |
|---|---|---|
max_workers |
2 |
so viele PDFs gleichzeitig — jede mit eigenem Seitenpuffer. Der direkte Multiplikator |
jobs |
4 |
Threads innerhalb einer PDF; mehrere Seiten gleichzeitig im Speicher |
oversample |
300 |
Auflösung gerasterter Seiten — der Bedarf wächst quadratisch mit der dpi |
Der Default max_workers = 2 erlaubt also, dass zwei solcher Seiten
parallel verarbeitet werden. Wer knapp dimensioniert, zieht zuerst
max_workers auf 1 herunter, danach jobs. oversample unter 300 zu
drücken, spart zwar Speicher, kostet aber Erkennungsqualität — das ist der
letzte Hebel, nicht der erste.
Wie sich ein OOM-Kill äußert
Von außen sieht ein OOM-Kill wie ein Anwendungsfehler aus — er ist keiner:
- Der Dienst ist weg bzw. wurde von systemd neu gestartet
(
Restart=on-failure);systemctl show -p NRestartssteigt. - Im journal bricht die Verarbeitung mitten in der Datei ab, ohne
Python-Traceback und ohne
ERROR-Zeile aus dem Tool. - Die betroffene PDF bleibt in
working/liegen.
Wie man ihn nachweist
Im Container:
cat /sys/fs/cgroup/memory.events
# oom_kill 1 <- alles über 0 ist ein Treffer
Auf dem LXC-Host (im Container zeigt dmesg diese Zeilen nicht):
dmesg -T | grep -i oom-kill
Diese Prüfung gehört an den Anfang der Fehlersuche, wenn Dateien
unerklärlich in working/ liegen bleiben: ohne sie sucht man den Fehler in
ocrmypdf, Tesseract oder der Config, wo keiner ist.
Datenverlust ist abgefangen, der OOM bleibt
Seit v0.6.0 greift der Dienst beim nächsten Start auf, was in working/
liegen geblieben ist (siehe UPDATE.md).
Eine vom OOM-Killer unterbrochene Datei geht also nicht verloren. Behoben ist
damit aber nur die Folge: bei unveränderter Dimensionierung läuft dieselbe Datei
nach dem Neustart erneut in denselben OOM — bis max_workers sinkt oder das
System mehr RAM bekommt.
Installation
git clone gitea@gitea.sonith.de:sonith_ug/pdf-ocr-hotfolder.git
cd pdf-ocr-hotfolder
sudo ./install.sh
install.sh ist Installer und Instanz-Manager in einem und idempotent —
jeder weitere Aufruf überspringt, was schon steht.
Basis-Install vs. Instanz-Anlage
Der Installer unterscheidet zwei Ebenen:
| Ebene | Wann | Was passiert |
|---|---|---|
| Basis-Install | einmalig; erkannt an venv + Template-Unit |
System-Pakete, Ghostscript-Check, Container-Erkennung (inkl. journald-Prüfung), Default-User pdfocr, Code und lib/ nach /opt/pdf-ocr-hotfolder/, venv, systemd-Template-Unit |
| Instanz-Anlage | bei jedem Lauf, beliebig oft | Abfragen pro Instanz, Datenverzeichnisse, <instanz>.toml, optionales User-Drop-in, enable --now |
Ist die Basis-Installation vorhanden, aber die venv passt nicht mehr zum
System-Python (typisch nach einem Distributions-Upgrade), läuft der Basis-Install
zur Reparatur erneut: die alte venv wird nach venv.old-<timestamp>
weggesichert und neu gebaut. Für den geplanten Weg über ein Debian-Major-Upgrade
ist aber update.sh --rebuild-venv gedacht, siehe OS-UPGRADE.md.
Beim Erstlauf ist mindestens eine Instanz Pflicht. Danach fragt der
Installer in der Schleife Weitere Instanz anlegen? [j/N]:.
Die Abfragen pro Instanz
create_instance() stellt fünf Fragen, in der Reihenfolge der folgenden
Abschnitte: Instanz-Name, Basis-Pfad, Service-User, OCR-Sprachen,
Original-Behandlung. Alle Antworten gelten nur für diese Instanz — nichts
davon ist global.
Instanz-Name
Instanz-Name (nur a-z, 0-9, -):
Muster ^[a-z0-9][a-z0-9-]*$. Der Name wird zum Unit-Suffix
(pdf-ocr-hotfolder@<name>.service) und zum Config-Dateinamen
(/etc/pdf-ocr-hotfolder/<name>.toml). Existiert die Config schon, bricht die
Anlage ab — ein versehentliches Überschreiben gibt es nicht.
Basis-Pfad für die Daten
Basis-Pfad für Daten [/var/lib/pdf-ocr-hotfolder/<name>]:
Darunter entstehen incoming/, outgoing/, working/, error/ und werden auf
den Service-User gechownt.
Service-User
Service-User [pdfocr]:
- Existiert der User (lokal oder als AD-User via SSSD/Winbind), wird er
übernommen; die primäre Gruppe ermittelt der Installer per
id -gn. - Existiert er nicht, bietet der Installer an, ihn lokal als System-User anzulegen. Wird das abgelehnt, bricht die Instanz-Anlage ab — der User muss dann erst über AD/SSSD bereitstehen.
- Ist der gewählte User nicht
pdfocr, legt der Installer das Drop-in/etc/systemd/system/pdf-ocr-hotfolder@<instanz>.service.d/user.confmitUser=/Group=an.
Bei AD-Usern mit lokaler UID werden die Datei-Berechtigungen über die UID gesetzt — das läuft transparent.
OCR-Sprachen
Tesseract-Sprachen [deu+eng]:
Die Sprachen werden bewusst je Instanz abgefragt: ein Hotfolder
buchhaltung sieht nur deutsche Belege, ein Hotfolder export
internationale Korrespondenz.
# /etc/pdf-ocr-hotfolder/buchhaltung.toml
languages = "deu"
# /etc/pdf-ocr-hotfolder/export.toml
languages = "deu+eng+fra"
Die Liste so eng wie möglich halten. Jede zusätzliche Sprache kostet
Laufzeit und Erkennungsqualität: Tesseract wägt mehr Modelle gegeneinander ab
und verwechselt dabei Wörter, die in einer Sprache eindeutig wären.
deu+eng+fra auf reinen Deutsch-Scans ist kein Sicherheitsnetz, sondern ein
Rückschritt.
Was der Installer damit macht:
- Format prüfen — Sprachcodes mit
+verbunden (^[a-z]{3}(_[A-Za-z]+)?(\+…)*$), alsodeu,deu+eng,chi_sim+eng. Bei Unsinn wird erneut gefragt, nicht abgebrochen. - Jeden Code gegen
tesseract --list-langsprüfen. Fehlt eine Sprachdatei, bietet er das passende apt-Paket an:tesseract-ocr-<code>, Unterstrich wird zum Bindestrich (chi_sim→tesseract-ocr-chi-sim). - Lehnt man ab oder scheitert die Installation, warnt er, dass OCR mit dieser Sprache bei jeder Datei scheitern würde, und fragt die Sprachen erneut ab — die fehlende Sprache kann man dann einfach weglassen.
- Ist
tesseractgar nicht aufrufbar, wird die Prüfung übersprungen und die Eingabe unverändert übernommen.
Original archivieren?
Original nach erfolgreichem OCR archivieren? [j/N]:
- Nein (Default) →
original_on_success = "delete", das Original wird nach erfolgreichem OCR gelöscht. - Ja →
original_on_success = "archive", danach:
Archiv-Verzeichnis [<basis>/archive]:
Der Pfad muss absolut sein und darf nicht incoming/, outgoing/,
working/ oder error/ der Instanz sein — im Eingang würde das Original sonst
endlos neu aufgegriffen, in den übrigen kollidiert es mit der Verarbeitung. Das
Verzeichnis wird angelegt und auf den Service-User gechownt; innerhalb des
Basis-Pfads erledigt das bestehende chown -R das mit, ein Archiv außerhalb
bekommt ein eigenes.
Was danach passiert
Die Instanz-Config entsteht per sed aus config.example.toml. Substituiert
werden die vier [paths]-Zeilen sowie [ocr].languages,
[output].original_on_success und [output].archive_dir. Anschließend liest
der Installer diese drei Keys aus der erzeugten Datei zurück und vergleicht sie
mit der Eingabe; erst wenn das passt, nennt die Zusammenfassung Sprachen und
Archiv-Verzeichnis. Sonst gibt es eine Warnung mit der Bitte, die Config von
Hand nachzuziehen.
Die Config bekommt chmod 640 und chown root:<service-gruppe>, das
Verzeichnis /etc/pdf-ocr-hotfolder selbst 750 root:pdfocr — in den
Instanz-Configs stehen Klartext-Passwörter für SMTP, Nextcloud und SFTP.
Zum Schluss: daemon-reload und systemctl enable --now pdf-ocr-hotfolder@<instanz>.service.
Test
cp irgendein-scan.pdf /var/lib/pdf-ocr-hotfolder/<instanz>/incoming/
journalctl -u pdf-ocr-hotfolder@<instanz> -f
Nach wenigen Sekunden liegt das OCR-PDF im outgoing/ der Instanz.
Multi-Instanz-Betrieb
Das Tool arbeitet komplett instanzbasiert über die systemd-Template-Unit
pdf-ocr-hotfolder@<name>.service. Jede Instanz hat eigene Config, eigene
Datenverzeichnisse, eigene Unit, optional eigenen Service-User — und eigene
OCR-Sprachen und Original-Behandlung.
sudo ./install.sh # legt z.B. kunde-a, kunde-b, buchhaltung an
systemctl status 'pdf-ocr-hotfolder@*'
journalctl -u pdf-ocr-hotfolder@kunde-a -f
Der Code unter /opt/pdf-ocr-hotfolder/ (inkl. venv) ist für alle Instanzen
gemeinsam. Ein Update trifft damit immer alle Instanzen auf einmal — siehe
UPDATE.md.
Das vollständige Verzeichnis-Layout steht im README.
LXC/Container: Error 226/NAMESPACE
In LXC-Containern schlagen die systemd-Hardening-Optionen der Unit
(PrivateTmp, ProtectSystem, ProtectKernelTunables, …) fehl; systemd
quittiert das mit Error 226/NAMESPACE.
Der Installer erkennt Container über systemd-detect-virt --container und
bietet das Drop-in automatisch an. Manuell:
sudo mkdir -p /etc/systemd/system/pdf-ocr-hotfolder@.service.d/
sudo cp /opt/pdf-ocr-hotfolder/systemd/lxc-compat.conf \
/etc/systemd/system/pdf-ocr-hotfolder@.service.d/
sudo systemctl daemon-reload
sudo systemctl restart 'pdf-ocr-hotfolder@*'
Das Drop-in setzt alle betroffenen Hardening-Schalter auf false. Es liegt auf
Template-Ebene (pdf-ocr-hotfolder@.service.d/) und gilt damit für alle
Instanzen. Ist es installiert, zieht update.sh es bei jedem Update aus dem
Repo nach, damit ein neu ergänzter Hardening-Schalter nicht alle
Container-Instanzen reißt.
Ghostscript-Bug auf Debian 12
Ghostscript 10.0.0 bis einschließlich 10.02.0 — der Debian-12-Default — enthält Regressionen, die PDFs mit vorhandenem Text beschädigen. ocrmypdf verweigert deshalb den Dienst, statt ein kaputtes Ergebnis zu liefern.
Wann genau ocrmypdf abbricht
Die Bedingung steht in ocrmypdf/builtin_plugins/ghostscript.py
(check_options()) und hängt an zwei Dingen — an der Config und an der
ocrmypdf-Version:
| ocrmypdf | Die Prüfung greift bei | Heißt für uns |
|---|---|---|
| ≤ 16.x | skip_text oder redo_ocr — unabhängig vom output_type |
Auch ohne PDF/A scheitert jede Datei, denn skip_text = true ist unser Default |
| ≥ 17.0 | dasselbe, aber nur innerhalb von if options.output_type.startswith('pdfa') |
Ohne PDF/A wird Ghostscript gar nicht angefasst — unkritisch |
⚠️
pdfa_level = ""allein ist damit kein Schutz. Die Entwarnung gilt nur zusammen mit ocrmypdf ≥ 17. Das war der Fehler in 0.6.0: der Pin stand aufocrmypdf==16.13.0, und auf Debian 12 landete daraufhin jede PDF inerror/— bei grünemsystemctl statusund „Preflight ok".requirements.txtpinnt deshalb 17.x.
Was das Tool dagegen tut
pdfa_level = ""ist der Default (kein PDF/A-Output).requirements.txtpinnt ocrmypdf 17.x. Ein Downgrade auf 16.x macht jede Debian-12-Instanz unbrauchbar;update.shweist Versionssprünge der gepinnten Pakete deshalb ausdrücklich aus.- Der Preflight bricht beim Dienststart mit Exit 2 ab, wenn die
Ghostscript-Version betroffen ist und die Kombination aus
skip_text,pdfa_levelund installierter ocrmypdf-Version tatsächlich zum Abbruch führen würde. Der Dienst startet dann gar nicht erst, statt jede Datei einzeln scheitern zu lassen. --check-configmeldet denselben Zustand als Fehler (Exit 2) und zeigt ocrmypdf- und Ghostscript-Version an (siehe UPDATE.md).- Der Rauchtest in
update.shschiebt nach dem Update eine Test-PDF durch die echte Pipeline — er hätte den Ausfall sofort gezeigt.
Abhilfe
Weg 1 — Ghostscript anheben (empfohlen). Der Installer erkennt betroffene Versionen und bietet auf Debian 12 bookworm-backports an. Manuell:
echo 'deb http://deb.debian.org/debian bookworm-backports main' | \
sudo tee /etc/apt/sources.list.d/bookworm-backports.list
sudo apt update && sudo apt install -t bookworm-backports ghostscript
Ab Ghostscript 10.02.1 ist alles in Ordnung; PDF/A kann dann eingeschaltet werden.
Weg 2 — skip_text = false setzen. Dann wird vorhandener Text neu erkannt
statt übersprungen, und die Bedingung greift nicht mehr. Das kostet Laufzeit bei
PDFs, die bereits eine Textebene haben.
Instanz manuell löschen
Der Installer legt Instanzen an, löscht aber keine. Von Hand:
sudo systemctl disable --now pdf-ocr-hotfolder@<name>
sudo rm /etc/pdf-ocr-hotfolder/<name>.toml
sudo rm -rf /etc/systemd/system/pdf-ocr-hotfolder@<name>.service.d
sudo systemctl daemon-reload
# Datenverzeichnis /var/lib/pdf-ocr-hotfolder/<name> manuell aufräumen
Das Datenverzeichnis bleibt bewusst liegen: dort können noch unverarbeitete
PDFs in incoming/, Fehlerfälle in error/ oder Originale im Archiv liegen.
Erst hineinsehen, dann löschen.
Solange die Config unter /etc/pdf-ocr-hotfolder/ liegt, zählt update.sh die
Instanz weiter mit — auch wenn sie gestoppt ist.
Konfigurationsreferenz
Vollständiges, kommentiertes Beispiel: config.example.toml.
Jede Instanz hat ihre eigene Kopie unter /etc/pdf-ocr-hotfolder/<instanz>.toml.
Unbekannte Keys werden beim Laden ignoriert, aber gemeldet — beim
Dienststart im Log und von --check-config. Ein Tippfehler wie
[ocr].langauges fällt damit auf.
[paths] — Pflicht
| Key | Bedeutung |
|---|---|
incoming |
Eingang, hier schreibt der Scanner hinein |
outgoing |
Ausgang, fertige OCR-PDFs |
working |
Arbeitsverzeichnis während der Verarbeitung |
error |
fehlgeschlagene PDFs |
Fehlt die Sektion oder einer der vier Einträge, gibt es eine deutsche
ConfigError-Meldung mit Datei- und Key-Nennung und Exit 2 — der Dienst
startet nicht.
Alle vier Pfade müssen absolut sein. Ein relativer Pfad wird gegen das
Arbeitsverzeichnis des Prozesses aufgelöst, bei der Unit also gegen
WorkingDirectory=/opt/pdf-ocr-hotfolder — nicht gegen das Verzeichnis, in
dem die Config liegt. incoming = "in" legte damit still
/opt/pdf-ocr-hotfolder/in an: der Scanner schreibt woanders hin als der
Dienst schaut, und niemand sieht einen Fehler. Seit 0.7.0 ist das ein
Config-Fehler mit Exit 2. Dieselbe Regel gilt für
[output].archive_dir und
[upload.folder].target;
install.sh erzeugt ohnehin nur absolute Pfade.
incoming muss außerdem auf einem lokalen Dateisystem liegen — siehe
Dateisystem.
[ocr]
| Key | Default | Bedeutung |
|---|---|---|
languages |
"deu+eng" |
Tesseract-Sprachen; der Installer fragt sie pro Instanz ab |
jobs |
4 |
Threads, die ocrmypdf innerhalb einer PDF nutzt |
skip_text |
true |
Seiten, die schon Text haben, nicht neu OCRen |
oversample |
300 |
Auflösung für gerasterte Seiten |
pdfa_level |
"" |
"1", "2", "3" oder leer für reines PDF — leer wegen des Ghostscript-Bugs. Achtung: leer allein schützt nur zusammen mit ocrmypdf ≥ 17 |
deskew |
true |
schiefe Scans begradigen |
clean |
false |
Hintergrund säubern (unpaper) |
max_workers |
2 |
wie viele PDFs parallel verarbeitet werden |
timeout |
300 |
max. Sekunden, die Tesseract pro Seite laufen darf; 0 = kein eigenes Limit |
timeout ist ein Seiten-Timeout, kein Gesamt-Timeout. Der Wert geht als
tesseract_timeout an ocrmypdf; ein Dokument-Timeout kennt ocrmypdf nicht. Wer
noch den alten Default 1800 aus einer Config vor 0.4.0 stehen hat, gibt
Tesseract 30 Minuten je Seite — Richtwert ist 300. Läuft eine Seite in den
Timeout, landet sie ohne Textebene im Ergebnis, die übrigen Seiten laufen
weiter. Ein durchgereichtes 0 würde ocrmypdf dazu bringen, OCR still zu
überspringen, deshalb wird 0 (oder negativ) gar nicht erst übergeben und der
ocrmypdf-Default greift. Siehe auch
Config-Drift.
[output]
| Key | Default | Bedeutung |
|---|---|---|
name_mode |
"prefix" |
prefix → OCR_scan.pdf, suffix → scan_OCR.pdf (vor der Extension), none → unverändert |
name_tag |
"OCR_" |
verbatim eingefügter String; leer wirkt wie none |
original_on_success |
"delete" |
delete oder archive — Installer fragt das ab |
archive_dir |
"" |
absoluter Pfad (relativ wird abgelehnt), Pflicht bei archive; Namenskollision → Zeitstempel-Suffix |
Ein Tippfehler in name_mode oder original_on_success führt beim Start zum
Abbruch mit Exit 2, nicht erst bei der ersten Datei.
Kollisionen überschreiben nichts. Liegt im Ziel bereits eine Datei
desselben Namens, wird die neue mit Zeitstempel danebengelegt
(scan.pdf → scan_20260923-081500.pdf; bei zwei Dateien innerhalb derselben
Sekunde zusätzlich mit Zähler), und es gibt eine Warnung im Journal. Das gilt
seit 0.7.0 einheitlich für outgoing/, das Archiv, error/ und den
Ordner-Upload — vorher ersetzte der zweite Durchlauf das Ergebnis des ersten
kommentarlos. Die E-Mail-Benachrichtigung und die Upload-Ziele nennen den
tatsächlich geschriebenen Namen.
Scheitert das Entsorgen des Originals (Platte voll, Verzeichnis
read-only), gilt der Durchlauf trotzdem als Erfolg: das fertige PDF liegt
bereits in outgoing/ und wird normal ausgeliefert. Es gibt aber eine
ERROR-Zeile im Journal, und die Mail geht als „OK mit Warnung" raus —
auch bei [notify.email].on = "errors". Das Original bleibt dann in
working/ liegen und wird beim nächsten Start erneut durch das OCR
geschickt; es gehört von Hand aufgeräumt und die Ursache behoben.
[verapdf]
| Key | Default | Bedeutung |
|---|---|---|
enabled |
false |
PDF/A-Validierung per veraPDF-CLI |
binary |
/opt/verapdf/verapdf |
Pfad zum veraPDF-Binary, oder ein nackter Name, der im PATH gesucht wird |
flavour |
"1b" |
PDF/A-Flavour |
veraPDF startet eine JVM und ist entsprechend teuer — nur einschalten, wenn die
Validierung wirklich gebraucht wird. Bei FAIL wandert das OCR-Ergebnis nach
error/; das Original folgt original_on_success (bei archive bleibt es also
erhalten).
Mit enabled = true wird binary im Preflight geprüft (seit 0.7.0). Zeigt
der Pfad nicht auf ein vorhandenes, ausführbares Programm, startet der Dienst
gar nicht erst (Exit 2), und --check-config meldet denselben Fehler.
--check-config zeigt Binary und Flavour außerdem in der Übersicht an.
⚠️ Warum das eine harte Sperre ist. Bis 0.6.3 war ein Tippfehler in
binaryder gefährlichste Fehler des ganzen Dienstes:run_verapdf()fand das Programm für jede Datei nicht, wertete das als „nicht konform", schob das OCR-Ergebnis nacherror/— und entsorgte das Original lautoriginal_on_success, beim Defaultdeletealso die Vorlage. Scan für Scan verschwanden so die Originale, während die Unit alsactive (running)dastand.
Störung ist kein FAIL. Lässt sich veraPDF im laufenden Betrieb nicht mehr
befragen — Programm verschwunden, JVM startet nicht, Timeout (300 s pro Datei),
oder die Ausgabe enthält weder PASS noch FAIL —, ist das kein Urteil über
die PDF. In diesem Fall wandern Original und OCR-Ergebnis nach error/,
und das Original wird weder gelöscht noch archiviert, unabhängig von
original_on_success. Im Journal steht die Ursache samt Hinweis auf
--check-config.
[upload.folder] / [upload.nextcloud] / [upload.sftp]
Beliebig viele Ziele gleichzeitig aktivierbar. Sind alle aus, bleibt das fertige
PDF einfach in outgoing/ liegen.
| Sektion | Keys |
|---|---|
[upload.folder] |
enabled, target — leer heißt [paths].outgoing, dann No-op; sonst absoluter Pfad (relativ wird abgelehnt, Exit 2) |
[upload.nextcloud] |
enabled, url, username, password, remote_path, verify_ssl |
[upload.sftp] |
enabled, host, port, username, key_file, password, remote_path |
Schlägt mindestens ein Ziel fehl, zählt das als Fehler und die Mail geht als
FEHLER raus — das PDF bleibt aber bewusst in outgoing/ liegen, das OCR
selbst war ja erfolgreich.
Liegt im target von [upload.folder] schon eine gleichnamige Datei, wird sie
seit 0.7.0 nicht mehr ersetzt, sondern die Kopie mit Zeitstempel
danebengelegt (samt Warnung) — dieselbe Regel wie in [output].
[notify.email]
| Key | Default | Bedeutung |
|---|---|---|
enabled |
false |
E-Mail-Benachrichtigung an/aus |
smtp_host, smtp_port, smtp_user, smtp_password, use_starttls |
— | SMTP-Zugang |
from_addr |
— | Absender |
to_addrs |
[] |
Empfängerliste |
on |
"errors" |
always | errors | never |
[logging]
| Key | Default | Bedeutung |
|---|---|---|
level |
"INFO" |
DEBUG | INFO | WARNING | ERROR |
Der Dienst schreibt kein eigenes Logfile — alles geht nach stdout und damit ins journal.
Exit-Codes
Der Dienst und die CLI benutzen vier Codes. systemctl status und
journalctl zeigen sie als status=<n>.
| Exit | Bedeutung | Startet systemd neu? | Was zu tun ist |
|---|---|---|---|
| 0 | regulärer Stopp (SIGTERM/SIGINT); bei --once: alles verarbeitet, auch „nichts da"; bei --check-config: Config sauber |
— | nichts |
| 1 | nur im Einmal-Betrieb: mindestens eine PDF ist fehlgeschlagen. Bei --check-config: Config nutzbar, aber mit Warnungen |
— | error/ ansehen bzw. Warnungen nachziehen |
| 2 | Config- oder Preflight-Fehler — kaputtes/unlesbares TOML, fehlender Pflicht-Key, relativer Pfad, ungültige [output]-Werte, fehlendes tesseract/gs, betroffene Ghostscript-Version, nicht aufrufbares veraPDF |
nein — RestartPreventExitStatus=2 in der Unit |
Config korrigieren, mit --check-config gegenprüfen, dann systemctl start |
| 3 | Der Verzeichnis-Watch ist gestorben — es würden keine neuen Dateien mehr erkannt | ja, und genau darum geht es | meist nichts; häuft es sich, fs.inotify.max_user_watches und den Mount von incoming/ prüfen |
Zu Exit 2: Ein Neustart heilt einen Config-Fehler nicht. Ohne
RestartPreventExitStatus=2 startete Restart=on-failure die Instanz endlos
im 5-Sekunden-Takt neu (das Start-Rate-Limit greift bei RestartSec=5 nie).
Seit 0.7.0 bleibt sie stattdessen sichtbar failed stehen — das ist gewollt
und soll beim Nachsehen auffallen.
Zu Exit 3: Stirbt der watchdog-Observer im Betrieb (erschöpftes
fs.inotify.max_user_watches, ersetztes oder neu gemountetes Verzeichnis),
blieb die Unit früher active (running) und verarbeitete stumm nichts mehr —
für einen Hotfolder der schlechteste denkbare Zustand. Der Dienst prüft den
Observer jetzt sekündlich mit, loggt eine ERROR-Zeile mit den möglichen
Ursachen und beendet sich mit 3, damit systemd ihn neu startet und der Watch
neu aufgesetzt wird. Ein einzelnes Vorkommnis ist damit selbstheilend; ein
steigendes systemctl show -p NRestarts ist der Hinweis, dass man nachsehen
sollte.
Troubleshooting
Tesseract findet die Sprache nicht
sudo apt install tesseract-ocr-deu tesseract-ocr-eng
Danach [ocr].languages prüfen. Der Installer nimmt einem das beim Anlegen
einer Instanz ab, ein nachträglich in die Config geschriebener Sprachcode wird
aber nicht geprüft — --check-config zeigt die eingestellten Sprachen an.
"PriorOcrFoundError"
ocrmypdf erkennt bereits vorhandenen OCR-Text. skip_text = true in der Config
setzen (Default).
Berechtigungsprobleme bei AD-User
Der Service-User braucht rw auf alle vier Verzeichnisse der Instanz (und auf das Archiv, falls konfiguriert):
sudo chown -R DOMAIN\\scanuser:DOMAIN\\scangroup /var/lib/pdf-ocr-hotfolder/<instanz>
veraPDF: Dienst startet nicht / Dateien landen in error/
Seit 0.7.0 prüft der Preflight [verapdf].binary. Startet die Instanz mit
Exit 2 nicht mehr, nachdem vorher „alles lief", ist das die gute Nachricht:
der Pfad war schon vorher falsch, nur hat es bisher niemand gemerkt. Vorher
wurde jede PDF als ungültig gewertet und das Original laut
original_on_success entsorgt.
ls -l /opt/verapdf/verapdf # vorhanden? ausführbar (chmod +x)?
Korrigieren — oder, wenn die Validierung nicht zwingend gebraucht wird,
[verapdf].enabled = false setzen. Landen Original und OCR-Ergebnis
gemeinsam in error/, war veraPDF im laufenden Betrieb nicht mehr
ansprechbar; das Original ist dann unangetastet, siehe
[verapdf].
Dienst startet nicht (Exit 2)
Exit 2 heißt immer: Config oder Preflight — siehe Exit-Codes.
Die Instanz bleibt bewusst failed stehen und wird nicht neu gestartet.
Die Ursache steht im journal und ausführlicher in:
cd /opt/pdf-ocr-hotfolder && sudo ./venv/bin/python -m pdf_ocr_hotfolder \
--check-config --config /etc/pdf-ocr-hotfolder/<instanz>.toml
Typische Fälle: kaputtes TOML (die Meldung nennt Zeile und Spalte, sofern der
Interpreter sie liefert), ein relativer Pfad in [paths],
[output].archive_dir oder [upload.folder].target, ein leeres archive_dir
bei original_on_success = "archive", ein nicht aufrufbares veraPDF oder eine
betroffene Ghostscript-Version.
Dienst läuft, verarbeitet aber nichts mehr
systemctl status sagt active (running), in incoming/ stapeln sich die
PDFs, im Journal passiert nichts. Drei Ursachen, in dieser Reihenfolge prüfen:
incoming/liegt auf einem CIFS/NFS-Mount. Dann liefert inotify grundsätzlich keine Events — der Dienst verarbeitet nur noch beim Start.findmnt -T /var/lib/pdf-ocr-hotfolder/<instanz>/incomingzeigt den Typ; Hintergrund und richtiger Aufbau unter Dateisystem.- Der Verzeichnis-Watch ist gestorben. Seit 0.7.0 fällt das auf: der
Dienst beendet sich mit Exit 3 und systemd startet ihn neu.
Im Journal steht die
ERROR-Zeile,systemctl show -p NRestartssteigt. Häuft sich das, ist meist das inotify-Limit erschöpft:cat /proc/sys/fs/inotify/max_user_watches - Es sind gar keine PDFs. Dateien ohne
.pdf-Endung werden ignoriert. Beim Start-Scan meldet der Dienst sie seit 0.7.0 als Sammelzeile mit Anzahl und bis zu drei Beispielnamen:journalctl -u pdf-ocr-hotfolder@<instanz> | grep 'ohne .pdf-Endung'
Dienst startet nicht (203/EXEC)
Der Interpreter der venv ist weg — fast immer nach einem Distributions-Upgrade. Siehe OS-UPGRADE.md.
Keine Logs: "No journal files were found"
Symptom: journalctl -u pdf-ocr-hotfolder@<instanz> bleibt leer oder meldet
No journal files were found. — auch dann, wenn der Dienst nachweislich läuft
und Dateien verarbeitet.
Erste Prüfung:
systemctl status systemd-journald
Ist systemd-journald.service selbst failed (beobachtet mit
status=243/CREDENTIALS), gibt es schlicht kein journal, in das geschrieben
werden könnte.
Einordnung. Der Dienst loggt seit v0.4.1 ausschließlich nach journald — es gibt bewusst kein eigenes Logfile und kein Logverzeichnis. Ein kaputtes journald ist damit ein blinder Fleck: jede Fehlersuche läuft ins Leere, und der Ausfall sieht aus wie ein stummer Dienst. Deshalb vor jeder Fehlersuche zuerst journald prüfen, nicht erst, wenn nichts anderes mehr passt.
Die häufigste Ursache ist kein Schaden an dieser einen Maschine, sondern systematisch: Debian 13 in einer LXC auf Proxmox — siehe den nächsten Abschnitt. Auf Debian 12 tritt sie nicht auf.
install.sh warnt beim Erstinstall in einem Container von sich aus, wenn
systemd-journald nicht läuft, nennt den Drop-in-Befehl und fragt, ob
fortgefahren werden soll.
Notbehelf, solange journald nicht zu retten ist: die Instanz einmal im Vordergrund laufen lassen — dann geht die Ausgabe direkt ins Terminal, am journal vorbei.
sudo systemctl stop pdf-ocr-hotfolder@<instanz>
cd /opt/pdf-ocr-hotfolder && sudo -u pdfocr ./venv/bin/python -m pdf_ocr_hotfolder \
--config /etc/pdf-ocr-hotfolder/<instanz>.toml
Das cd ist zwingend: das Paket wird nicht pip-installiert, sondern nach
/opt/pdf-ocr-hotfolder kopiert und nur über das Arbeitsverzeichnis gefunden
(ohne cd gibt es No module named pdf_ocr_hotfolder, v0.6.2). Beenden mit
Strg+C, danach sudo systemctl start pdf-ocr-hotfolder@<instanz>. Wer nur den
Bestand abarbeiten und dann aussteigen will, hängt --once an (siehe
Manueller Lauf).
Debian 13 in LXC auf Proxmox: journald scheitert (243/CREDENTIALS)
Verifizierter Befund. Er betrifft jede Debian-13-LXC auf Proxmox 8.4, nicht nur eine einzelne Maschine — und er trifft nicht nur dieses Tool, sondern alles, was auf dem Container-journal aufsetzt.
Symptom im Container:
systemctl status systemd-journald
# ● systemd-journald.service - Journal Service
# Active: failed
# Process: ... (code=exited, status=243/CREDENTIALS)
# Main PID exited, status=243/CREDENTIALS
journalctl -u pdf-ocr-hotfolder@<instanz>
# No journal files were found.
Ursache. Ab systemd 255 (Debian 13 liefert 257) setzt die
journald-Unit ImportCredential=journal.*. Zum Einlesen dieser Credentials
startet systemd den Hilfsprozess (sd-mkdcreds), und der mountet dafür.
Genau diesen Mount verbietet das AppArmor-Profil des Proxmox-Hosts. Im Log des
Hosts steht dazu:
apparmor="DENIED" operation="mount" profile="lxc-<id>_</var/lib/lxc>" name="/dev/" comm="(sd-mkdcreds)"
Debian 12 hat systemd 252, kennt ImportCredential in dieser Unit nicht und
ist deshalb nicht betroffen. Das Upgrade 12 → 13 ist damit der Auslöser,
nicht der Container an sich.
Dieselbe Ursache legt weitere Units lahm — beobachtet bei
systemd-logind, systemd-networkd, console-getty und
systemd-tmpfiles-setup. Wer nur journald repariert, hat die übrigen noch vor
sich; ein Blick auf systemctl --failed lohnt sich.
Abhilfe im Container (reboot-fest verifiziert) — ImportCredential wird
per Drop-in auf leer gesetzt und damit abgeschaltet:
sudo mkdir -p /etc/systemd/system/systemd-journald.service.d
printf '[Service]\nImportCredential=\n' | \
sudo tee /etc/systemd/system/systemd-journald.service.d/no-credentials.conf
sudo systemctl daemon-reload
sudo systemctl restart systemd-journald
sudo journalctl --flush
Danach systemctl status systemd-journald (muss active (running) sein) und
journalctl -u pdf-ocr-hotfolder@<instanz> gegenprüfen. Für die anderen
betroffenen Units gilt dasselbe Muster mit deren Unit-Namen.
Der saubere Weg liegt host-seitig. Das Drop-in kuriert das Symptom im Container. Richtig behoben wird es auf dem Proxmox-Host: Update von
pve-container/lxc-pveauf eine Fassung mit passenden AppArmor-Regeln — oder, als grobes Mittel,lxc.apparmor.profile: unconfinedin der Container-Config, was die AppArmor-Isolation dieses Containers allerdings komplett aufgibt.
Dienst bricht mitten in der Verarbeitung weg
Datei bleibt in working/, kein Traceback, NRestarts steigt: das ist fast
immer der OOM-Killer, kein Anwendungsfehler. Nachweis und Dimensionierung unter
Systemanforderungen.
Manueller Lauf (One-Shot)
Bestehende PDFs einer Instanz einmalig verarbeiten und beenden — greift auch
Dateien auf, die in working/ liegen geblieben sind:
cd /opt/pdf-ocr-hotfolder && sudo -u pdfocr ./venv/bin/python -m pdf_ocr_hotfolder \
--config /etc/pdf-ocr-hotfolder/kunde-a.toml --once
Exit-Code: 0 = alles verarbeitet (auch "nichts da"), 1 = mindestens eine
Datei fehlgeschlagen, 2 = Config- oder Preflight-Fehler. Exit 3 gibt es hier
nicht — der gehört zum Dauerbetrieb, siehe Exit-Codes.