Zum Hauptinhalt springen

Konfigurationsreferenz

sol2flow liest seine Konfiguration aus Umgebungsvariablen, im Docker-Compose-Setup aus der Datei .env neben compose.yml. Die App prüft sie beim Start und startet mit einer klaren Meldung nicht, wenn eine Pflichtvariable fehlt oder ungültig ist. Ändern Sie einen Wert und übernehmen Sie ihn dann mit docker compose up -d.

Die meisten Einstellungen sind keine Umgebungsvariablen: Anmeldemethoden, Selbstregistrierung, Upload-Limits und Benachrichtigungs-E-Mails legen Sie in der App unter Admin fest (Instanz-Einstellungen).

Secrets wie AUTH_SECRET oder DATABASE_URL können auch aus Dateien kommen, etwa aus Docker Secrets (Secrets aus Dateien).

Pflichtangaben​

VariableBeschreibung
APP_URLDie öffentliche Adresse der Instanz, z. B. https://flow.example.com. Wird für Links in E-Mails, Weiterleitungen bei der Anmeldung und Passkeys verwendet. Muss exakt dem entsprechen, was Personen eingeben.
DATABASE_URLPostgreSQL-Verbindungsstring, z. B. postgresql://sol2flow:secret@db:5432/sol2flow?schema=public. PostgreSQL 16, 17 oder 18. Die Beispiel-compose.yml baut ihn aus POSTGRES_PASSWORD zusammen.
AUTH_SECRETMindestens 16 Zeichen; verwenden Sie 32 zufällige Bytes: openssl rand -base64 32. Schlüssel für interne Signaturen (zum Beispiel die anonymisierten Zähler der Ratenbegrenzung).
APP_ENCRYPTION_KEYMindestens 32 Zeichen; verwenden Sie openssl rand -base64 32. Verschlüsselt Secrets, die in der Datenbank gespeichert sind. Nie verlieren und nie leichtfertig ändern. Siehe Verschlüsselungsschlüssel rotieren.

Allgemein​

VariableStandardBeschreibung
LOG_LEVELinfotrace, debug, info, warn, error oder fatal. Logs sind JSON-Zeilen auf der Standardausgabe, mit LOG_FILE auch in einer Datei (unten).
RUN_MIGRATIONStrueDatenbankmigrationen beim Start anwenden. Setzen Sie false nur, wenn Sie sie auf anderem Weg ausführen.
TRUST_PROXY_HOPS1Wie viele Reverse-Proxys vor der App etwas an X-Forwarded-For anhängen (1–10). Bestimmt, welche Adresse als die des Clients gilt. Siehe Reverse-Proxy und TLS.
PUBLIC_WEBSITE_URLhttps://sol2flow.comWohin der Hinweis „Geteilt mit sol2flow“ in der Fußzeile eines öffentlichen Board-Links führt (http oder https; öffnet sich in einem neuen Tab ohne Referrer). Tragen Sie Ihre eigene Website ein oder lassen Sie den Wert leer, um den Text ohne Link anzuzeigen.
ROLE(leer)web = in diesem Container keine Hintergrundjobs ausführen; wird mit einem separaten Worker-Container verwendet (Installation).

Datenbankverbindungen​

Jeder App-Container hält zwei Pools mit PostgreSQL-Verbindungen: einen für Seiten, Aktionen und die API und einen kleineren für die Warteschlange der Hintergrundjobs. Ihre Größe gilt pro Container:

VariableStandardBeschreibung
DATABASE_POOL_SIZE10; 5 mit ROLE=workerVerbindungen für Seiten, Aktionen, die API und die Jobs. Ein connection_limit in DATABASE_URL (oder im Secret DATABASE_URL_FILE) hat Vorrang.
DATABASE_JOBS_POOL_SIZE5; 2 mit ROLE=web, der Jobs nur einreihtVerbindungen für die Job-Warteschlange.

Ist ein Pool voll, wartet eine Anfrage auf eine freie Verbindung (bis zu 10 Sekunden), statt eine weitere zu öffnen. In der Datenbank erscheinen die Verbindungen als sol2flow-web, sol2flow-worker oder sol2flow-all (ein Container ohne ROLE), die der Job-Warteschlange mit angehängtem -jobs. So sehen Sie, wer wie viele hält:

