Durch Freunde wurde ich auf das Document Management System paperless aufmerksam, dass im derzeit aktuellen Fork paperless-ngx weiterlebt. In Verbindung mit einem Einzugsscanner ist es ein wahr gewordener Traum, der mich sehr in meinem Alltag entlastet.

Das Aktualisieren von paperless-ngx (im Folgenden nur noch paperless genannt) unter Docker ist zwar in der Dokumentation recht gut beschrieben, aber das Updaten der Datenbank, finde ich zu kompliziert, weil es nur auf die offizielle PostgreSQL-Dokumentation verweist.

Hier müsste ich mit den CLI-Tools wie pg_dumpall und pg_upgrade hantieren, was arbeiten im Docker-Container notwendig macht und vermutlich auch doppelte Docker-Container-Instanzen nötig macht. Eine automagische Migration unterstützen die postgres-images nach meinem Wissen derzeit nicht. Also habe ich eine andere Möglichkeit gesucht und auch gefunden!

paperless liefert die beiden Tools document_exporter und document_importer mit, die ein vollständiges Backup und Wiederherstellung (Restore) der kompletten Instanz bereitstellt.

Mit diesen beiden Tools lässt sich ein Major-Upgrade bewerkstelligen. Wie ich heute rausfand ist sogar der Wechsel der Datenbank-Engine ohne allzu große Umstände möglich.

Überblick

Der grobe Pfad für ein Major-Upgrade ist daher:

  • Zuerst ein vollständiges Backup anfertigen.
  • Alle Daten inklusive Volumes wegwerfen.
  • Die PostgreSQL-Version im docker-compose.yml anpassen.
  • Schlussendlich alle Daten wieder importieren.

Das hat bisher anstandslos geklappt.

Der Wechsel auf eine andere Datenbank-Engine wird weiter unten beschrieben.

Voraussetzungen

Es setzt voraus, dass die Daten außerhalb der Docker-Umgebung landen. Das export Volume ist also als Verzeichnis unterhalb der docker-compose.yml so wie in den Beispielen definiert.

1      - ./export:/usr/src/paperless/export

Wie ich ein Datenbank-Upgrade im Detail durchführe, beschreibe ich im Folgenden.

Zuerst sollte sichergestellt sein, dass keine Importe ins Paperless stattfinden. Da ich alleine auf dem System arbeite und keinen E-Mail-Import verwende, kann ich das gut sicherstellen.

Backup

Als ersten Schritt fertige ich ein Backup an. Dies geht wunderbar mit dem bereits erwähnten document_exporter folgendermaßen:

1docker compose exec -T webserver document_exporter ../export -z -zn full-backup

Hier sollte dann eine Datei namens full-backup.zip außerhalb des Docker-Umgebung entstanden sein. Dies enthält alle relevanten Informationen für ein Desaster-Recovery.

Zurück auf Null

Danach fahre ich alle Container herunter und lösche gleichzeitig alle Docker Volumes:

1docker compose down --volumes

Die Ausgabe sollte ungefähr so aussehen:

1[+] down 8/8
2 ✔ Container paperless-webserver-1 Removed   5.6s
3 ✔ Container paperless-broker-1    Removed   0.4s
4 ✔ Container paperless-db-1        Removed   0.3s
5 ✔ Volume paperless_media          Removed   0.2s
6 ✔ Volume paperless_pgdata         Removed   0.0s
7 ✔ Volume paperless_data           Removed   0.0s
8 ✔ Volume paperless_redisdata      Removed   0.0s
9 ✔ Network paperless_default       Removed                     

Neue Version wählen

Nun kann ich in der docker-compose.yml-Datei das Datenbank-Image auf die gewünschte Version der Datenbank hochziehen.

Für PostgreSQL gibt es hier im Repo eine Vorlage an der ich mich orientiert habe. Im Mai 2025 war es die Version 17. Also änderte ich den Eintrag docker-compose.yml von der Version 16.

1  db:
2    image: docker.io/library/postgres:16

auf die Version 17

1  db:
2    image: docker.io/library/postgres:17

Es empfiehlt sich eh ab und zu da mal einzuschauen, ob nicht z.B. eine neue redis-Version unterstützt wird. Ich selbst stütze mich nur auf die Haupt (Major-Versionen). Bei Minor- oder Patch-Updates pulle ich das jeweilige neue Image und starte alle Docker-Container neu, das reicht mir und hat sich bei mir bewährt.

