289 lines
8.4 KiB
Markdown
289 lines
8.4 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```yaml
|
|
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:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
docker compose exec webserver python manage.py shell
|
|
```
|
|
|
|
```python
|
|
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.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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](02-export-aus-ecodms.md).
|