SELECT application_name, state, count(*) FROM pg_stat_activity WHERE datname = 'sol2flow' GROUP BY 1, 2;

Alles, was sich verbindet, zählt gegen max_connections des Datenbankservers: standardmäßig 100, davon 3 für Superuser reserviert; andere Datenbanken auf demselben Server zählen mit.

AufbauVerbindungen höchstens
Ein Container (ohne ROLE), die Standard-compose.yml10 + 5 = 15
Ein Container mit ROLE=webje 10 + 2 = 12
Ein Container mit ROLE=workerje 5 + 5 = 10
Migrationen beim Start, Kommandozeilen-Tools, Backups, psqlje 1–2, solange sie laufen
Ein Rolling Update, das den neuen Container zuerst startetdie aktualisierten Container doppelt, bis die alten stoppen

Web und Worker auf einem Server nutzen 22 Verbindungen, höchstens 44, während beide ersetzt werden – weit unter dem Standardlimit. Mehr Container oder größere Pools addieren sich: Halten Sie die Summe, verdoppelt, wenn Ihre Updates neue Container vor dem Stoppen der alten starten, unter max_connections minus 3. Steht im Log sorry, too many clients already, verkleinern Sie die Pools oder erhöhen Sie max_connections in PostgreSQL.

Anmeldeort​

sol2flow weist Personen auf ungewöhnliche Anmeldungen hin: ein neues Gerät, ein neues Land oder eine Anmeldung aus einem anderen Land kurz nach einer anderswo. Für das Land braucht es den ungefähren Ort jeder Anmeldung. Ohne Quelle für den Ort funktionieren die Hinweise trotzdem, vergleichen aber Geräte und Netzwerke statt Länder.

VariableStandardBeschreibung
GEO_HEADERS(leer)cloudflare = den Ort aus den Headern von Cloudflare lesen (CF-IPCountry, cf-ipcity, cf-region). Nur, wenn jeder Zugriff über Cloudflare zur App kommt.
GEOIP_DB(leer)Pfad im Container zu einer MaxMind-GeoLite2-City- oder -Country-Datei oder einer DB-IP-Lite-Datei (.mmdb), zum Beispiel /data/geoip/GeoLite2-City.mmdb.

Sind beide gesetzt, gewinnen die Cloudflare-Header, und die Datei springt bei Anfragen ohne sie ein. Gespeichert werden nur Land und Stadt, nie die IP-Adresse: siehe Was gespeichert wird.

Hinter Cloudflare​

Läuft Ihre Instanz hinter Cloudflare — als Proxy (orange Wolke) oder über einen Cloudflare Tunnel —, kann Cloudflare jeder Anfrage den Ort mitgeben:

  1. Öffnen Sie im Cloudflare-Dashboard Ihre Domain und prüfen Sie, dass Network → IP Geolocation eingeschaltet ist (das ist es standardmäßig). Das ergänzt das Land (CF-IPCountry).
  2. Für die Stadt gehen Sie zu Rules → Settings, suchen Managed Transforms und schalten Add visitor location headers ein. Das ergänzt unter anderem cf-ipcity und cf-region.
  3. Setzen Sie GEO_HEADERS=cloudflare in der .env und übernehmen Sie es mit docker compose up -d.
Warnung

Setzen Sie GEO_HEADERS=cloudflare nur, wenn die App auf keinem anderen Weg erreichbar ist. Einen CF-IPCountry-Header kann jeder schicken; nur Cloudflare vor der App garantiert, dass er von Cloudflare stammt. Veröffentlichen Sie bei einem Tunnel den Port der App nicht auf dem Host; lassen Sie bei einem DNS-Eintrag mit Proxy nur die Adressen von Cloudflare durch Ihre Firewall. Ohne die Variable ignoriert sol2flow diese Header.

Ein Cloudflare Tunnel setzt auch X-Forwarded-For; steht sonst nichts vor der App, bleibt TRUST_PROXY_HOPS=1.

Mit einer GeoIP-Datenbank​

