Files
migrate-ecodms-to-paperless/docs/01-paperless-vorbereiten.md
2026-09-04 19:51:02 +02:00

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.