Skalieren
Cluster-Modus
Im Cluster-Modus bedienen mehrere Flowplan-Instanzen eine Installation: eine PostgreSQL-Datenbank, ein Redis, ein S3-Bucket und beliebig viele Instanzen hinter einem Load Balancer – für mehr gleichzeitig aktive Personen und Updates ohne Unterbrechung.
10 Min. Lesezeit
Auf dieser Seite
Brauchst du ihn?
Für die meisten Teams nicht: Ein einzelner Container trägt mit SQLite etwa 20, mit PostgreSQL etwa 30 gleichzeitig aktive Personen (siehe Dimensionierung). Der Cluster-Modus lohnt sich, wenn regelmäßig mehr Personen gleichzeitig arbeiten oder wenn Flowplan bei Updates und beim Ausfall eines Servers weiterlaufen soll. Er bringt dafür drei Dienste mit, die du betreiben (oder mieten) und sichern musst.
Voraussetzungen
Eingeschaltet wird er mit FLOWPLAN_CLUSTER=1. Dann sind Pflicht:
| Was | Variable | Wofür |
|---|---|---|
| PostgreSQL 16+ | DATABASE_URL=postgres://… | Alle Daten. SQLite ist eine Datei auf einem Host und geht nicht. Siehe PostgreSQL. |
| Redis (getestet mit 7) | REDIS_URL=redis://… oder rediss://… (TLS) | Live-Bearbeitung, Präsenz und Rate-Limits zwischen den Instanzen. Nichts davon muss Redis dauerhaft speichern. |
| S3-Speicher | S3_BUCKET, S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | Alle Dateien; der lokale Ordner uploads/ ist nur noch ein Zwischenspeicher. Siehe Speicher, S3 und Sicherung. |
Fehlt eines davon, startet eine Instanz nicht und nennt im Log alle Probleme auf einmal. REDIS_URL ohne Cluster-Modus wird mit einer Warnung ignoriert.
Ebenfalls Pflicht – auch hier startet eine Instanz ohne sie nicht: Schlüssel, die auf allen Instanzen gleich sind. Ein einzelner Container legt manche Schlüssel selbst im Datenordner an – im Cluster hat jede Instanz ihren eigenen Ordner, und jede würde andere erzeugen. Setze deshalb überall dieselben Werte:
FLOWPLAN_ENCRYPTION_KEYoderFLOWPLAN_SECRETS_KEY(oder ihre Varianten…_FILE, etwa für Docker-Secrets) – sonst kann eine Instanz gespeicherte Geheimnisse und Signaturen der anderen nicht lesen (Zwei-Faktor-Anmeldung, KI-Schlüssel, entsperrte Freigabelinks). Siehe Gespeicherte Geheimnisse.WEB_PUSH_PUBLIC_KEYundWEB_PUSH_PRIVATE_KEY(ein VAPID-Schlüsselpaar, z. B. ausnpx web-push generate-vapid-keys) – sonst passen Push-Abos nur zu einer Instanz. Siehe Push-Benachrichtigungen.
Kommst du von einem einzelnen Container ohne diese Variablen, übernimm die Werte aus seinem Datenordner, damit gespeicherte Geheimnisse und Push-Abos gültig bleiben: den Inhalt von secrets.key als FLOWPLAN_SECRETS_KEY (zusätzlich zu einem FLOWPLAN_ENCRYPTION_KEY, beide werden gelesen) und publicKey/privateKey aus web-push-keys.json als WEB_PUSH_PUBLIC_KEY/WEB_PUSH_PRIVATE_KEY.
Was die Instanzen teilen
- Live-Bearbeitung und Präsenz: Änderungen an Dokumenten, Datensätzen und Whiteboards, Cursor und „wer ist gerade hier“ gehen über Redis an alle Instanzen. Zwei Personen auf verschiedenen Instanzen arbeiten also gemeinsam wie auf einer. Die Daten selbst stehen in PostgreSQL; geht eine Nachricht verloren, holt der Browser den Stand beim nächsten Abgleich nach. Sticky Sessions braucht es nicht.
- Rate-Limits: Grenzen etwa für KI-Anfragen zählen über alle Instanzen (über Redis). Grenzen, die schon in der Datenbank stehen (fehlgeschlagene Anmeldungen, Demos, MCP), galten ohnehin für alle.
- Aufgaben der leitenden Instanz: Genau eine Instanz ist jeweils die leitende (bestimmt über eine Sperre in PostgreSQL). Nur sie erledigt die zeitgesteuerten Aufgaben: Erinnerungen, Automationen, Seitenprüfungen, Suchindex, Texterkennung von Dateien, Aufräumen und Aufbewahrung, Zusammenfassungen, geplante Sicherungen und Markdown-Exporte, LDAP-, Kalender- und Issue-Abgleich, E-Mail-Eingang, Telegram, KI-Hintergrundarbeit und die Warnungen der Überwachung. Fällt sie aus, übernimmt innerhalb weniger Sekunden eine andere.
- Warteschlangen: E-Mails, Push, Webhooks und Chat-Apps verschickt jede Instanz; jede Nachricht wird genau einmal von einer Instanz übernommen.
- Dateien: Eine hochgeladene Datei landet zuerst im Zwischenspeicher der Instanz, die sie angenommen hat, und diese lädt sie sofort in den Bucket (meist in deutlich unter einer Sekunde). Andere Instanzen holen sie bei Bedarf aus dem Bucket und warten dafür bis zu 5 Sekunden.
Gesundheit, Bereitschaft und Herunterfahren
| Adresse | Antwortet | Gedacht für |
|---|---|---|
/api/health/live | 200, solange der Prozess antwortet – ohne Datenbank-Prüfung, denn ein Neustart behebt keinen Datenbankausfall | Liveness-Probe |
/api/health/ready | 200, wenn Datenbank, Redis und Bucket erreichbar sind; sonst 503, ebenso beim Herunterfahren | Readiness-Probe, Load Balancer |
/api/health | wie bisher, siehe Healthcheck | Docker-Healthcheck, Apps |
Ohne Token antwortet /api/health/ready nur mit ok/error je Prüfung; mit FLOWPLAN_METRICS_TOKEN als Bearer-Token kommen Antwortzeiten und Fehlermeldungen dazu.
Welche Prüfungen über die Bereitschaft entscheiden, legt FLOWPLAN_READY_REQUIRE fest – eine Liste aus database, redis und objectStorage, Standard: alle drei. Die Datenbank zählt immer. Fällt Redis aus, werden mit dem Standard alle Instanzen zugleich „nicht bereit“, und der Load Balancer hat nichts mehr. Mit FLOWPLAN_READY_REQUIRE=database,objectStorage bleiben sie bereit: Bearbeiten und Speichern funktionieren weiter, nur Live-Änderungen zwischen den Instanzen kommen erst beim nächsten Abgleich an, und Rate-Limits zählt jede Instanz für sich.
Herunterfahren: Auf SIGTERM meldet die Instanz sofort 503 auf /api/health/ready, schließt die Live-Verbindungen (die Browser verbinden sich über den Load Balancer mit einer anderen Instanz), beantwortet offene Anfragen, gibt die Leitung ab und meldet sich bei den anderen ab. Gib ihr dafür 30 Sekunden (stop_grace_period, terminationGracePeriodSeconds).
Schema vor dem Start
Jede Instanz bringt das Datenbankschema beim Start selbst auf ihren Stand (unter einer Sperre, sie warten aufeinander). Sauberer ist ein eigener Schritt vor dem Start der Instanzen, der bei einem Fehler laut abbricht:
DATABASE_URL=postgres://… node /app/scripts/db-schema.mjsDann FLOWPLAN_PG_SCHEMA_AUTO=off für die Instanzen setzen. Das Docker-Compose-Beispiel und das Helm-Chart machen es so. Das Schema wird nur ergänzt, nie verkleinert: Während eines Rolling Updates laufen alte und neue Instanzen nebeneinander.
Docker Compose
Ein vollständiges Beispiel liegt unter deploy/compose/cluster (öffnet in neuem Tab): PostgreSQL 16, Redis 7 mit Passwort und ohne Speicherung, MinIO als S3 samt Bucket-Anlage, der Schema-Schritt, zwei Flowplan-Instanzen und Caddy als Load Balancer. Es ist zum Ausprobieren auf einem Host gedacht und ein Ausgangspunkt; für den Betrieb gehören Datenbank, Redis und Speicher auf eigene, gesicherte Dienste.
git clone https://github.com/yannicschuller/flowplan.git
cd flowplan/deploy/compose/cluster
cp .env.example .env # jeden Wert „change-me“ ersetzen
docker compose up -dDarin:
.enventhältAPP_URL,FLOWPLAN_SETUP_TOKENund die Passwörter für PostgreSQL (POSTGRES_PASSWORDundPOSTGRES_ADMIN_PASSWORD), Redis und MinIO – nur Platzhalter, die du ersetzt. Pflicht sind außerdemFLOWPLAN_ENCRYPTION_KEY,WEB_PUSH_PUBLIC_KEYundWEB_PUSH_PRIVATE_KEY– in der.env.exampleleer; ohne sie brichtdocker compose upmit einem Hinweis ab (siehe Voraussetzungen).- Der Dienst
schemaläuft einmal vor den Instanzen (node /app/scripts/db-schema.mjs); die Instanzen habenFLOWPLAN_PG_SCHEMA_AUTO=off. flowplan-1undflowplan-2haben je ein eigenes Volume als Zwischenspeicher,FLOWPLAN_PG_POOL=20und einen Healthcheck auf/api/health/ready. Mehr Instanzen: den Dienstflowplan-2kopieren und imCaddyfileergänzen.- Caddy verteilt reihum auf bereite Instanzen (Health-Check auf
/api/health/ready) und reicht Live-Verbindungen ungepuffert durch (flush_interval -1). Für HTTPS:8080imCaddyfiledurch deinen Hostnamen ersetzen. - PostgreSQL läuft mit
max_connections=200. Flowplan verbindet sich als Rolleflowplan, der die Datenbank gehört, die aber kein Superuser ist:POSTGRES_PASSWORDist ihr Passwort,POSTGRES_ADMIN_PASSWORDdas des Superuserspostgresaus dem Image, nur für die Verwaltung. Beim ersten Start der Datenbank legtpostgres-init.shdie Rolle an und installiertpg_trgm(undpgvector, wenn das Image es hat) inflowplan_ext. Ein Volume aus einer älteren Fassung des Beispiels behält seine Rollen – dort istflowplannoch Superuser, bis du ihn zurückstufst.
Ein anderer Load Balancer (nginx, Traefik, HAProxy) braucht dasselbe: Health-Check auf /api/health/ready, keine Pufferung für Server-Sent Events (/api/documents/live und die Cursor-Kanäle), lange Lese-Timeouts, Uploads bis 110 MB und FLOWPLAN_TRUSTED_PROXIES passend zur Zahl der Proxys.
Kubernetes (Helm)
Das Chart unter deploy/helm/flowplan (öffnet in neuem Tab) hat den Cluster-Modus eingebaut (cluster.enabled=true). PostgreSQL, Redis und S3 legt es nicht an – nimm verwaltete Dienste oder eigene Charts. Die Verbindungsadressen stehen nur in Secrets, die du selbst anlegst:
kubectl create namespace flowplan
kubectl -n flowplan create secret generic flowplan-cluster \
--from-literal=DATABASE_URL='postgres://flowplan:<Passwort>@postgres:5432/flowplan' \
--from-literal=REDIS_URL='redis://:<Passwort>@redis-master:6379/0'
kubectl -n flowplan create secret generic flowplan-env \
--from-literal=S3_ACCESS_KEY_ID='<Key-ID>' \
--from-literal=S3_SECRET_ACCESS_KEY='<Secret>'
# die Schlüssel, die alle Pods teilen (Push-Schlüssel: npx web-push generate-vapid-keys)
kubectl -n flowplan create secret generic flowplan-keys \
--from-literal=FLOWPLAN_ENCRYPTION_KEY="$(openssl rand -base64 32)" \
--from-literal=WEB_PUSH_PUBLIC_KEY='<öffentlicher VAPID-Schlüssel>' \
--from-literal=WEB_PUSH_PRIVATE_KEY='<privater VAPID-Schlüssel>'
helm install flowplan deploy/helm/flowplan -n flowplan \
--set appUrl=https://flowplan.example.org \
--set existingSecret=flowplan-env \
--set cluster.enabled=true \
--set cluster.replicas=3 \
--set cluster.database.existingSecret=flowplan-cluster \
--set cluster.redis.existingSecret=flowplan-cluster \
--set cluster.keys.existingSecret=flowplan-keys \
--set s3.bucket=flowplan --set s3.endpoint=https://s3.example.orgWas das Chart im Cluster-Modus tut:
- Schema-Job als
pre-install/pre-upgrade-Hook (node /app/scripts/db-schema.mjs), bevor Pods einer neuen Version starten. Ohne ihn (cluster.migration.enabled=false) erledigen die Pods das beim Start. - Rolling Updates ohne Unterbrechung (
maxUnavailable: 0), ein PodDisruptionBudget (cluster.podDisruptionBudget.minAvailable, Standard 1) und Pods möglichst auf verschiedenen Knoten. - Probes: Liveness auf
/api/health/live, Readiness auf/api/health/ready; vor dem Stoppen wartet ein Podcluster.preStopSeconds(5), damit der Load Balancer ihn herausnimmt. - Kein Volume:
/app/dataist einemptyDirje Pod (cluster.cacheSizeLimit, 5 Gi); Dateien liegen im Bucket, Sicherungen macht PostgreSQL (pg_dump). - Datenbankrolle: Die Rolle in
DATABASE_URLsollte die Datenbank besitzen, aber kein Superuser sein. Viele PostgreSQL-Charts machen ihren Benutzer zum Superuser – lege dann eine eigene Rolle an, siehe Rolle ohne Superuser-Rechte. - Verbindungen:
cluster.poolSize(10) je Pod plus zwei eigene;replicas × (poolSize + 2)muss untermax_connectionsdes Servers bleiben. - Gemeinsame Schlüssel: Aus dem Secret in
cluster.keys.existingSecretbekommt jeder PodFLOWPLAN_ENCRYPTION_KEY,WEB_PUSH_PUBLIC_KEYundWEB_PUSH_PRIVATE_KEY. Heißen die Schlüssel im Secret anders, nennst du sie mitcluster.keys.encryptionKey,cluster.keys.webPushPublicKeyundcluster.keys.webPushPrivateKey. Bewahre eine Kopie des Verschlüsselungsschlüssels getrennt von den Sicherungen auf. - Das Chart verweigert die Installation, wenn Datenbank, Redis, Bucket oder
cluster.keys.existingSecretfehlen.
Mit ingress-nginx für die Live-Verbindungen die Annotationen nginx.ingress.kubernetes.io/proxy-buffering: "off" und nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" setzen, für Uploads nginx.ingress.kubernetes.io/proxy-body-size: 110m. Alle Werte erklärt das README des Charts.
Administration → Überwachung → System
Der Abschnitt System unter Administration → Überwachung zeigt, wie die Installation läuft:
- Betriebsart (einzelner Container oder Cluster mit Zahl der Instanzen), Datenbank mit Version und Erreichbarkeit,
- Verbindungen dieser Instanz (offen, frei, wartend) und die Zeitlimits,
- Redis mit Version und gesendeten, empfangenen und fehlgeschlagenen Nachrichten, Objektspeicher mit Bucket und Erreichbarkeit,
- die leitende Instanz und seit wann sie leitet,
- die gemeinsamen Warteschlangen (Dateien, E-Mail, Push, Webhooks, Chat-Apps),
- im Cluster eine Tabelle aller Instanzen mit Status (leitend, bereit, fährt herunter), belegten und wartenden Verbindungen, Arbeitsspeicher und letzter Meldung. Eine Instanz, die sich 30 Sekunden nicht meldet, verschwindet aus der Liste.
„Wartend“ bei den Verbindungen heißt: Anfragen warten auf eine freie Datenbankverbindung – mehr FLOWPLAN_PG_POOL oder mehr Instanzen.
Weitere Variablen
| Variable | Standard | Bedeutung |
|---|---|---|
FLOWPLAN_CLUSTER | leer | 1 (oder true, on) schaltet den Cluster-Modus ein. Gleichwertig: FLOWPLAN_MODE=cluster. |
REDIS_URL | leer | redis://:<Passwort>@host:6379/0, mit TLS rediss://…. Benutzer, Passwort und Datenbanknummer stehen in der Adresse. |
FLOWPLAN_READY_REQUIRE | database,redis,objectStorage | Prüfungen, die über /api/health/ready entscheiden (oben). |
FLOWPLAN_INSTANCE_ID | Hostname + Zufall | Name der Instanz in der Übersicht und in application_name der Datenbankverbindungen. Das Helm-Chart nimmt den Pod-Namen. |
FLOWPLAN_REDIS_PREFIX | flowplan: | Präfix für Schlüssel und Kanäle, wenn sich mehrere Installationen ein Redis teilen. |
FLOWPLAN_LEADER_INTERVAL | 5000 | Wie oft (ms) die Instanzen die Leitung prüfen; bestimmt, wie schnell eine ausgefallene leitende Instanz ersetzt wird. |
Workflows im Cluster
Die Schritte von Workflows stehen in einer Warteschlange in der Datenbank. Jede Instanz holt sich fällige Schritte mit einer Lease (2 Minuten); ein Fencing-Token sorgt dafür, dass eine Instanz, deren Lease abgelaufen ist, nichts mehr festschreibt. Fällt eine Instanz aus, übernimmt eine andere ihre Schritte nach Ablauf der Lease. Neue Schritte wecken die anderen Instanzen über Redis; ohne Bus prüfen sie im Takt. Zeitpläne und Datums-Auslöser laufen nur auf der Leader-Instanz und genau einmal je Termin. FLOWPLAN_WORKFLOW_WORKERS legt fest, wie viele Schritte eine Instanz gleichzeitig ausführt.
Grenzen
- Dateien lädt die Instanz hoch, die sie angenommen hat. Bis dahin – normalerweise deutlich unter einer Sekunde – warten andere Instanzen bis zu 5 Sekunden. Einige Wege lesen den lokalen Ordner direkt (Exporte und Archive, manche Importe, Kopien beim Veröffentlichen) und können eine gerade auf einer anderen Instanz hochgeladene Datei noch verpassen. Fällt eine Instanz aus, bevor sie hochgeladen hat, ist die Datei verloren.
- Wiederherstellen und Instanzumzug mit einer Instanz. Eine hochgeladene Sicherung liegt im Zwischenspeicher der Instanz, die sie angenommen hat, und wird erst eingespielt, wenn diese leitet. Das Einspielen ersetzt alle Tabellen in einer Transaktion; Anfragen anderer Instanzen warten so lange und schlagen nach
FLOWPLAN_PG_LOCK_TIMEOUTfehl. Im Kubernetes geht einemptyDirbeim Neustart verloren. Daher: vorher auf eine Instanz herunterskalieren (cluster.replicas=1bzw. nurflowplan-1), wiederherstellen, dann wieder hochskalieren. Dasselbe gilt für das Umzugswerkzeug. - Redis ist ein Single Point: Fällt es aus, funktionieren Bearbeiten und Speichern weiter, aber Live-Änderungen zwischen den Instanzen kommen erst beim nächsten Abgleich an, Rate-Limits zählt jede Instanz für sich, und mit dem Standard von
FLOWPLAN_READY_REQUIREwerden alle Instanzen „nicht bereit“ (oben). - Kein Redis Sentinel und kein Redis Cluster: Flowplan spricht mit genau einer Redis-Adresse. Ein verwalteter Redis mit fester Adresse funktioniert.
- Pro Instanz bleiben die Fortschrittsanzeigen von Wiederherstellen und Umzug sowie die Rückstandszahlen des Suchindex.
Weiter
- Dimensionierung: wie viele Instanzen und welche Hardware.
- Datenbank wechseln: eine bestehende Instanz nach PostgreSQL bringen. Für den Cluster vorher S3 im einzelnen Container einschalten und warten, bis alle Dateien im Bucket sind (Administration → Speicher & Sicherung, oder
objectStorage.pendingauf0unter/api/health).