Erster Start

Nach dem Ändern der Major-Version (bzw. des Tags) vom postgres-Image, können die Docker-Container von paperless schon wieder gestartet werden. Sofern das Image lokal noch nicht vorliegt, wird es automatisch heruntergeladen.

1docker compose up -d

Folgendes sollte als Ausgabe zu sehen sein:

 1[+] up 22/22
 2 ✔ Image docker.io/library/postgres:18 Pulled     15.3s
 3 ✔ Network paperless_default           Created    0.0s
 4 ✔ Volume paperless_data               Created    0.0s
 5 ✔ Volume paperless_media              Created    0.0s
 6 ✔ Volume paperless_redisdata          Created    0.0s
 7 ✔ Volume paperless_pgdata             Created    0.0s
 8 ✔ Container paperless-db-1            Created    0.2s
 9 ✔ Container paperless-broker-1        Created    0.2s
10 ✔ Container paperless-webserver-1     Created   

Nun muss solange gewartet, bis die Login-Maske wieder mit dem Browser besuchbar ist.

Da dies einer Neuinstallation entspricht und daher Datenbanken etc. eingerichtet werden müssen, dauert dies je nach Rechenpower etwas. Daher ist etwas Geduld erforderlich.

Zeigt’s im Browser 502 Bad Gateway ist noch nicht alles fertig. Neugierige werfen ein Blick in die Docker-Logs

1docker compose logs -f

Wenn ungefähr folgende Log-Einträge zu sehen sind

 1webserver_1  | [tasks]
 2webserver_1  |   . documents.bulk_edit.delete
 3webserver_1  |   . documents.signals.handlers.send_webhook
 4webserver_1  |   . documents.tasks.bulk_update_documents
 5webserver_1  |   . documents.tasks.check_scheduled_workflows
 6webserver_1  |   . documents.tasks.consume_file
 7webserver_1  |   . documents.tasks.empty_trash
 8webserver_1  |   . documents.tasks.index_optimize
 9webserver_1  |   . documents.tasks.sanity_check
10webserver_1  |   . documents.tasks.train_classifier
11webserver_1  |   . documents.tasks.update_document_content_maybe_archive_file
12webserver_1  |   . paperless_mail.mail.apply_mail_action
13webserver_1  |   . paperless_mail.mail.error_callback
14webserver_1  |   . paperless_mail.tasks.process_mail_accounts
15webserver_1  | 
16webserver_1  | [2025-05-29 12:50:03,792] [INFO] [celery.worker.consumer.connection] Connected to redis://broker:6379//
17webserver_1  | [2025-05-29 12:50:03,982] [INFO] [celery.apps.worker] celery@7e94fdb4f941 ready.

sollte die Login-Maske zur Einrichtung des ersten Benutzerkontos verfügbar sein und es kann mit dem nächsten Schritt weiter gemacht werden.

Paperless-ngx Anmelde-Maske zur Einrichtung des ersten Benutzerkontos

Daten wieder importieren

Über den document_importer lässt sich nun wieder alles importieren: Alle Dokumente, alle Tags, alle Benutzeraccounts, gespeicherten Ansichten usw.

1docker compose exec -T webserver document_importer ../export/full-backup.zip

Ausgabe:

1Checking the manifest
2Installed 4248 object(s) from 1 fixture(s)
3Copy files into paperless...
4100%|██████████| 1014/1014 [00:04<00:00, 210.59it/s]
5Updating search index...
6100%|██████████| 1014/1014 [00:10<00:00, 94.50it/s] 

Falls im Browser Weiterleitung auf den Unterpfad /accounts/signup/?next=%2F stattfand, diesen Pfad löschen! Im Browser also wieder die gewohnte URL ansurfen und es sollte wieder die gewohnte Login-Maske erscheinen.

Fertig, anmelden

Schlussendlich stoße ich noch einmal vorsichtshalber mein Backup-Script von Paperless noch an und bin fertig.

PostgreSQL 18

Während ich diesen Artikel schrieb, stellte ich fest, dass in den Vorlagen im Repo zu docker-compose.yml schon PostgreSQL 18 unterstützt wird. Ich bin daher genau nach meiner Anleitung vorgegangen und musste feststellen, dass der Datenbank-Container nicht nach oben kam.

