8.4 KiB
1. Paperless-Instanz vorbereiten
Ziel ist eine leere Paperless-Instanz, in die importiert wird. Ob sie später produktiv läuft oder nur der Migration dient, ist zunächst gleichgültig — die Konfiguration sollte aber von Anfang an die Produktivfassung sein.
Wo migrieren?
Der Import ist CPU-gebunden, nicht I/O-gebunden. Wenn die spätere Produktivumgebung ein schwacher Server ist, lohnt es, die Migration auf einer stärkeren Maschine zu fahren und den Bestand danach umzuziehen.
Ein Paperless-Bestand ist portabel: Postgres-Dump plus die Verzeichnisse
media und data. Voraussetzung ist, dass Paperless- und
Postgres-Hauptversion auf beiden Seiten übereinstimmen.
Zweiter Vorteil: Man kann beim Skriptbau beliebig oft von vorn anfangen, ohne die produktive Instanz anzufassen.
Versionen festlegen
Nie :latest. Alle fünf Images pinnen — Paperless, Postgres, Redis,
Gotenberg und Tika.
Bei Tika ist das keine Stilfrage: apache/tika:latest zeigt inzwischen
auf Tika 4.0.0, und Paperless 3.1 quittiert das mit HTTP 406 Not Acceptable bei jedem Office-Dokument und jeder .eml. In einem
Testlauf waren das 315 Fehlschläge in einer einzigen Tranche, ohne
verwertbare Fehlermeldung.
Verzeichnisse
mkdir -p data/{data,media,export,consume,pgdata,redis}
Auf Btrfs vor dem ersten Start das Copy-on-Write für die Datenbankverzeichnisse abschalten, sonst fragmentieren sie stark:
chattr +C data/pgdata data/redis
lsattr -d data/pgdata # muss ---------------C zeigen
Wirkt nur auf leere Verzeichnisse. Nachträglich greift es bei bestehenden Dateien nicht.
Auf ZFS ist das nicht nötig. Dort lohnt getrenntes Anlegen der Datenbereiche, damit Snapshots und Sicherungen gezielt greifen.
Benutzer im Container
Das Paperless-Image startet seit 3.x bereits als unprivilegierter Benutzer
(1000:1000), nicht als root. Damit ist USERMAP_UID/USERMAP_GID
wirkungslos — dieser Mechanismus setzt voraus, dass das Einstiegsskript als
root beginnt und selbst herunterschaltet. Im Log erscheint dann:
[init-user] WARNING: USERMAP_UID/USERMAP_GID are set but have no effect
when the container is started as a non-root user
Setz die UID stattdessen fest in der Compose und richte den Dateibesitz danach aus:
user: "1500:1500"
Zwei Container, bei denen das falsch wäre
Postgres. Sein Einstiegsskript korrigiert beim Start Besitzer und Rechte des Datenverzeichnisses und schaltet dann selbst auf UID 999 herunter. Ein erzwungener Benutzer lässt das scheitern:
chmod: changing permissions of '/var/lib/postgresql/data': Permission denied
Stattdessen gehört das Verzeichnis auf dem Host der 999:
chown 999:999 data/pgdata
Gotenberg. Chromium legt beim Start ein Crashpad-Verzeichnis im Home des internen Benutzers an. Unter fremder UID scheitert das:
chrome failed to start: chrome_crashpad_handler: --database is required
Symptom ist ein HTTP 500 beim Rendern von .eml. Unkritisch ohne eigene
UID, weil Gotenberg keine Bind Mounts hat und nur im internen Netz
erreichbar ist.
SELinux
Auf Systemen mit SELinux im Enforcing-Modus — etwa openSUSE Tumbleweed —
brauchen Bind Mounts eine Kennzeichnung, sonst bekommen die Container
Permission denied, obwohl Besitzer und Rechte stimmen.
Erkennbar am Punkt hinter den Rechten und am Kontext unlabeled_t:
ls -ldZ data/pgdata
# drwxr-xr-x. 999 999 unconfined_u:object_r:unlabeled_t:s0
unlabeled_t tritt typischerweise bei frisch angelegten
Btrfs-Subvolumes auf. Dauerhafte Lösung:
zypper in policycoreutils-python-utils
semanage fcontext -a -t container_file_t "/pfad/zu/data(/.*)?"
restorecon -Rv /pfad/zu/data
Die :z-Option an den Bind Mounts wirkt nur, wenn der Docker-Daemon mit
"selinux-enabled": true läuft. Der semanage-Eintrag ist unabhängig
davon und überlebt Neustarts.
Setz das Label, bevor große Datenmengen hineinkopiert werden — sonst
läuft restorecon später über den gesamten Bestand.
Konfiguration
.env.example nach .env kopieren und anpassen. Geheimnisse erzeugen mit
openssl rand -hex 32 — hexadezimal statt base64, dann enthält der Wert
garantiert keine Zeichen, die die Env-Datei durcheinanderbringen.
Was besonders zu beachten ist
PAPERLESS_DBPASS ausschreiben. Docker Compose ersetzt Variablen nur
in der docker-compose.yml, nicht innerhalb einer über env_file
eingebundenen Datei. Ein ${POSTGRES_PASSWORD} dort wird wörtlich
durchgereicht.
PAPERLESS_FILENAME_FORMAT einmal festlegen. Eine spätere Änderung
benennt den gesamten Bestand um. Seit 3.x doppelte geschweifte Klammern:
PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ document_type }}/{{ title }}
OCR-Einstellungen in 3.x-Syntax. Die alten Werte OCR_MODE=skip und
OCR_SKIP_ARCHIVE_FILE sind entfallen:
PAPERLESS_OCR_MODE=auto
PAPERLESS_ARCHIVE_FILE_GENERATION=auto
auto überspringt OCR, wenn bereits ein Textlayer vorhanden ist — bei
einem Bestand aus einem anderen DMS der Regelfall. Der Unterschied liegt
bei Stunden gegen Tage.
PAPERLESS_CONSUMER_IGNORE_PATTERNS nicht setzen. In 3.x ist das eine
Liste von regulären Ausdrücken, nicht von Glob-Mustern. Ein Eintrag wie
*.part lässt den Consumer beim Start abstürzen:
re.PatternError: nothing to repeat at position 0
Die eingebauten Vorgaben decken .DS_Store, Thumbs.db und Ähnliches
bereits ab.
Consume-Ordner abschalten. Während der Migration:
PAPERLESS_CONSUMER_POLLING=0
Worker hochdrehen, aber mit Bedacht. Die Migration ist seriell je Dokument, Parallelität nutzt vor allem den Sidecar-Diensten.
Starten
docker compose up -d --build
docker compose logs -f webserver
Das --build ist nötig, wenn ein eigenes Image mit Plugins verwendet
wird. Ohne baut Compose nicht neu, sobald der Tag existiert.
Erwartet wird eine saubere Startmeldung ohne Warnungen. Jede Warnung im Log ist eine spätere Fehlerquelle — die drei häufigsten betreffen OCR-Einstellungen, das Dateinamensformat und die Benutzerabbildung.
Benutzer und Gruppen anlegen
Vor dem ersten Import, damit die Rechteübersetzung sie zuordnen kann.
docker compose exec webserver python manage.py shell
from django.contrib.auth.models import User, Group
for n in ["benutzer_a", "benutzer_b"]:
User.objects.get_or_create(username=n)
for g in ["Gruppe_A", "Gruppe_B"]:
Group.objects.get_or_create(name=g)
U = lambda n: User.objects.get(username=n)
Group.objects.get(name="Gruppe_A").user_set.set([U("benutzer_a"), U("benutzer_b")])
for g in Group.objects.all():
print(g.id, g.name, [u.username for u in g.user_set.all()])
for u in User.objects.all():
print(u.id, u.username)
Notier die IDs. Sie werden in rollen.yml und in
PAPERLESS_DEFAULT_PERMISSIONS_OWNER gebraucht — und dort erwartet
Paperless eine numerische ID, keinen Benutzernamen. Eine falsche ID
lässt jeden Konsumvorgang mit Error while queuing document scheitern.
Löschrechte
Keiner Gruppe das Recht zum endgültigen Löschen geben. Zusammen mit Audit-Log, Versionierung und Papierkorb-Frist ist das der vierte Baustein der Nachvollziehbarkeit.
API-Token
Im Benutzerprofil erzeugen, nicht in der allgemeinen Verwaltung.
export PT=dein_token_40_zeichen
curl -s -H "Authorization: Token $PT" http://localhost:8000/api/documents/
Erwartet wird {"count":0,...}. Kommt
{"detail":"Authentication credentials were not provided."}, fehlt der
Header-Name — die Variable enthält nur den Token, nicht Authorization:.
Alternativ per Shell:
docker compose exec webserver python manage.py shell -c \
"from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
t,_=Token.objects.get_or_create(user=User.objects.get(username='DEIN_BENUTZER'))
print(t.key)"
Snapshot
Wenn Instanz, Benutzer, Gruppen und Token stehen: einen Snapshot des Datenverzeichnisses anlegen. Das ist der Zustand, zu dem man beim Bau des Migrationsskripts immer wieder zurückwill.
docker compose down
btrfs subvolume snapshot -r data ../snapshots/paperless-vorbereitet
docker compose up -d
Auf ZFS entsprechend zfs snapshot. Container vorher stoppen, sonst ist
der Datenbankstand nicht konsistent zu den Dateien.
Weiter mit Export aus ecoDMS.