Zum Inhalt
flowplanDokumentationCommunityMarketplaceZur Startseite

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
  1. Brauchst du ihn?
  2. Voraussetzungen
  3. Was die Instanzen teilen
  4. Gesundheit, Bereitschaft und Herunterfahren
  5. Schema vor dem Start
  6. Docker Compose
  7. Kubernetes (Helm)
  8. Administration → Überwachung → System
  9. Weitere Variablen
  10. Workflows im Cluster
  11. Grenzen
  12. Weiter

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:

WasVariableWofü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-SpeicherS3_BUCKET, S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEYAlle 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_KEY oder FLOWPLAN_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_KEY und WEB_PUSH_PRIVATE_KEY (ein VAPID-Schlüsselpaar, z. B. aus npx 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

AdresseAntwortetGedacht für
/api/health/live200, solange der Prozess antwortet – ohne Datenbank-Prüfung, denn ein Neustart behebt keinen DatenbankausfallLiveness-Probe
/api/health/ready200, wenn Datenbank, Redis und Bucket erreichbar sind; sonst 503, ebenso beim HerunterfahrenReadiness-Probe, Load Balancer
/api/healthwie bisher, siehe HealthcheckDocker-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.mjs

Dann 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 -d

Darin:

  • .env enthält APP_URL, FLOWPLAN_SETUP_TOKEN und die Passwörter für PostgreSQL (POSTGRES_PASSWORD und POSTGRES_ADMIN_PASSWORD), Redis und MinIO – nur Platzhalter, die du ersetzt. Pflicht sind außerdem FLOWPLAN_ENCRYPTION_KEY, WEB_PUSH_PUBLIC_KEY und WEB_PUSH_PRIVATE_KEY – in der .env.example leer; ohne sie bricht docker compose up mit einem Hinweis ab (siehe Voraussetzungen).
  • Der Dienst schema läuft einmal vor den Instanzen (node /app/scripts/db-schema.mjs); die Instanzen haben FLOWPLAN_PG_SCHEMA_AUTO=off.
  • flowplan-1 und flowplan-2 haben je ein eigenes Volume als Zwischenspeicher, FLOWPLAN_PG_POOL=20 und einen Healthcheck auf /api/health/ready. Mehr Instanzen: den Dienst flowplan-2 kopieren und im Caddyfile ergä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 :8080 im Caddyfile durch deinen Hostnamen ersetzen.
  • PostgreSQL läuft mit max_connections=200. Flowplan verbindet sich als Rolle flowplan, der die Datenbank gehört, die aber kein Superuser ist: POSTGRES_PASSWORD ist ihr Passwort, POSTGRES_ADMIN_PASSWORD das des Superusers postgres aus dem Image, nur für die Verwaltung. Beim ersten Start der Datenbank legt postgres-init.sh die Rolle an und installiert pg_trgm (und pgvector, wenn das Image es hat) in flowplan_ext. Ein Volume aus einer älteren Fassung des Beispiels behält seine Rollen – dort ist flowplan noch 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.org

Was 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 Pod cluster.preStopSeconds (5), damit der Load Balancer ihn herausnimmt.
  • Kein Volume: /app/data ist ein emptyDir je Pod (cluster.cacheSizeLimit, 5 Gi); Dateien liegen im Bucket, Sicherungen macht PostgreSQL (pg_dump).
  • Datenbankrolle: Die Rolle in DATABASE_URL sollte 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 unter max_connections des Servers bleiben.
  • Gemeinsame Schlüssel: Aus dem Secret in cluster.keys.existingSecret bekommt jeder Pod FLOWPLAN_ENCRYPTION_KEY, WEB_PUSH_PUBLIC_KEY und WEB_PUSH_PRIVATE_KEY. Heißen die Schlüssel im Secret anders, nennst du sie mit cluster.keys.encryptionKey, cluster.keys.webPushPublicKey und cluster.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.existingSecret fehlen.

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

VariableStandardBedeutung
FLOWPLAN_CLUSTERleer1 (oder true, on) schaltet den Cluster-Modus ein. Gleichwertig: FLOWPLAN_MODE=cluster.
REDIS_URLleerredis://:<Passwort>@host:6379/0, mit TLS rediss://…. Benutzer, Passwort und Datenbanknummer stehen in der Adresse.
FLOWPLAN_READY_REQUIREdatabase,redis,objectStoragePrüfungen, die über /api/health/ready entscheiden (oben).
FLOWPLAN_INSTANCE_IDHostname + ZufallName der Instanz in der Übersicht und in application_name der Datenbankverbindungen. Das Helm-Chart nimmt den Pod-Namen.
FLOWPLAN_REDIS_PREFIXflowplan:Präfix für Schlüssel und Kanäle, wenn sich mehrere Installationen ein Redis teilen.
FLOWPLAN_LEADER_INTERVAL5000Wie 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_TIMEOUT fehl. Im Kubernetes geht ein emptyDir beim Neustart verloren. Daher: vorher auf eine Instanz herunterskalieren (cluster.replicas=1 bzw. nur flowplan-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_REQUIRE werden 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.pending auf 0 unter /api/health).
War das hilfreich?
JaDanke!
Nein – sag uns, was fehlt

Fehler gefunden oder etwas fehlt?Auf GitHub bearbeiten