In den Docker-Logs entdeckte ich dann folgende Meldungen:

 1db-1         | Error: in 18+, these Docker images are configured to store database data in a
 2db-1         |        format which is compatible with "pg_ctlcluster" (specifically, using
 3db-1         |        major-version-specific directory names).  This better reflects how
 4db-1         |        PostgreSQL itself works, and how upgrades are to be performed.
 5db-1         | 
 6db-1         |        See also https://github.com/docker-library/postgres/pull/1259
 7db-1         | 
 8db-1         |        Counter to that, there appears to be PostgreSQL data in:
 9db-1         |          /var/lib/postgresql/data (unused mount/volume)
10db-1         | 
11db-1         |        This is usually the result of upgrading the Docker image without
12db-1         |        upgrading the underlying database using "pg_upgrade" (which requires both
13db-1         |        versions).
14db-1         | 
15db-1         |        The suggested container configuration for 18+ is to place a single mount
16db-1         |        at /var/lib/postgresql which will then place PostgreSQL data in a
17db-1         |        subdirectory, allowing usage of "pg_upgrade --link" without mount point
18db-1         |        boundary issues.
19db-1         | 
20db-1         |        See https://github.com/docker-library/postgres/issues/37 for a (long)
21db-1         |        discussion around this process, and suggestions for how to do so.
22db-1 exited with code 1 (restarting)

Der Downgrade auf 17 war nach der obigen Anleitung recht schnell erledigt und paperless lief wieder zufriedenstellend mit der Version 17 von PostgreSQL.

Umstellung auf MariaDB 12

Ich nahm dies als Anlass, die Datenbank-Engine auf MariaDB umzustellen. Im Repo des Projektes gibt es dazu ebenfalls eine Vorlage. Es gab drei Stellen, die ich dafür anpassen musste.

Es mussten im Abschnitt von db ein paar Umgebungsvariablen und natürlich das image umgestellt werden; hier ist 12 gerade aktuell.

Weiter unten im Abschnitt webserver mussten ebenfalls noch ein paar Umgebungsvariablen hinzugefügt werden.

Als letztes musste ich den Volume-Namen im Abschnitt volumes umbenennen. Danach konnte ich die Container wieder starten und die Daten importieren, wie weiter oben beschrieben.

Nachfolgend ein diff aus meinem ansible-Playbook. (Ja, ich habe restart auch noch gleich umstellt bzw. an die neue Vorlage aus dem Projekt angeglichen. )

 1--- a/roles/paperless/files/docker-compose.yml
 2+++ b/roles/paperless/files/docker-compose.yml
 3@@ -28,26 +28,24 @@
 4 
 5 services:
 6   broker:
 7-    image: docker.io/library/redis:8
 8-    restart: unless-stopped
 9+    image: docker.io/library/redis:7
10+    restart: always
11     volumes:
12       - redisdata:/data
13 
14   db:
15-    image: docker.io/library/mariadb:12
16-    restart: unless-stopped
17+    image: docker.io/library/postgres:17
18+    restart: always
19     volumes:
20-      - dbdata:/var/lib/mysql
21+      - pgdata:/var/lib/postgresql/data
22     environment:
23-      MARIADB_HOST: paperless
24-      MARIADB_DATABASE: paperless
25-      MARIADB_USER: paperless
26-      MARIADB_PASSWORD: paperless
27-      MARIADB_ROOT_PASSWORD: paperless
28+      POSTGRES_DB: paperless
29+      POSTGRES_USER: paperless
30+      POSTGRES_PASSWORD: paperless
31 
32   webserver:
33     image: ghcr.io/paperless-ngx/paperless-ngx:latest
34-    restart: unless-stopped
35+    restart: always
36     depends_on:
37       - db
38       - broker
39@@ -68,13 +66,9 @@ services:
40       - extra.env
41     environment:
42       PAPERLESS_REDIS: redis://broker:6379
43-      PAPERLESS_DBENGINE: mariadb
44       PAPERLESS_DBHOST: db
45-      PAPERLESS_DBUSER: paperless # only needed if non-default username
46-      PAPERLESS_DBPASS: paperless # only needed if non-default password
47-      PAPERLESS_DBPORT: 3306
48 volumes:
49   data:
50   media:
51-  dbdata:
52+  pgdata:
53   redisdata: