Initial commit

This commit is contained in:
2026-09-04 19:51:02 +02:00
parent 7840783dc6
commit 13bae7d600
14 changed files with 3766 additions and 0 deletions

View File

@@ -0,0 +1,288 @@
# 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).