Wenn Sie nicht hinter Cloudflare sind, laden Sie eine kostenlose GeoIP-Datenbank herunter und lassen GEOIP_DB darauf zeigen. Die Abfrage passiert in der App: Keine Adresse wird irgendwohin geschickt. sol2flow liefert wegen der Lizenzen keine Datenbank mit:

  • MaxMind GeoLite2 (City oder Country): Legen Sie ein kostenloses MaxMind-Konto an, akzeptieren Sie die GeoLite2-Lizenz (EULA) und erstellen Sie einen Lizenzschlüssel. Die Lizenz verlangt, die Daten aktuell zu halten und MaxMind dort zu nennen, wo Sie sie zeigen. Am einfachsten halten Sie sie mit MaxMinds geoipupdate aktuell, zum Beispiel als Dienst in Ihrer compose.yml:

    services:
    geoipupdate:
    image: ghcr.io/maxmind/geoipupdate
    restart: unless-stopped
    environment:
    GEOIPUPDATE_ACCOUNT_ID: '123456'
    GEOIPUPDATE_LICENSE_KEY: '<Ihr Lizenzschlüssel>'
    GEOIPUPDATE_EDITION_IDS: GeoLite2-City
    GEOIPUPDATE_FREQUENCY: '72' # Stunden
    volumes: ['./geoip:/usr/share/GeoIP']
  • DB-IP Lite („IP to City Lite“, Format MMDB): einmal im Monat von db-ip.com herunterladen. Sie steht unter CC BY 4.0, das eine Namensnennung von DB-IP verlangt.

Binden Sie dann den Ordner in die App ein und setzen Sie den Pfad:

services:
app:
volumes: ['./geoip:/data/geoip:ro']
GEOIP_DB=/data/geoip/GeoLite2-City.mmdb

Die App liest die Datei bei der ersten Anmeldung und bemerkt eine neuere innerhalb von 10 Minuten; ein Neustart ist nicht nötig. Fehlt die Datei oder ist sie nicht lesbar, schreibt die App eine Warnung ins Log und arbeitet ohne Orte weiter.

Was gespeichert wird​

Für jede Person merkt sich sol2flow die Geräte und Orte, von denen aus sie sich angemeldet hat: die Browser- und Betriebssystemfamilie (etwa „Chrome“, „macOS“), Land und Stadt sowie einen Hash mit Schlüssel des Netzwerks (die ersten drei Teile einer IPv4-Adresse oder die ersten drei Gruppen einer IPv6-Adresse). Keine IP-Adresse und keine vollständige Browserkennung. Einträge, die 180 Tage nicht genutzt wurden, werden automatisch gelöscht, und alle gehen mit dem Konto. Hinweise zeigen die IP-Adresse teilweise verborgen (203.0.x.x).

Logs und Fehlerverfolgung​

Die App schreibt JSON-Zeilen auf die Standardausgabe (docker compose logs app). Um sie zusätzlich in Dateien aufzubewahren, setzen Sie LOG_FILE: Die App schreibt Dateien mit Datum im Namen neben diesen Pfad und beginnt neue von selbst:

VariableStandardBeschreibung
LOG_FILE(leer)Wohin die Log-Dateien kommen und wie sie heißen, z. B. /data/logs/app.log; auf der Standardausgabe erscheinen die Zeilen weiterhin. Leer = nur Standardausgabe.
LOG_FILE_FREQUENCYdailyJeden Tag (daily, app-2026-09-30.log) oder jede Stunde (hourly, app-2026-09-30-14.log) eine neue Datei, in UTC; none = kein Datum im Namen (app.log).
LOG_FILE_MAX_SIZE(leer)Leer = keine Größengrenze. Mit einer Größe wie 20m (k, m oder g) geht eine volle Datei in app-2026-09-30.1.log weiter, dann .2.log usw.
LOG_FILE_MAX_FILES10Wie viele Log-Dateien dieses Namens aufbewahrt werden, die neuesten; ältere werden gelöscht. Täglich ohne Größengrenze sind das zehn Tage. 0 = alle behalten.

Mit LOG_FILE=/data/logs/app.log liegen im Verzeichnis zum Beispiel:

