Paperless-ngx ist in einer Stunde aufgesetzt: Docker-Compose-Datei, consume-Ordner, fertig. Der Dienst läuft, du wirfst die ersten Scans rein, die Volltextsuche funktioniert. Alles fühlt sich gelöst an.
Dann kommt Woche zwei, und plötzlich stehst du an drei Stellen gleichzeitig an:
- Erreichbarkeit. Das Webinterface hängt an
http://…:8091— IP und Port. Kein sauberes HTTPS, und der zentrale Reverse Proxy im Netz soll die Instanz eigentlich gar nicht sehen. - Anmeldung. Du hast einen lokalen Superuser mit allen Dokumenten darin. Jetzt willst du dich über den zentralen Verzeichnisdienst anmelden — und merkst, dass daran mehr hängt als ein zweiter Login.
- OCR. Ein dicker Scanner-Stapel crasht den Import reproduzierbar, ohne brauchbare Fehlermeldung. Der Container-OOM-Killer schlägt zu, während du auf Zeitüberschreitung tippst.
Die drei Baustellen hängen enger zusammen, als sie aussehen: Punkt 2 braucht Punkt 1 (ein SSO-Redirect funktioniert nur, wenn die Instanz wirklich über HTTPS erreichbar ist), und Punkt 1 plus Punkt 3 kommen sich beim Container-Recreate in die Quere. Dieser Guide zieht alle drei in einem Rutsch durch — mit den Werten und Stolpersteinen, die tatsächlich aufgetreten sind.
Hinweis zur Umgebung: Die Beispiele unten nutzen ein anonymisiertes Homelab. Der Paperless-Host heißt
doc-vader, der Keycloak-/Step-CA-Hostauth-yoda, der Verzeichnisdienstdc-obiwan, die interne Domaindarkside.local, der Realmrebellion.
Abschnitt 1 — HTTPS ohne Port
Das Ziel
Paperless soll intern unter zwei Namen erreichbar sein:
https://paperless.darkside.local— der vollständige Name, ohne Porthttps://paperless— der Kurzname, der per 301 auf den FQDN umleitet
Kein Port 8091 mehr in der Adresszeile, keine WAN-Exposition. Die Instanz bleibt physisch vom Internet getrennt.
Die Architektur-Entscheidung
Der Reflex ist, den zentralen Reverse Proxy zu nehmen, der ohnehin im Netz steht. Das ist hier bewusst nicht passiert. Entscheidend war das Argument: ein lokaler Reverse Proxy direkt auf dem Paperless-Host trennt die Instanz physisch vom Internet, statt sie über eine IP-Allowlist „irgendwie" zu schützen. Dazu kommt: kein DNS-Cutover, kein Blast-Radius auf die vielen anderen Domains, die über den zentralen Proxy laufen.
Die Wahl fiel auf den Nginx Proxy Manager (NPM) als Docker-Container, statt rohem nginx. Grund: das ist das Standard-Muster in der übrigen Infrastruktur, und Zertifikate sowie Hosts lassen sich darüber sauber verwalten. Das TLS-Zertifikat kommt aus der internen Step-CA.
Was du brauchst
- Den Paperless-Host (hier
doc-vader, intern192.168.66.70) mit laufendem Docker und Paperless auf Port8091 - Einen zweiten internen Host mit Step-CA (
auth-yoda) zur Zertifikatsausstellung - Einen DNS-Eintrag für
paperless.darkside.localauf den Paperless-Host - Die Root-CA der Step-CA, um sie auf den Clients zu installieren
Schritt 1: Nginx Proxy Manager aufsetzen
NPM läuft als eigener Docker-Container in einem eigenen Verzeichnis auf doc-vader. Zwei Dinge sind hier wichtig:
Erstens: Deinstalliere rohes nginx, falls es herumliegt. Ein hängengebliebener apt install nginx blockiert die Ports 80/443 und damit NPM. In der Praxis reichte apt purge.
Zweitens: NPM in Version 2.15.1 bringt keinen Default-Login mehr mit. Du kommst also nicht über das Standard-Passwort rein, sondern legst die erste Admin-Instanz über den unauthentifizierten Endpunkt POST /api/users an:
# Ersten Admin anlegen — NPM 2.15.1 hat keinen Default-Login mehr.
# Admin-UI ist nur intern erreichbar (kein WAN).
# Passwort NICHT in die Kommandozeile (Shell-History/ps) — besser per stdin/Env-Datei.
# Port 81 ist bis hier ein unauthentifizierter Admin-Create: nur aus dem LAN erreichbar
# lassen und nach dem ersten Login wieder schließen.
curl -X POST http://192.168.66.70:81/api/users \
-H 'Content-Type: application/json' \
-d '{"name":"Admin","nickname":"labadmin","email":"admin@darkside.local","roles":["admin"],"password":"<dein-passwort>"}'
Danach meldest du dich an der Weboberfläche an (http://192.168.66.70:81) und legst deine Hosts an.
Erwartete Ausgabe: POST /api/users antwortet mit HTTP 201 und dem angelegten Benutzer-Objekt. Ein anschließender Login an der UI ist möglich.
Schritt 2: Zertifikat aus der Step-CA
Das Zertifikat wird nicht über Let’s Encrypt geholt (die Instanz ist nicht öffentlich), sondern aus der internen Step-CA. Wichtig sind die Subject Alternative Names (SAN = alternative Namen, unter denen das Zertifikat gilt). Das Zertifikat muss auf allen drei Wegen passen, über die du die Instanz ansprichst:
paperless.darkside.local(FQDN)paperless(Kurzname)192.168.66.70(IP, für den Fall, dass dich etwas direkt anspricht)
Gültigkeit: 1 Jahr. Ausgestellt wird über den internen Provisioner (rebellion-admin).
# Auf auth-yoda: Zertifikat für alle drei Namen ausstellen.
# SANs: FQDN + Kurzname + IP, Laufzeit 1 Jahr.
step ca certificate paperless.darkside.local paperless.crt paperless.key \
--san paperless.darkside.local \
--san paperless \
--san 192.168.66.70 \
--not-after 8760h \
--provisioner rebellion-admin
Das Zertifikat samt Key lädst du anschließend im NPM unter SSL Certificates → Add → Custom Certificate hoch. Kein ACME, kein automatisches Renewal — die Erneuerung machst du jährlich von Hand oder per Job.
Erwartete Ausgabe: step ca certificate schreibt paperless.crt und paperless.key. Der NPM akzeptiert das Paar als Custom Certificate ohne Fehler.
Schritt 3: Proxy-Host für den FQDN
Im NPM legst du einen Proxy Host an:
| Feld | Wert |
|---|---|
| Domain Names | paperless.darkside.local |
| Scheme | http |
| Forward Hostname / IP | 192.168.66.70 |
| Forward Port | 8091 |
| SSL Certificate | das Step-CA-Zertifikat aus Schritt 2 |
Das Backend bleibt bewusst auf HTTP auf 8091 — NPM terminiert TLS und leitet intern im Klartext weiter. Genau deshalb muss Paperless später über die Proxy-Header erfahren, dass die ursprüngliche Anfrage HTTPS war (Schritt 6).
Schritt 4: Redirection-Host für den Kurznamen
Für den Kurznamen paperless legst du einen Redirection Host an, der per 301 auf den FQDN umleitet:
paperless → 301 → https://paperless.darkside.local
Bewusst kein zweiter gleichwertiger Origin. Würdest du beide Namen als gleichwertige Proxy-Hosts betreiben, bräuchtest du in Keycloak zwei gültige Redirect-URIs und in Paperless zwei gültige CSRF-Origins — und handelst dir doppelte Fehlerquellen ein. Ein 301-Redirect ist die robustere Lösung.
Schritt 5: Keycloak-Redirect-URI ergänzen
Damit das spätere SSO (Abschnitt 2) über HTTPS funktioniert, trägst du im Keycloak-Client paperless die neue Redirect-URI ein:
https://paperless.darkside.local/accounts/oidc/keycloak/login/callback/
Die alte URI mit http://…:8091/… bleibt bewusst registriert und dient als Rollback-Pfad. Beide URIs gleichzeitig zu haben kostet nichts und hält den Rückweg offen.
Schritt 6: Paperless auf HTTPS umstellen
Jetzt teilst du Paperless mit, dass es hinter einem TLS-Proxy läuft. In der docker-compose.env bzw. den environment-Werten des webserver-Containers kommen die HTTPS-relevanten Variablen dazu:
# docker-compose.env auf doc-vader (Auszug, HTTPS-Umstellung)
PAPERLESS_URL=https://paperless.darkside.local
PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL=https
# Proxy-Header: Paperless muss erfahren, dass TLS vor ihm terminiert wurde
PAPERLESS_PROXY_SSL_HEADER=<Platzhalter: Wert aus deiner laufenden Django-Env übernehmen>
PAPERLESS_TRUSTED_PROXIES=<IP des Proxy, wie Paperless sie im Socket sieht>
# CSRF muss den öffentlichen HTTPS-Origin kennen
PAPERLESS_CSRF_TRUSTED_ORIGINS=https://paperless.darkside.local
Drei Dinge dazu:
PAPERLESS_URList der absolute URL, den Paperless nach außen verwendet — er muss auf den FQDN zeigen, nicht auf IP:Port.PAPERLESS_PROXY_SSL_HEADERsagt Paperless, dass es demX-Forwarded-Proto-Header des Proxys vertrauen soll. Ohne das hält sich Paperless weiter für HTTP und baut falsche Redirect- und Callback-URLs.PAPERLESS_TRUSTED_PROXIESmuss die Adresse des NPM enthalten — sonst ignoriert Paperless die Proxy-Header aus Sicherheitsgründen. Achtung: Läuft NPM als Container auf demselben Host, sieht Paperless nicht die LAN-IP des Hosts, sondern die Bridge-/Container-IP des Proxys. Die falsche IP führt zum selben Symptom (Invalid redirect_uri) wie ein fehlender Header — prüf perdocker exec, welche Quell-IP an Paperless ankommt.
Wichtig: Die beiden
<>-Werte sind bewusst Platzhalter. Übernimm sie 1:1 aus deiner laufenden Instanz (bzw. der Doku deiner Paperless-Version) — nicht blind aus irgendeinem Blog kopieren.
Vorher ein Backup der Compose-Dateien anlegen (z. B. .bak-<datum>), dann den Webserver neu erzeugen:
docker compose up -d --no-deps webserver
Wichtig: up -d und nicht restart — nur up -d liest die geänderte Env-Datei neu ein, ein restart tut das nicht.
Schritt 7: Root-CA auf die Clients
Das Step-CA-Zertifikat gilt nur, wenn die Root-CA der Step-CA auf jedem Client als vertrauenswürdig installiert ist. Sonst bekommst du im Browser eine Zertifikatswarnung, obwohl die Kette technisch sauber ist.
Exportiere die Root-CA auf dem Step-CA-Host und installiere sie auf deinen Geräten. Das genaue Verfahren hängt von der Distribution ab — auf einem Debian/Ubuntu-Client läuft es über apt und update-ca-certificates, auf einem Arch-basierten Client über pacman plus trust extract-compat. Der Feuerteufel (Firefox) übernimmt den System-Trust-Store automatisch; es war kein separater enterprise_roots-Schalter nötig.
Erwartete Ausgabe: curl -v https://paperless.darkside.local zeigt keinen Zertifikatsfehler mehr und arbeitet ohne --insecure.
Verifikation
| Prüfung | Erwartetes Ergebnis |
|---|---|
| TLS-Kette gegen die Step-CA-Root | valide, ohne --insecure |
https://paperless.darkside.local | HTTP 302 auf die Login-Seite, dann 200 |
https://paperless (Kurzname) | 301 → FQDN → 200 |
| Post-Consume-Hook | exit 0 |
| Container-Status | healthy |
Fallstricke — Das Perfide:
Das Perfide: Der Abbruch des apt install nginx war nur die Anzeige. Der Installationsjob lief im Hintergrund trotzdem zu Ende und belegte die Ports 80 und 443 — NPM kam nicht hoch, obwohl „nichts installiert" aussah. Erst apt purge gab die Ports frei.
Das Perfide: restart liest die Env-Datei nicht neu. Wer nach dem Setzen von PAPERLESS_URL nur docker compose restart webserver macht, sieht keine Änderung und sucht den Fehler in der falschen Ecke. Es muss up -d sein.
Das Perfide (Netzwerk-Split): Beim Recreate landete der frisch erzeugte webserver-Container in einem anderen Docker-Netz als die bereits laufenden Container (broker, db). Ergebnis: Error -5 connecting to broker:6379. No address associated with hostname. Der Fix ist ein Umhängen per docker network connect / docker network disconnect plus docker restart — kein Full-Stack-Recreate. Das ist wichtig, weil ein kompletter Recreate Container-interne Patches verliert (siehe unten).
Das Perfide (verlorene Patches): Ein manuell im Container angewandter Patch (ein bulk=100 in der Mail-Komponente) ist nach jedem Recreate weg und muss erneut angewendet werden. Solche Patches gehören, wenn möglich, ins Image — sonst in die Checkliste jedes Recreate.
Das Perfide (Shell-Escaping): Ein Keycloak-Client-Secret mit Sonderzeichen scheitert bei curl -d, weil die Shell die Zeichen interpretiert. curl --data-urlencode umgeht das.
Lessons Learned (HTTPS)
- Ein lokaler Reverse Proxy ist eine Architektur-Entscheidung, kein Provisorium. Physische Trennung schlägt IP-Allowlist.
- Der Kurzname gehört auf einen 301, nicht auf einen zweiten Origin. Das spart doppelte Redirect-URIs und doppelte CSRF-Origins.
- SANs müssen FQDN, Kurzname und IP abdecken. Sonst funktioniert einer der drei Zugriffswege nicht.
up -d, nichtrestart— sonst bleibt die Env-Änderung wirkungslos.- Container-Patches sind flüchtig. Jeder Recreate setzt sie zurück.
Abschnitt 2 — SSO über Keycloak mit dem Connect-Flow
Das Ziel
Ein bestehender lokaler Superuser (hier c3po, id=3, 909 Dokumente) soll sich zusätzlich über den zentralen Verzeichnisdienst anmelden können — konkret als leia.organa. Ohne Datenverlust, ohne Duplikat-User, ohne Lockout. Das lokale Passwort bleibt als Fallback aktiv.
Der falsche Weg — E-Mail-Matching (Das Perfide)
Der naheliegende Plan: einfach die E-Mail-Adresse des lokalen Users auf die des Verzeichnis-Users ändern, dann verbindet die Anmeldung beide automatisch. In diesem Fall hieße das, c3po@darkside.local auf leia.organa@darkside.local umzustellen.
Das funktioniert nicht. Zwei Gründe, die während der Umsetzung gegen den allauth-Quellcode geprüft wurden:
- allauth matcht nicht über E-Mail, sondern ausschließlich über
SocialAccount(provider, uid). Deruidist die eindeutige ID des Users beim Identity Provider — im OIDC-Fall eine UUID. Ohne einen passendenSocialAccount-Eintrag findet allauth nichts, egal wie die E-Mail lautet. - Der E-Mail-Auth-Pfad ist sogar gefährlich. Hätte man ihn erzwungen, würde allauth beim vermeintlichen Match das lokale Passwort löschen (
wipe_password()). Der lokale Fallback wäre in genau dem Moment weg, in dem man ihn braucht.
Das Perfide: Beide Fehlerbilder sehen von außen harmlos aus — „E-Mail passt doch". Der eine führt zu einem stillen Nicht-Match, der andere zu einem Passwortverlust. Die Lösung ist deshalb nicht das E-Mail-Matching, sondern der Connect-Flow: Der bereits eingeloggte User verbindet sein bestehendes Konto explizit mit dem Provider. Kein Signup, kein Auto-Linking, kein Passwortverlust.
Schritt 1: Keycloak-Client anlegen
Im Realm (rebellion) legst du einen Client für Paperless an:
- Client-ID:
paperless - Access Type: Confidential (es gibt ein Secret)
- Standard Flow: aktiviert
- Redirect-URI: die HTTPS-URI aus Abschnitt 1, plus die alte HTTP-URI als Rollback
https://paperless.darkside.local/accounts/oidc/keycloak/login/callback/
http://paperless.darkside.local:8091/accounts/oidc/keycloak/login/callback/
Schritt 2: Paperless konfigurieren
In der docker-compose.env auf doc-vader kommen die SSO-Variablen dazu. Die Provider-Registrierung trägst du mit der OIDC-Konfiguration deines Keycloak-Clients ein (Issuer, Client-ID, Client-Secret, PKCE aktiviert):
# docker-compose.env auf doc-vader (Auszug, SSO-Anbindung)
# Provider-Registrierung (Werte je nach OIDC-Provider/Client) inkl. OAUTH_PKCE_ENABLED
PAPERLESS_APPS=<allauth-OIDC-Provider>
PAPERLESS_SOCIALACCOUNT_PROVIDERS=<Provider-Konfiguration, enthält OAUTH_PKCE_ENABLED>
# Kein automatisches Anlegen und kein Auto-Linking von Accounts
PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS=false
PAPERLESS_SOCIAL_AUTO_SIGNUP=false
# URL und Schema passend zur HTTPS-Instanz (siehe Abschnitt 1)
PAPERLESS_URL=https://paperless.darkside.local
PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL=https
Im docker-compose.yml ergänzt du außerdem einen extra_hosts-Eintrag, damit der Container den öffentlichen Keycloak-Namen auflösen kann, ohne auf öffentliches DNS angewiesen zu sein:
# docker-compose.yml auf doc-vader (Auszug)
services:
webserver:
extra_hosts:
- "auth-yoda.darkside.local:10.99.99.29"
Die beiden Variablen PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS=false und PAPERLESS_SOCIAL_AUTO_SIGNUP=false sind hier keine Deko: Sie verhindern, dass sich irgendjemand über den Provider automatisch ein Konto anlegt. Die Verknüpfung soll ausschließlich über den bewussten Connect-Flow passieren.
Achtung, Hostname-Mismatch: Der Keycloak-Issuer ist an einen festen Hostnamen gebunden. Der OIDC-Issuer lautet in diesem Setup https://auth-yoda.darkside.local/realms/rebellion, egal über welchen Host du das well-known-Endpoint abfragst. Wenn du diesen Namen falsch setzt, scheitert die Discovery.
Schritt 3: Container neu erzeugen
Wie in Abschnitt 1: docker compose up -d --no-deps webserver (nicht restart). Danach prüfen, ob der Netzwerk-Split wieder zugeschlagen hat (Abschnitt 1) — und den bulk=100-Patch erneut anwenden.
Schritt 4: Den Connect-Flow durchführen
Die Verknüpfung passiert eingeloggt mit dem lokalen Passwort:
- Mit dem lokalen Konto (
c3po) über das lokale Passwort anmelden. - Oben rechts auf den Avatar → My Profile.
- Abschnitt Social account providers → SSO → Connect.
Falls der Button in der UI fehlt, geht es auch direkt über die URL:
https://paperless.darkside.local/accounts/oidc/keycloak/login/?process=connect
Der Connect-Flow verknüpft das bestehende Konto mit dem Provider-Account. Es wird kein neuer User angelegt und kein Passwort angefasst.
Verifikation (Closed-Loop)
Die Verifikation läuft gegen die Datenbank, mit einem Snapshot vorher und nachher. Erwartet wird ein einzelner neuer SocialAccount-Eintrag, der auf den bestehenden User zeigt:
SocialAccountmituser_id=3,provider=keycloakund deruid(UUID) des Verzeichnis-Users- Die
uidentspricht exakt demleia.organa-Account - Kein Duplikat-User — die Gesamtzahl der User bleibt bei 3
is_superuser=Trueunverändert- Das lokale Passwort funktioniert weiterhin (Fallback intakt)
- 909 Dokumente unverändert
Nur wenn alle sechs Punkte stimmen, ist die Anbindung sauber. Der entscheidende Test ist der zweite: kein zusätzlicher User.
Fallstricke — Das Perfide:
Das Perfide: E-Mail-Matching wirkt logisch und ist trotzdem falsch. allauth kennt nur SocialAccount(provider, uid), und der E-Mail-Pfad löscht beim Match das lokale Passwort. Wer hier auf „das passt schon" setzt, verliert den Fallback genau dann, wenn er ihn braucht.
Das Perfide: PAPERLESS_ACCOUNT_DEFAULT_HTTP_PROTOCOL muss zum tatsächlich genutzten Schema passen. Läuft Paperless über HTTP, du setzt aber https, lehnt Keycloak den Callback mit Invalid redirect_uri ab — und der Fehler sieht aus wie ein falsch konfigurierter Redirect, ist aber ein Schema-Mismatch. Nach dem HTTPS-Umbau (Abschnitt 1) steht der Wert korrekt auf https, davor war er explizit http.
Das Perfide: Auto-Signup. Ohne die beiden false-Werte kann sich praktisch jeder mit einem gültigen Provider-Account ein Paperless-Konto anlegen. Der Connect-Flow macht Auto-Signup überflüssig.
Das Perfide: Ein Secret mit Sonderzeichen scheitert im curl -d-Aufruf an der Shell. --data-urlencode löst es.
Lessons Learned (SSO)
- Verlasse dich nie auf E-Mail-Matching in allauth. Der Match läuft über
(provider, uid), niemals über die E-Mail. - Der E-Mail-Auth-Pfad ist ein Passwort-Killer (
wipe_password()). - Der Connect-Flow ist der sichere Weg: bewusst, eingeloggt, ohne Signup und ohne Passwortänderung.
- Closed-Loop heißt hier: User-Zahl vorher/nachher vergleichen. Ein SSO-Login, der funktioniert, aber einen Duplikat-User anlegt, ist ein Fehlschlag.
- Provider-Konfiguration gegen den Quellcode prüfen, nicht gegen den Bauch. Bei Bibliotheken wie allauth ist die Doku oft unvollständig.
Abschnitt 3 — OCR stabil ohne tmpfs-Explosion
Diese Baustelle hat einen eigenen Incident-Post, weil sie sich als tief genug erwies: Paperless-ngx OCR sprengt das tmpfs. Hier die Kurzfassung als Teil des Produktiv-Setups.
Das Problem
Drei Scanner-Scans kommen per Mail rein (48 + 14 + 34 Seiten, zusammen 81 MB PDF). Alle drei crashen beim Import, reproduzierbar, mitten in der OCR-Verarbeitung, ohne verwertbare Fehlermeldung.
Der Container-/tmp ist ein tmpfs mit 512 MB — das Standard-Setup. Der eigentliche Verbraucher: unpaper, der Vorverarbeitungsschritt von OCRmyPDF (Deskew/Clean). unpaper arbeitet nicht auf dem komprimierten PDF, sondern konvertiert jede Seite in ein unkomprimiertes PPM-Bitmap und legt das in /tmp ab. Bei ~26 MB pro Seite sind das schon für einen 48-Seiten-Scan rund 1,2 GB Temp — gegen ein 512-MB-Limit. Der Worker stirbt mitten im Job.
Schritt 1: Diagnose
# Logs: oft kein klarer Fehler, nur ein abgebrochener Worker
docker logs paperless_webserver --since 1h | grep -i error
# Wichtig: die Container-Disk während des laufenden Imports beobachten
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
Zusätzlich: Die Host-Disk stand unabhängig davon bei 100 %. Grund waren 7 GB verwaiste ocrmypdf.io.*-Temp-Verzeichnisse aus früheren, abgebrochenen OCR-Läufen, die sich nie aufgeräumt hatten.
du -sh /usr/src/paperless/scratch/ocrmypdf.io.* 2>/dev/null | sort -rh | head
Schritt 2: Die Ursache verstehen
Das Perfide: Die Variable, die jede Paperless-Doku zuerst nennt, wenn es um OCR-Temp geht — PAPERLESS_OCR_TMPDIR — existiert in der laufenden Version nicht. Sie wird gesetzt, akzeptiert und dann ignoriert. Ein stiller No-Op. Du denkst, das Problem sei behoben; der nächste Crash beweist das Gegenteil.
Das Perfide: Das tmpfs einfach größer ziehen, z. B. auf 2 GB, funktioniert nicht, wenn der Container-Cgroup selbst bei 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 viel Temp produziert, killt der OOM-Killer den Prozess. Mehr tmpfs verschiebt den Crash-Punkt nur — es löst nichts.
Die eigentliche Ursache: unpaper ist der Haupt-Temp-Verbraucher der gesamten OCR-Kette — nicht OCRmyPDF, nicht Tesseract.
Schritt 3: Die Env-Stellschrauben
Fünf Werte, zusammen in der docker-compose.env:
# docker-compose.env auf doc-vader (Auszug, OCR-Stabilität)
# Persistenten Scratch-Pfad statt tmpfs verwenden
TMPDIR=/usr/src/paperless/scratch
# unpaper deaktivieren — der Haupt-Temp-Verbraucher
PAPERLESS_OCR_CLEAN=none
# PDF-Optimierung (zusätzlicher Temp-Verbrauch) abschalten
PAPERLESS_OCR_USER_ARGS={"optimize": 0}
# Weniger parallele Threads = weniger gleichzeitiger Temp-Verbrauch
PAPERLESS_THREADS_PER_WORKER=4
# Großzügigeres Timeout für die nun langsameren, aber stabilen Jobs
PAPERLESS_WORKER_TIMEOUT=7200
Was passiert:
TMPDIRbiegt den Temp-Pfad vom kleinen tmpfs auf einen persistenten Pfad auf der Container-Disk um (/usr/src/paperless/scratch) — begrenzt nur durch echten Plattenplatz.PAPERLESS_OCR_CLEAN=noneschaltetunpaperkomplett ab und beseitigt damit die PPM-Temp-Flut. Der Trade-off: keine automatische Entrauschung/Begradigung mehr. Für saubere Scanner-Scans ein akzeptabler Tausch.PAPERLESS_OCR_USER_ARGS={"optimize": 0}schaltet die PDF-Optimierung als zusätzlichen Temp-Verbraucher ab.PAPERLESS_THREADS_PER_WORKER=4reduziert die gleichzeitig erzeugten Temp-Daten.PAPERLESS_WORKER_TIMEOUT=7200(2 Stunden) gibt den jetzt etwas langsameren Jobs genug Luft.
Dann den Webserver neu erzeugen:
docker compose up -d --no-deps webserver
Schritt 4: Aufräumen und vorbeugen
# Einmalig: alte ocrmypdf-Reste löschen (geben die Host-Disk frei)
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 {} \;
Der Cleanup-Cron ist kein Luxus: Verwaiste Temp-Dateien aus gecrashten Jobs räumen sich nicht selbst auf und füllen sonst zuverlässig die Platte.
Verifikation
# Während eines neuen, großen Imports: tmpfs bleibt fast leer
docker exec paperless_webserver df -h /tmp
Good — nach dem Fix:
Filesystem Size Used Avail Use% Mounted on
tmpfs 512M ~10M ... ~3% /tmp
/tmp bleibt nahezu leer, weil unpaper gar nicht mehr läuft; der verbleibende Temp von OCRmyPDF/Tesseract ist marginal. Zusätzlich prüfen: Container healthy, Import läuft durch, Host-Disk stabil.
Fallstricke — Das Perfide:
Das Perfide: PAPERLESS_OCR_TMPDIR ist ein dokumentierter Name ohne Funktion. Bevor du eine Variable als Fix einsetzt, verifiziere, dass sie tatsächlich etwas bewirkt — prüfe die Wirkung, nicht die Doku.
Das Perfide: „tmpfs größer machen" tötet den Job per OOM statt per vollem tmpfs. Beide Wege enden mit einem toten Worker.
Das Perfide: Der Temp-Fresser ist selten das Haupttool. Bei OCR-Speicherproblemen zuerst die Vorverarbeitung prüfen — hier unpaper.
Das Perfide: Verwaiste ocrmypdf.io.*-Reste füllen die Platte unabhängig vom tmpfs. Ohne Cron sammelt sich das bei jedem Crash neu an.
Lessons Learned (OCR)
- Dokumentierte Env-Variablen sind nicht automatisch wirksame Env-Variablen. Wirkung prüfen, nicht Namen vertrauen.
- tmpfs zählt gegen den RAM-Cgroup. Größer ziehen ist kein Fix, nur eine Verlagerung.
unpaperzuerst verdächtigen. Es produziert die großen unkomprimierten Zwischendateien.- Cleanup-Cron von Anfang an. Gecrashte OCR-Jobs hinterlassen Temp-Müll, der nie verschwindet.
- Nach jedem Recreate: Container-Patches erneut anwenden.
Gesamt-Checkliste
HTTPS ohne Port
- Rohes nginx entfernt (Ports 80/443 frei), NPM als Docker-Container
- Erster NPM-Admin über
POST /api/usersangelegt (kein Default-Login in 2.15.1) - Step-CA-Zertifikat mit SANs
FQDN + Kurzname + IP, 1 Jahr - Proxy-Host
paperless.darkside.local→192.168.66.70:8091 - Redirection-Host Kurzname → 301 → FQDN
- Keycloak-Redirect-URI (HTTPS) ergänzt, alte als Rollback registriert
-
PAPERLESS_URL,…_HTTP_PROTOCOL=https,PROXY_SSL_HEADER,TRUSTED_PROXIES,CSRF_TRUSTED_ORIGINSgesetzt -
docker compose up -d(nichtrestart) - Root-CA auf allen Clients installiert, TLS ohne
--insecurevalide
SSO über Keycloak
- Keycloak-Client
paperless, Confidential, Standard Flow, Realmrebellion - Provider-Konfiguration inkl. PKCE gesetzt
-
PAPERLESS_SOCIALACCOUNT_ALLOW_SIGNUPS=false,PAPERLESS_SOCIAL_AUTO_SIGNUP=false -
extra_hostsfür den Keycloak-Namen - Kein E-Mail-Matching — Connect-Flow verwendet
- Closed-Loop:
SocialAccount(provider, uid)vorhanden, User-Zahl unverändert, Superuser-Flag unverändert, lokales Passwort funktioniert weiter
OCR-Stabilität
-
PAPERLESS_OCR_TMPDIRnicht als Fix verwenden (No-Op) -
TMPDIR=/usr/src/paperless/scratch(persistent, nicht tmpfs) -
PAPERLESS_OCR_CLEAN=none(unpaper aus) -
PAPERLESS_OCR_USER_ARGS={"optimize": 0} -
PAPERLESS_THREADS_PER_WORKER=4 -
PAPERLESS_WORKER_TIMEOUT=7200 - Verwaiste
ocrmypdf.io.*gelöscht, Cleanup-Cron eingerichtet - Nach dem Recreate:
bulk=100-Patch erneut angewendet
Lessons Learned (übergreifend)
- Produktiv wird Paperless an den Rändern, nicht im Kern. Der Import funktioniert sofort — HTTPS, SSO und OCR-Stabilität sind die drei Stellen, an denen der Betrieb wirklich beginnt.
- Bibliotheken gegen den Quellcode prüfen. Sowohl das allauth-Matching als auch die nicht existente
PAPERLESS_OCR_TMPDIR-Variable waren dokumentierte Erwartungen, die die Realität widerlegt hat. - Closed-Loop verifizieren, nicht „geht doch". Ein SSO-Login, der einen Duplikat-User anlegt, ist kaputt. Ein OCR-Fix, der das tmpfs leert, aber die Ursache nicht kennt, ist Zufall.
up -dstattrestart, immer. Env-Änderungen wirken nur beim Neuerzeugen.- Recreate ist der Moment, in dem die versteckten Fehler zuschlagen: Netzwerk-Split und verlorene Container-Patches. Beide in die Checkliste, beide nach jedem
up -dprüfen.
🛒 Empfohlene Hardware
Paperless ist genügsam, aber OCR frisst beim Import kurzzeitig CPU und Disk-I/O. Ein sparsamer Mini-PC mit schneller SSD reicht für ein Homelab locker:
- 🖥️ Mini-PC N100 Beelink S12 Pro — leiser, stromsparender Docker-Host für Paperless + Nginx Proxy Manager
- 💾 Samsung SSD 990 NVMe (2 TB) — schnelle SSD für Media, Scratch und Datenbank
- 💾 Synology DS224+ — NAS als Backup-Target für das Dokumentenarchiv
Einige Links auf dieser Seite sind Affiliate-Links. Wenn du über diese Links einkaufst, erhalte ich eine kleine Provision — für dich ändert sich am Preis nichts. So unterstützt du diesen Blog und ermöglicht weitere kostenlose Tutorials. Danke! 🙏