Skalieren
PostgreSQL
Statt SQLite kann Flowplan eine PostgreSQL-Datenbank nutzen – wenn du ohnehin einen gepflegten Datenbankserver betreibst, mehr Personen gleichzeitig arbeiten oder du mehrere Instanzen im Cluster-Modus laufen lassen willst.
10 Min. Lesezeit
Auf dieser Seite
SQLite oder PostgreSQL?
SQLite bleibt der Standard und für die meisten Instanzen die beste Wahl: ein Container, ein Datenordner, nichts weiter zu betreiben. Eine einzelne Abfrage ist auf SQLite sogar schneller, weil sie im selben Prozess läuft.
PostgreSQL lohnt sich, wenn
- du einen Datenbankserver schon betreibst oder mietest (mit eigener Überwachung, Replikation und Point-in-Time-Recovery) und Flowplan dort einordnen willst,
- regelmäßig mehr als etwa 20 Personen gleichzeitig in Flowplan arbeiten (siehe Dimensionierung),
- du mehrere Flowplan-Instanzen hinter einem Load Balancer betreiben willst – der Cluster-Modus setzt PostgreSQL voraus.
Funktional gibt es keinen Unterschied: Alle Funktionen, die Suche und die Sicherungen arbeiten mit beiden Datenbanken. Wechseln kannst du jederzeit in beide Richtungen, siehe Datenbank wechseln.
Auch mit PostgreSQL braucht Flowplan seinen Datenordner (/app/data): Dort liegen die hochgeladenen Dateien (ohne S3), secrets.key, die Push-Schlüssel und Zwischendateien von Sicherungen. Nur die Datenbank wandert nach PostgreSQL.
Voraussetzungen
- PostgreSQL 16 oder neuer, eine leere Datenbank (oder ein leeres Schema) und eine Rolle, der sie gehört. Superuser-Rechte braucht Flowplan nicht – und sollte sie auch nicht haben, siehe Rolle ohne Superuser-Rechte.
- Locale mit UTF-8, etwa
en_US.utf8oderde_DE.utf8– so ist es im offiziellenpostgres-Image und bei den meisten verwalteten Diensten. Mit der LocaleCfindet die Suche chinesischen und japanischen Text zwar weiterhin richtig, aber deutlich langsamer. - Erweiterung
pg_trgmfür die Volltextsuche. Sie gehört zu jedem normalen PostgreSQL und ist „trusted“: Flowplan installiert sie selbst in ein eigenes Schemaflowplan_ext, wenn die Rolle in der Datenbank Objekte anlegen darf (CREATE-Recht, als Eigentümerin der Datenbank gegeben). Sonst einmal als Administrator:CREATE EXTENSION pg_trgm;. Ohnepg_trgmfunktioniert die Suche weiter, durchsucht aber jedes Mal alle Einträge (das Log sagt das). - Optional
pgvectorfür die semantische KI-Suche. Ab pgvector 0.8 installiert Flowplan sie selbst wiepg_trgm. Vor 0.8 ist die Erweiterung nicht „trusted“ – dann führt ein Administrator in der Datenbank von Flowplan einmalCREATE EXTENSION vector;aus, und Flowplan nutzt sie ab dem nächsten Start. Ohnepgvectorrechnet Flowplan die semantische Suche selbst, wie auf SQLite; abschalten lässt sie sich mitFLOWPLAN_PGVECTOR=off. - Für die Sicherungen
pg_dumpundpg_restoremindestens in der Hauptversion des Servers. Das Docker-Image bringt die Werkzeuge von PostgreSQL 18 mit; sie sichern und spielen auf Servern von Version 16 bis 18 ein. Wer das Image selbst baut, wählt die Version mit--build-arg PG_CLIENT_MAJOR=18. Ohne Docker das Paketpostgresql-clientpassend installieren oder mitFLOWPLAN_PG_BINauf den Ordner mit den Programmen zeigen.
-- als Administrator (Superuser) auf dem PostgreSQL-Server
CREATE ROLE flowplan LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '<Passwort>';
CREATE DATABASE flowplan OWNER flowplan;
-- in der neuen Datenbank die Erweiterungen in ein eigenes Schema:
\c flowplan
CREATE SCHEMA IF NOT EXISTS flowplan_ext AUTHORIZATION flowplan;
CREATE EXTENSION IF NOT EXISTS pg_trgm SCHEMA flowplan_ext;
-- optional, für die semantische Suche (vor pgvector 0.8 nur so):
CREATE EXTENSION IF NOT EXISTS vector SCHEMA flowplan_ext;Rolle ohne Superuser-Rechte
Lass Flowplan mit einer Rolle verbinden, der die Datenbank gehört, die aber kein Superuser ist. Flowplan legt in seiner Datenbank Tabellen, Funktionen und Trigger an und ändert sie – dafür reicht die Eigentümerschaft. Ein Superuser dagegen kann Dateien auf dem Datenbankserver lesen und dort Programme starten (COPY … TO PROGRAM); eine Lücke in Flowplan oder ein Administrator, der sie ausnutzt, käme damit bis auf den Host.
Viele Images und Charts machen den eingestellten Benutzer zum Superuser – das offizielle postgres-Image etwa POSTGRES_USER. Lege dann wie oben eine eigene Rolle an. Die Erweiterungen installiert der Administrator am besten gleich mit, in das Schema flowplan_ext: Flowplan nutzt eine installierte Erweiterung dort, wo sie liegt, und legt pg_trgm (und pgvector ab 0.8) sonst selbst in flowplan_ext an. pgvector vor 0.8 kann nur ein Administrator installieren; ohne läuft die semantische Suche langsamer.
Eine bestehende Rolle, die Superuser ist, stuft ein anderer Superuser zurück:
-- einmalig, solange flowplan noch Superuser ist (als flowplan):
CREATE ROLE admin LOGIN SUPERUSER PASSWORD '<Passwort>';
-- dann als admin:
ALTER ROLE flowplan NOSUPERUSER NOCREATEROLE NOCREATEDB;Datenbank und Tabellen gehören weiter flowplan, und die bisher installierten Erweiterungen bleiben nutzbar. Den Zugang admin hebst du für die Verwaltung auf; Flowplan braucht ihn nicht.
Einrichten
PostgreSQL wählst du mit DATABASE_URL. Beginnt der Wert mit postgres:// oder postgresql://, nutzt Flowplan PostgreSQL; fehlt er, bleibt es bei SQLite.
DATABASE_URL=postgres://flowplan:<Passwort>@db.example.internal:5432/flowplan
# mit TLS zum Datenbankserver:
# DATABASE_URL=postgres://flowplan:<Passwort>@db.example.internal:5432/flowplan?sslmode=requireSonderzeichen im Passwort (@, :, /, # …) müssen in der Adresse prozentkodiert sein, z. B. %40 für @.
Beim ersten Start legt Flowplan alle Tabellen, Indizes und Trigger selbst an. Bei jedem Update ergänzt es, was die neue Version braucht – es löscht nie etwas, und mehrere Instanzen stimmen sich dabei über eine Sperre ab. Ein Update geht also wie gewohnt: neues Image holen, Container neu starten.
Mit Docker Compose
Ein Beispiel für einen einzelnen Container mit PostgreSQL daneben. Für pgvector statt postgres:16 das Image pgvector/pgvector:0.8.0-pg16 nehmen.
services:
flowplan:
image: ghcr.io/yannicschuller/flowplan:latest
env_file: .env
environment:
FLOWPLAN_DATA_DIR: /app/data
DATABASE_URL: postgres://flowplan:${POSTGRES_PASSWORD}@postgres:5432/flowplan
ports:
- "127.0.0.1:3000:3000"
volumes:
- flowplan-data:/app/data
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD}
FLOWPLAN_DB_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./postgres-init.sh:/docker-entrypoint-initdb.d/10-flowplan.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U flowplan -d flowplan"]
interval: 5s
retries: 20
restart: unless-stopped
volumes:
flowplan-data:
postgres-data:POSTGRES_PASSWORD (das Passwort der Rolle flowplan) und POSTGRES_ADMIN_PASSWORD (das des Superusers postgres aus dem Image, nur für die Verwaltung) stehen in der .env neben der compose.yaml (je openssl rand -hex 24). Das Skript postgres-init.sh (öffnet in neuem Tab) aus dem Cluster-Beispiel legt beim ersten Start der Datenbank die Rolle flowplan ohne Superuser-Rechte an, gibt ihr die Datenbank flowplan und installiert die Erweiterungen in flowplan_ext – leg es neben die compose.yaml. Ein Volume aus einer älteren Fassung dieses Beispiels (POSTGRES_USER: flowplan) behält seine Rollen: Dort ist flowplan der Superuser, bis du ihn zurückstufst.
Variablen
| Variable | Standard | Bedeutung |
|---|---|---|
DATABASE_URL | leer (SQLite) | Adresse der PostgreSQL-Datenbank, postgres://… oder postgresql://…. |
DATABASE_SESSION_URL | DATABASE_URL | Eigene Verbindung für Benachrichtigungen, die Leitung im Cluster und pg_dump/pg_restore/psql – nur nötig, wenn DATABASE_URL auf PgBouncer im Modus transaction zeigt (unten). |
FLOWPLAN_PG_SCHEMA | public | Schema innerhalb der Datenbank, falls Flowplan sich die Datenbank mit anderem teilt. Wird angelegt, wenn es fehlt. |
FLOWPLAN_PG_POOL | 10 | Verbindungen je Instanz (1–500). |
FLOWPLAN_PG_STATEMENT_TIMEOUT | 120000 | Höchstdauer einer Abfrage in Millisekunden; 0 = kein Limit. |
FLOWPLAN_PG_LOCK_TIMEOUT | 10000 | So lange wartet eine Abfrage höchstens auf eine Sperre (ms). |
FLOWPLAN_PG_IDLE_TX_TIMEOUT | 60000 | Eine Transaktion, die so lange nichts tut, wird beendet (ms). |
FLOWPLAN_PG_CONNECT_TIMEOUT | 30000 | So lange wartet eine Anfrage auf eine freie Verbindung aus dem Pool (ms). |
FLOWPLAN_PG_PREPARE | an | off schickt jede Abfrage ohne Namen statt als benanntes Prepared Statement – für PgBouncer im Modus transaction vor 1.21 oder ohne max_prepared_statements. |
FLOWPLAN_PG_SCHEMA_AUTO | an | off: Die Instanz legt das Schema nicht selbst an oder aktualisiert es – das übernimmt ein eigener Schritt (node /app/scripts/db-schema.mjs, siehe Cluster-Modus). |
FLOWPLAN_PGVECTOR | an | off nutzt pgvector nicht, auch wenn es installiert ist. |
FLOWPLAN_BACKUP_FORMAT | pg_dump | sqlite: geplante Sicherungen im portablen SQLite-Format statt mit pg_dump (unten). |
FLOWPLAN_PG_BIN | leer | Ordner mit pg_dump, pg_restore und psql, wenn sie nicht im PATH liegen. |
PGCONNECT_TIMEOUT | 15 | So viele Sekunden warten pg_dump, pg_restore und psql höchstens auf die Verbindung zum Server (die Variable von libpq). |
Alle Variablen stehen auch kommentiert in der .env.example (öffnet in neuem Tab) des Repositorys.
Lange Arbeiten wie Sicherungen, Wiederherstellungen und Schema-Änderungen heben die Zeitlimits für sich selbst auf.
Verbindungen: Jede Instanz öffnet bis zu FLOWPLAN_PG_POOL Verbindungen und bis zu zwei eigene (Benachrichtigungen, im Cluster die Wahl der leitenden Instanz). max_connections des Servers muss also mindestens Instanzen × (FLOWPLAN_PG_POOL + 2) plus Reserve für Sicherungen und Administration betragen. Der PostgreSQL-Standard von 100 reicht für eine Instanz mit dem Standardpool bequem.
Suche auf PostgreSQL
Die Suche liefert auf PostgreSQL dieselben Treffer in derselben Reihenfolge wie auf SQLite: Teile von Wörtern („plan“ in „Projektplanung“), chinesischer und japanischer Text ohne Leerzeichen, Umlaute und Akzente, Titel vor Text, dieselben Ausschnitte. Technisch arbeitet sie mit Trigramm-Indizes aus pg_trgm.
Erster Start nach dem Update oder Wechsel: Flowplan baut die beiden Suchindizes beim Öffnen der Datenbank. Auf großen Instanzen kann das einige Minuten dauern; so lange antwortet die Instanz noch nicht. Gib Healthchecks entsprechend Zeit (start_period, startupProbe). Danach füllt sich der Index im Hintergrund – die Suche funktioniert schon und findet mit der Zeit mehr.
Den Suchindex komplett neu aufbauen, etwa nach dem Einspielen von pg_trgm oder wenn Treffer fehlen:
docker compose exec flowplan node /app/scripts/db-search-rebuild.mjsDas Skript liest DATABASE_URL und FLOWPLAN_PG_SCHEMA aus der Umgebung des Containers; die laufende Instanz indexiert danach alles neu (Seiten, Einträge, Kommentare, Nachrichten und Dateien). Auf SQLite funktioniert derselbe Befehl.
Sicherungen mit pg_dump
Auf PostgreSQL schreiben die geplante Sicherung und Jetzt sichern (Administration → Speicher & Sicherung) ein Archiv von pg_dump im Custom-Format: flowplan-2026-11-07-03-00.dump, mit FLOWPLAN_ENCRYPTION_KEY verschlüsselt als .dump.enc. Sonst bleibt alles wie bei SQLite:
- Ziele: S3 unter
<Präfix>/backups/, SFTP oder der Datenordner – mit derselben Aufbewahrung und demselben verzögerten Löschen der Dateien. - Verschlüsselung wie gehabt;
decrypt.mjsentschlüsselt auch.dump.enc. - Das Archiv enthält nur das Schema von Flowplan, ohne Suchindex und
pgvector-Tabelle – beide baut Flowplan nach dem Einspielen neu auf. Es ist dadurch kleiner und lässt sich auch auf einem Server ohnepg_trgmoderpgvectoreinspielen. pg_dumpläuft außerhalb von Flowplan und belastet die Instanz kaum.
Ausweichen auf SQLite: Fehlt ein passendes pg_dump (zu alt oder nicht installiert), schreibt die geplante Sicherung stattdessen eine portable SQLite-Datei (flowplan-….sqlite) und vermerkt das im Log. Mit FLOWPLAN_BACKUP_FORMAT=sqlite ist das immer so. Eine solche Datei lässt sich auf SQLite und PostgreSQL einspielen.
Die Instanzsicherung (Administration → Speicher & Sicherung → Gesamte Instanz → Herunterladen) enthält die Datenbank immer im portablen SQLite-Format und lässt sich deshalb auf beiden Datenbanken wiederherstellen. Litestream läuft mit PostgreSQL nicht – für eine laufende Sicherung mit beliebigem Zeitpunkt nutze die Mittel deines Datenbankservers (WAL-Archivierung, Point-in-Time-Recovery des Anbieters).
Wiederherstellen
- Ein
pg_dump-Archiv auf einer laufenden Instanz (flowplan-….dump,.dump.enc): unter Administration → Speicher & Sicherung → Aus Sicherung wiederherstellen hochladen. Flowplan prüft es sofort und spielt es beim nächsten Neustart ein – im Cluster-Modus die leitende Instanz, siehe Grenzen. Nur die Datenbank wird ersetzt; die Dateien bleiben, wie sie sind. - Auf einer neuen Instanz mit PostgreSQL geht dasselbe auf dem Einrichtungsbildschirm: hochladen oder aus S3 bzw. per SFTP holen, wie unter Beim Einrichten wiederherstellen.
- Eine Instanz mit SQLite lehnt solche Archive ab – dafür gibt es die Instanzsicherung oder den Wechsel der Datenbank.
- Flowplan prüft das Archiv vorher (eine Flowplan-Datenbank, nicht aus einer neueren Version), ersetzt dann das ganze Schema in einer Transaktion und baut den Suchindex neu. Aus dem Archiv kommen dabei nur die Daten der Tabellen von Flowplan; Tabellen, Funktionen und Trigger legt die laufende Version selbst an. Ältere Archive passen so in die aktuellen Tabellen.
- Enthält das Archiv Objekte, die Flowplan nicht selbst anlegt – eigene Funktionen, Views, Trigger oder Regeln im Schema von Flowplan –, lehnt Flowplan es ab: „Die Sicherung enthält Datenbankobjekte, die Flowplan nicht selbst anlegt (etwa Funktionen oder Trigger), und wird deshalb nicht wiederhergestellt.“ Eigene Objekte gehören in ein anderes Schema; das sichert
pg_dumpvon Flowplan nicht mit. Der vorherige Stand bleibt alspre-restore-<Zeit>/flowplan.dumpim Datenordner – plane dafür Platz für ein weiteres Archiv ein. Schlägt das Einspielen fehl, kommt der vorherige Stand zurück. - Ein Archiv aus einem anderen Schema (
FLOWPLAN_PG_SCHEMA) lässt sich ebenfalls einspielen. - Die Instanzsicherung (ZIP) stellst du wie bisher an derselben Stelle wieder her; sie passt auf beide Datenbanken.
PgBouncer
Flowplan hält seine Verbindungen selbst im Pool; eine direkte Verbindung zum Server ist der einfachste Weg. PgBouncer davor sollte so funktionieren – mit einem echten PgBouncer getestet ist es bisher nicht:
- Benannte Prepared Statements: Flowplan nutzt sie standardmäßig. Im Modus
transactionbraucht PgBouncer dafür Version 1.21 oder neuer mitmax_prepared_statementsgrößer als0. SonstFLOWPLAN_PG_PREPARE=offsetzen: Flowplan schickt dann jede Abfrage ohne Namen (kostet rund ein Drittel des Durchsatzes). - Eine eigene Sitzung für Benachrichtigungen, Leitung und Sicherungen:
LISTEN, die Sperre der leitenden Instanz im Cluster undpg_dump,pg_restoreundpsqlbrauchen eine Verbindung, die ihnen allein gehört. Sie nutzenDATABASE_SESSION_URL– zeig damit direkt auf den Server oder auf einen Pool im Modussession. Ohne die Variable nehmen sieDATABASE_URL. - Startparameter
options: Flowplan übergibt Schema (search_path) und Zeitlimits beim Verbindungsaufbau alsoptions. Lehnt PgBouncer das ab, hilftignore_startup_parameters = options– dann muss das Schemapublicsein, und die Zeitlimits setzt du an der Rolle (ALTER ROLE flowplan SET statement_timeout = '120s'usw.).
# Pool über PgBouncer im Modus transaction, eigene Sitzungen direkt zum Server
DATABASE_URL=postgres://flowplan:<Passwort>@pgbouncer:6432/flowplan
DATABASE_SESSION_URL=postgres://flowplan:<Passwort>@postgres:5432/flowplan
# nur ohne max_prepared_statements bzw. vor PgBouncer 1.21:
# FLOWPLAN_PG_PREPARE=offWeiter
- Datenbank wechseln: von SQLite zu PostgreSQL und zurück.
- Cluster-Modus: mehrere Instanzen mit Redis und S3.
- Dimensionierung: wie viele Personen eine Instanz trägt.