app-2026-09-29.log
app-2026-09-30.log
app-2026-09-30.1.log # nur mit LOG_FILE_MAX_SIZE: die zweite Datei des Tages
app.log -> app-2026-09-30.1.log
  • Tage sind UTC-Tage: Die neue Datei beginnt um Mitternacht UTC, nicht um Mitternacht Ortszeit.
  • app.log zeigt immer auf die aktuelle Datei (ein symbolischer Link), sodass tail -F /data/logs/app.log dem Log auch über neue Dateien hinweg folgt. Nehmen Sie -F, nicht -f: tail -f bleibt bei der zuerst geöffneten Datei. Mit LOG_FILE_FREQUENCY=none und ohne Größengrenze ist app.log die Datei selbst; mit Größengrenze werden volle Dateien zu app.1.log, app.2.log … (höher = neuer).
  • Ein Neustart schreibt in der Datei des Tages weiter (der neuesten des Tages); nichts wird überschrieben. Gelöscht werden nur Dateien dieses Namens: Dateien der früheren Benennung (app.log.1 …) und andere Dateien im Verzeichnis bleiben.
  • Ein Dateiname pro Container. Zwei Prozesse dürfen nie dieselbe LOG_FILE schreiben: Teilt sich ein separater Worker das Log-Verzeichnis, nehmen Sie z. B. /data/logs/web.log für die App und /data/logs/worker.log für den Worker.

Das Image hat dafür ein Verzeichnis /data/logs: Binden Sie dort ein Volume ein, wie im Beispiel. Lassen sich die Dateien nicht schreiben (ein falscher Pfad, eine fehlende Berechtigung, eine volle Platte), protokolliert die App eine Warnung und schreibt weiter auf die Standardausgabe; das Schreiben in Dateien hält sie nie an.

Fehlerverfolgung sendet Fehler an Sentry oder an einen Dienst, der dessen Protokoll spricht, etwa das selbst gehostete Bugsink oder GlitchTip. Sie ist aus, bis Sie einen DSN setzen:

