Paperless-NGX ist an und für sich schon eine recht performante Lösung. Wenn aber viele Anwenderinnen und Anwender gleichzeitig damit arbeiten sollen, dann kann der Bedarf nach Skalierung der Maschinenressourcen entstehen. Ich habe mir eine Lösung überlegt, die aus drei Paperless-NGX-Webservern mit jeweils den Komponenten Redis, Gotenberg und Tika sowie traefik als Loadbalancer besteht. Damit diese drei Server eine gemeinsame Datenbasis nutzen, liegen die Dokumente auf einem gemeinsam genutzten NFS-Share und die Daten in einer abgesetzten Datenbank. Das System besteht also aus sechs Servern:
Loadbalancer und NFS-Server können aber auch auf einer Maschine laufen.
Paperless-NGX kann mit Postgresql und MariaDB betrieben werden. Empfohlen wird Postgresql, also habe ich hier PostgreSQL verwendet. Ich nutze Ubuntu Linux Server in der Version 24.04 LTS. Mit anderen Linux-Servern z.B. unter Debian sollte das genauso funktionieren. Ich melde mich auf der Konsole des Servers an, um die Befehle auf der Kommandozeile abzusetzen. Zunächst stelle ich sicher, dass das System auf dem aktuellen Stand ist mit
sudo apt update && sudo apt upgrade -y
Dann installiere ich zunächst den Postgresql-Server
sudo apt install postgresql
In der Datei "/etc/postgresql/{PostgreSQL-Version}/main/postgresql.conf" muss ich den Eintrag
listen_addresses = 'localhost'
in
listen_addresses = '*'
ändern, damit der Datenbankserver auf Anfragen aus dem Netz reagiert. Man könnte hier feste IP-Adressen eingeben, aber da die CIDR-Schreibweise für ganze Subnetze (z.B. 192.168.0.0/24) hier nicht funktioniert, gebe ich das System komplett frei und beschränke die Zugriffe später über die Firewall.
Dann muss ich noch in der Datei "/etc/postgresql/{PostgreSQL-Version}/main/pg_hba.conf" die Zeile
hostssl paperless paperless 192.168.0.0/24 scram-sha-256
einfügen, damit der Benutzer paperless, den ich noch anlegen muss, per ssl auf den DB-Server zugreifen darf. Hier funktioniert die CIDR-Schreibweise übrigens. Danach muss der PostgreSQL-Dienst mit
sudo systemctl restart postgresql
neu gestartet werden. Dann melde ich mich auf der lokalen Konsole an dem Datenbankserver an, um den User und die Datenbank anzulegen:
sudo -u postgres psql
und dann auf der Postgresql-Konsole:
CREATE USER paperless with password '123456';
CREATE DATABASE paperless;
\c paperless
GRANT ALL PRIVILEGES ON DATABASE paperless TO paperless;
EXIT;
Hier wird also zunächst der User paperless mit dem (natürlich total unsicheren) Passwort 123456 angelegt. Dann wird die Datenbank paperless angelegt und zur Bearbeitung aufgerufen (\c {Datenbankname}). Schließlich werden dem User paperless alle Rechte auf diese Datenbank eingerichtet. Die Datenbankbefehle enden immer mit einem Semikolon. Groß- und Kleinschreibung ist dabei aber egal.
Danach kann ich mich von dieser Konsole und auch von dem Server wieder abmelden.
Als Loadbalancer wird trafik unter Docker eingesetzt. Auf dem Server, auf dem traefik laufen soll, habe ich dazu Docker und Docker Compose entsprechend meiner Anleitung installiert (s. auch hier: Tutorial-Beitrag zu Docker). Ich melde mich auf der Konsole des Servers an. Dann erstelle ich zunächst ein Verzeichnis traefik im Homeshare und wechsele in selbiges
mkdir traefik && cd traefik
und erstelle darin die Datei rules.yml
nano rules.yml
mit folgendem Inhalt:
http:
routers:
paperless:
rule: "Host(`paperless.local`)"
service: paperless
entryPoints:
- "web"
services:
paperless:
loadBalancer:
sticky:
Cookie:
name: paperless_sticky
servers:
- url: "http://192.168.0.113:8000"
- url: "http://192.168.0.59:8000"
- url: "http://192.168.0.149:8000"
healthCheck:
path: /api/health/
interval: 10s
timeout: 5s
scheme: http
port: 8000
und die Datei docker-compose.yml
nano docker-compose.yml
mit folgendem Inhalt:
services:
traefik:
image: traefik:v2.10
container_name: traefik
command:
- --api.insecure=true
- --providers.docker=false # Docker Provider nicht noetig,
# da Services auf anderen Servern laufen
- --providers.file=true
- --providers.file.filename=/etc/traefik/rules.yml
- --entrypoints.web.address=:80
ports:
- "80:80"
- "8080:8080" # Dashboard (optional)
volumes:
- ./rules.yml:/etc/traefik/rules.yml
networks:
- traefik_net
networks:
traefik_net:
driver: bridge
In der Datei rules.yml wird die Adresse, unter der der Service nachher erreichbar sein soll, definiert. Ich nutze da das hier nur im lokalen Netz läuft, das System ohne SSL-Zertifikate. Außerdem werden hier die Server, auf denen nachher Paperless-NGX laufen wird, mit ihren jeweiligen IP-Adressen aufgelistet. Der Sticky-Teil sorgt dafür, dass man während einer Sitzung auf einem Server bleibt und der Health-Check sorgt dafür, dass traefik nach spätestens 5 Sekunden einen Server, der nicht reagiert, aus dem Spiel nimmt.
Auch die Datei docker-compose.yml ist eigentlich recht überschaubar. Zunächst wird der Service traefik definiert. Hier wähle ich Version 2.10, weil ich damit gute Erfahrungen gemacht habe. Dann erlaube ich Zugriffe ohne Zertifikate. Für den Zugriff auf die Webserver brauche ich hier nicht den lokalen Docker Provider, weil die Webserver ja auf separaten Maschinen laufen. Der Rest ist selbsterklärend. Das kann ich dann schon einmal mit
docker compose up -d
starten und mit
docker ps
oder mit
docker logs traefik
prüfen, ob alles in Ordnung ist. Wenn das der Fall ist, kann ich diese Konsole auch wieder verlassen.
Damit das Gesamtsystem später auch eine einheitliche Dokumentenbasis verwendet, müssen die Verzeichnisse "data" und "media" auf einem gemeinsam genutztem Share liegen. In einer reinen Linux-Umgebung bietet sich hierfür NFS an. Ich nehme also einen frischen Linux-Server mit ausreichend Massenspeicher und melde mich an der Konsole dieses Servers an. Hierauf installiere ich nfs-server mit
sudo apt install nfs-server
Für die Freigabe erzeuge ich zunächst einen Ordner mit
sudo mkdir /mnt/storage
und darauf schon einmal die Unterordner data und media
sudo mkdir /mnt/storage/data
sudo mkdir /mnt/storage/media
Die Webserver werden später mit der Benutzer- und der Gruppen-ID 1000 zugreifen. Darum muss ich die Besitzrechte mit
sudo chown -R 1000:1000 /mnt/storage
anpassen. Nun muss ich noch die Freigaben definieren. Dazu bearbeite ich die Datei /etc/exports
sudo nano /etc/exports
und füge folgende Zeilen am Ende an:
/mnt/storage/data 192.168.0.59(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000)
/mnt/storage/media 192.168.0.59(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000)
/mnt/storage/data 192.168.0.113(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000)
/mnt/storage/media 192.168.0.113(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000)
/mnt/storage/data 192.168.0.149(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000)
/mnt/storage/media 192.168.0.149(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=1000)
Die Zeilenumbrüche sind hier der Darstellung auf diesen Webseiten geschuldet. Natürlich steht hinter "data" und "media" jeweils keine Zeilenschaltung. So werden die Verzeichnisse /mnt/storage/data und /mnt/storage/media für die drei Webserver freigegeben. Danach weise ich den NFS-Server mit
sudo exportfs -ra
an, die neuen Freigaben einzulesen und anzuwenden. Wenn es hier keine Probleme gibt, dann kann ich auch diesen Server wieder verlassen.
Die Bezeichnung Webserver trifft es nicht ganz, denn auf diesen Servern laufen neben den Webservern noch jeweils die Dienste Gotenberg und Tika für die Verarbeitung der Dokumente und die In-Memory-Datenbank redis, die die DB-Zugriffe auf die Postgresql-Datenbank puffert. Ich habe also drei Linux-Server, auf denen bereits Docker und Docker-Compose installiert ist (s.o.). Hier muss ich zunächst die NFS-Freigaben mounten. Dazu muss ich zuerst den NFS-Client mit
sudo apt install nfs-common
installieren. Ich bearbeite dann die Datei /etc/fstab
sudo nano /etc/fstab
und füge folgende Zeilen ein:
192.168.0.140:/mnt/storage/data /home/andreas/paperless-ngx/data nfs rw 0 0
192.168.0.140:/mnt/storage/media /home/andreas/paperless-ngx/media nfs rw 0 0
Dabei steht hier am Anfang die IP-Adresse des NFS-Servers und der Pfad auf die Freigaben. Dann folgt der Pfad für die Freigaben auf den Webservern. Die Freigaben werden jeweils uneingeschränkt zum Lesen und Schreiben eingehängt. Nun muss ich diese Verzeichnisse natürlich noch erstellen mit
mkdir paperless-ngx
cd paperless-nxg
mkdir data
mkdir media
Danach kann ich die geänderten Systemdateien mit
sudo systemctl daemon-reload
neu einlesen und dann die Freigaben einhängen mit
sudo mount -a
Wenn dabei kein Fehler aufgetreten ist, dann sind die NFS-Shares auf den Servern verfügbar. Das kann ich mir mit
df -h
ansehen. Die Ausgabe sollte folgende Zeilen enthalten:
192.168.0.140:/mnt/storage/data XG XM XG X% /home/andreas/paperless-ngx/data
192.168.0.140:/mnt/storage/media XG XM XG X% /home/andreas/paperless-ngx/media
Wobei anstelle der Xe natürlich die tatsächlichen Kapazitäten angezeigt werden. Da ich mich bereits im Verzeichnis paperless-ngx befinde, kann ich hier auch gleich die Datei docker-compose.yml erstellen
nano docker-compose.yml
mit folgendem Inhalt:
services:
webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:latest
restart: unless-stopped
depends_on:
- broker
- gotenberg
- tika
ports:
- "8000:8000"
volumes:
- ./data:/usr/src/paperless/data
- ./media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
environment:
PAPERLESS_DBHOST: 192.168.0.139
PAPERLESS_DBNAME: paperless
PAPERLESS_DBUSER: paperless
PAPERLESS_DBPASS: 123456
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
broker:
image: docker.io/library/redis:7
restart: unless-stopped
volumes:
- redisdata:/data
gotenberg:
image: docker.io/gotenberg/gotenberg:8.7
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
command:
- "gotenberg"
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
tika:
image: docker.io/apache/tika:latest
restart: unless-stopped
volumes:
redisdata:
Hier wird zuerst der Webserver definiert, der auf Port 8000 lauschen soll. Hier sind die zuvor gemounteten Shares als persistende Volumes eingetragen. Dann kommen die Verbindungsparameter für die Datenbank. An der Stelle kann ich noch einmal erwähnen, dass das natürlich kein sicheres Passwort ist. In meinen Produktivumgebungen verwende ich natürlich sicherere Passworte wie z.B. sechs Sterne ;-)
Dann werden im Environment-Teil die Services Redis, Gotenberg und Tika verbunden, die im darauf folgenden Teil definiert werden. Das ist schon alles. Ich starte de Container mit
docker compose up -d
Dann werden die jeweiligen Images gepullt, was ein wenig dauern kann und auch das anschließende Starten der Container kann etwas dauern. Nach dem eigentliche Start der Container, wenn also die Konsole wieder frei gegeben ist, wird der Webserver initialisiert. Das kann auch wiederum etwas dauern. Mit
docker logs paperless-ngx-webserver-1
kann ich zwischendurch nachsehen, ob die Initialisierung abgeschlossen ist und ob alles in Ordnung ist.
Wenn es nun keine Probleme gegeben hat, dann liefert traefik Paperless-NGX unter dem URL http://paperless.local aus. Das kann ich im Browser allerdings nur aufrufen, wenn mein Rechner unter diesem URL die IP-Adresse des traefik-Servers findet. Wenn ich einen lokalen DNS-Server verwende, dann trage ich diese Adresse dort ein. Ansonsten muss ich sie meinem Client bekannt machen. Das geht unter Linux mit einem Eintrag in die Datei /etc/hosts die ich mit
sudo nano /etc/hosts
bearbeite. Dort füge ich ein:
192.168.0.139 paperless.local
Wobei am Anfang natürlich die IP-Adresse des Traefik-Servers steht. Beim ersten Aufruf der Webseite werde ich aufgefordert, die Credentials für den Hauptuser einzugeben.
Paperless-NGX ist nun mit verteilten Kapazitäten verfügbar. Die Last wird auf drei Webserver und einen Datenbankserver aufgeteilt. Die Zahl der Webserver lässt sich steigern. Dabei wendet der Loadbalancer ein sogenanntes Round-Robin-Prinzip an, das heißt, bei jeder Anmeldung wird der Reihe nach der jeweils nächste Server gewählt. Dass das funktioniert, kann man ausprobieren, indem man sich zum Beispiel mit zwei Browsern anmeldet, jeweils Dokumente hochlädt und in den Protokollen der Webserver nachsieht, wer das gerade verarbeitet hat.
Die hier gezeigte Lösung ist geeignet, Umgebungen für eine große Anzahl an Anwenderinnen und Anwender performant bereit zu stellen. Eine Umgebung mit wenigen Anwenderinnen und Anwendern profitiert davon nicht. Das ist angesichts der guten Performancewerte von Paperless-NGX auch nicht notwendig. Die Lösung stellt auch einen einfachen Failover für den Ausfall einzelner Webserver dar.