VariableStandardBeschreibung
SENTRY_DSN(leer)Der DSN Ihres Projekts (https://<key>@<host>/<project>). Leer = aus: Es wird nichts geladen oder gesendet. Aus einer Datei: SENTRY_DSN_FILE.
SENTRY_ENVIRONMENTproductionDie Umgebung, unter der die Fehler abgelegt werden, z. B. staging.
SENTRY_RELEASEdie Image-VersionDas Release, zu dem die Fehler gehören; standardmäßig die Version des Images, z. B. 1.4.0.
SENTRY_TRACES_SAMPLE_RATE0Anteil der Requests, die für die Performance-Analyse verfolgt werden, von 0 bis 1. 0 = nur Fehler.
SENTRY_LOGSfalsetrue sendet zusätzlich Log-Einträge ab Level warn an die Logs von Sentry. Bugsink und GlitchTip haben keine Logs: Dort hat es keine Wirkung.

Was gesendet wird: Fehler auf dem Server (fehlgeschlagene Requests, Fehler, die die App protokolliert, fehlgeschlagene Hintergrundjobs, versehen mit dem Namen des Jobs) und im Browser (Fehler auf Seiten, auch die hinter der Seite „Etwas ist schiefgelaufen“). Was nicht: keine Session-Replays, keine Namen, E-Mail-Adressen oder IP-Adressen, keine Cookies, Request-Bodies oder Query-Strings; Tokens in Adressen (öffentliche Board-Links, Einladungen, Passwort-Resets) werden entfernt. Der Browser erhält den DSN aus der Seite, eine Änderung braucht also nur einen Neustart der App.

Zum Beispiel mit Bugsink: Betreiben Sie es neben sol2flow hinter Ihrem Reverse-Proxy (hier unter errors.example.com), melden Sie sich an, legen Sie ein Team und ein Projekt an und kopieren Sie den DSN des Projekts in die .env:

docker run -d --name bugsink --restart unless-stopped -p 127.0.0.1:8000:8000 -v bugsink:/data \
-e SECRET_KEY="$(openssl rand -base64 50)" -e CREATE_SUPERUSER=[email protected]:<a password> \
-e BASE_URL=https://errors.example.com -e BEHIND_HTTPS_PROXY=true bugsink/bugsink
SENTRY_DSN=https://<key>@errors.example.com/1

Auch Browser senden ihre Fehler an die Adresse des DSN; sie muss daher von außen erreichbar sein, mit HTTPS.

Speicher​

VariableStandardBeschreibung
STORAGE_DRIVERlocallocal (ein Verzeichnis, binden Sie ein Volume ein) oder s3 (beliebiger S3-kompatibler Objektspeicher).
STORAGE_LOCAL_DIR/data/uploadsWo Dateien mit dem lokalen Treiber gespeichert werden.
S3_ENDPOINT(leer)API-Adresse, die der Server verwendet, z. B. https://s3.eu-central-1.amazonaws.com oder https://<account>.r2.cloudflarestorage.com. Leer = AWS für S3_REGION.
S3_PUBLIC_ENDPOINT= S3_ENDPOINTAPI-Adresse, die Browser für Download-Links verwenden, wenn sie von der des Servers abweicht (interner Hostname, privates Netz).
S3_REGIONus-east-1Die Region des Buckets (Cloudflare R2: auto).
S3_BUCKETsol2flowDer Bucket; legen Sie ihn vorher an.
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY(leer)Zugangsdaten. Leer = die Standardkette des SDK (zum Beispiel eine IAM-Rolle).
S3_FORCE_PATH_STYLEtruetrue für https://host/bucket/key (die meisten selbst gehosteten Speicher), false für https://bucket.host/key.
MAX_UPLOAD_MB10Veraltet. Upload-Limits legen Sie unter Admin → Allgemein → Uploads fest; dies bestimmt nur die Größe interner Formular-Requests.

Details und Beispiele: Speicher.

E-Mail​

VariableStandardBeschreibung
MAIL_DRIVERlogsmtp, um E-Mails zu versenden; log schreibt Nachrichten stattdessen ins App-Log (nur zum Testen).
MAIL_FROMsol2flow <[email protected]>Absendername und -adresse.
SMTP_HOSTlocalhostSMTP-Server.
SMTP_PORT1025Meist 587 (STARTTLS) oder 465 (TLS).
SMTP_USER, SMTP_PASSWORD(leer)Zugangsdaten, falls der Server sie braucht.
SMTP_SECUREfalsetrue für implizites TLS (Port 465); false für STARTTLS oder unverschlüsselt.

Details: E-Mail.

Erster Administrator​

Wird nur bei einem Start verwendet, solange die Instanz keine Benutzer hat; danach ignoriert.

VariableStandardBeschreibung
BOOTSTRAP_ADMIN_EMAIL(leer)E-Mail-Adresse des ersten Staff-Mitglieds, des Owners. Optional.
BOOTSTRAP_ADMIN_USERNAMEadminSein Benutzername.
BOOTSTRAP_ADMIN_PASSWORD(leer)Sein Passwort. Das Konto wird nur angelegt, wenn es gesetzt ist; leer lassen, um den Owner stattdessen über die Kommandozeile anzulegen (Installation).

Secrets aus Dateien (Docker Secrets)​

Jedes Secret kann statt aus der Umgebung auch aus einer Datei gelesen werden, so wie beim offiziellen PostgreSQL-Image: Setzen Sie X_FILE auf den Pfad einer Datei, die den Wert von X enthält. So gelangen Docker-Swarm- und Compose-Secrets in einen Container: als Dateien unter /run/secrets/.

VariableAus einer Datei
AUTH_SECRETAUTH_SECRET_FILE
APP_ENCRYPTION_KEYAPP_ENCRYPTION_KEY_FILE
DATABASE_URLDATABASE_URL_FILE
S3_ACCESS_KEY_IDS3_ACCESS_KEY_ID_FILE
S3_SECRET_ACCESS_KEYS3_SECRET_ACCESS_KEY_FILE
SMTP_USERSMTP_USER_FILE
SMTP_PASSWORDSMTP_PASSWORD_FILE
BOOTSTRAP_ADMIN_PASSWORDBOOTSTRAP_ADMIN_PASSWORD_FILE
SENTRY_DSNSENTRY_DSN_FILE

Die Regeln:

  • Der ganze Dateiinhalt ist der Wert, bis auf einen abschließenden Zeilenumbruch (LF oder CRLF), den docker secret create und die meisten Editoren anfügen. Leerzeichen am Anfang oder Ende gehören zum Wert.
  • Setzen Sie das eine oder das andere. Sind X und X_FILE beide gesetzt, startet die App nicht und nennt die Variable. Ebenso, wenn die Datei fehlt oder nicht lesbar ist. Eine leere Variable gilt als nicht gesetzt.
  • Werte erscheinen nie im Log, nur Variablennamen und Pfade.
  • Die Datei muss für den Benutzer des Containers lesbar sein, und das ist nicht root (uid 1001). Swarm-Secrets sind es (standardmäßig Modus 0444); bei einer selbst eingebundenen Datei prüfen Sie die Rechte.
  • DATABASE_URL_FILE enthält den ganzen Verbindungsstring, nicht nur das Passwort. Ein Passwort mit Sonderzeichen (@, :, /, %, …) muss darin URL-kodiert sein; oder Sie verwenden wie unten ein Hex-Passwort.

Beispiel für Docker Swarm, bei dem auch PostgreSQL sein Passwort aus einem Secret liest. Legen Sie die Secrets einmal an:

DB_PASSWORD=$(openssl rand -hex 16)
printf '%s' "$DB_PASSWORD" | docker secret create sol2flow_db_password -
printf 'postgresql://sol2flow:%s@db:5432/sol2flow?schema=public' "$DB_PASSWORD" | docker secret create sol2flow_database_url -
unset DB_PASSWORD
openssl rand -base64 32 | docker secret create sol2flow_auth_secret -
openssl rand -base64 32 | docker secret create sol2flow_app_encryption_key -
openssl rand -hex 12 | tee /dev/stderr | docker secret create sol2flow_admin_password - # gibt das Passwort einmal aus

Bewahren Sie eine Kopie des Verschlüsselungsschlüssels sicher außerhalb des Swarms auf: Ein Secret lässt sich nicht wieder auslesen (warum das wichtig ist). Dann eine Stack-Datei, stack.yml:

services:
app:
image: ghcr.io/sol2flow/sol2flow:latest # in Produktion ein festes Release festlegen
environment:
APP_URL: https://flow.example.com
AUTH_SECRET_FILE: /run/secrets/sol2flow_auth_secret
APP_ENCRYPTION_KEY_FILE: /run/secrets/sol2flow_app_encryption_key
DATABASE_URL_FILE: /run/secrets/sol2flow_database_url
BOOTSTRAP_ADMIN_PASSWORD_FILE: /run/secrets/sol2flow_admin_password
LOG_FILE: /data/logs/app.log # optional: Dateien app-JJJJ-MM-TT.log, app.log → die aktuelle
secrets: [sol2flow_auth_secret, sol2flow_app_encryption_key, sol2flow_database_url, sol2flow_admin_password]
ports: ['3000:3000']
volumes: [uploads:/data/uploads, logs:/data/logs]
db:
image: postgres:18-alpine
environment:
POSTGRES_USER: sol2flow
POSTGRES_DB: sol2flow
POSTGRES_PASSWORD_FILE: /run/secrets/sol2flow_db_password
secrets: [sol2flow_db_password]
volumes: [pgdata:/var/lib/postgresql] # from PostgreSQL 18 not …/data (data: …/18/docker)
secrets:
sol2flow_auth_secret: { external: true }
sol2flow_app_encryption_key: { external: true }
sol2flow_database_url: { external: true }
sol2flow_db_password: { external: true }
sol2flow_admin_password: { external: true }
volumes: { pgdata: {}, uploads: {}, logs: {} }

Starten Sie sie mit docker stack deploy -c stack.yml sol2flow und melden Sie sich als admin mit dem ausgegebenen Passwort an. Mit einfachem Docker Compose funktioniert es genauso, mit dateibasierten Secrets (secrets: { sol2flow_auth_secret: { file: ./auth_secret } }).

Nur für die Compose-Datei​

Diese Variablen liest die Beispiel-compose.yml, nicht die App:

VariableStandardBeschreibung
SOL2FLOW_VERSIONlatestDer Image-Tag, der ausgeführt wird. Legen Sie in Produktion ein festes Release fest.
POSTGRES_PASSWORDsol2flowPasswort des mitgelieferten PostgreSQL. Setzen Sie ein zufälliges.

SOL2FLOW_VERSION aus der .env gelangt auch in den Container und ist dort die Version, die die Fehlerverfolgung meldet. Legen Sie eine vollständige Version wie 1.4.0 fest statt latest.

Nicht für Produktion

Die App kennt außerdem einige Einstellungen, die nur für Entwicklung und automatisierte Tests existieren. Setzen Sie in einer produktiven .env nichts, was nicht auf dieser Seite steht.