Zum Inhalt

Umgebungsvariablen

Alle Konfigurationsparameter des Kamerplanter-Backends werden über Umgebungsvariablen gesteuert. Die Variablen werden von pydantic-settings geladen — Groß-/Kleinschreibung ist nicht relevant.

Lokale Konfiguration

Für die Docker-Compose-Umgebung alle Werte in eine .env-Datei im Repository-Wurzelverzeichnis eintragen. Eine Vorlage liegt als .env.example bereit:

cp .env.example .env


Datenbankverbindung

Variable Standard Pflicht Beschreibung
ARANGODB_HOST localhost Ja Hostname oder IP-Adresse der ArangoDB-Instanz
ARANGODB_PORT 8529 Nein TCP-Port der ArangoDB
ARANGODB_SCHEME http Nein https für eine ArangoDB, die TLS selbst anbietet (externe oder verwaltete Instanz); andere Werte verweigern den Start
ARANGODB_DATABASE kamerplanter Ja Name der Zieldatenbank
ARANGODB_USERNAME root Ja Datenbanknutzer
ARANGODB_PASSWORD — Ja Passwort des Datenbanknutzers
ARANGO_ROOT_PASSWORD — Ja* Root-Passwort für den ArangoDB-Container (nur Docker)

*ARANGO_ROOT_PASSWORD wird direkt an den ArangoDB-Container übergeben und ist für den Start der Datenbank erforderlich.

Produktionspasswörter

Verwenden Sie niemals den Standardwert rootpassword in produktiven Umgebungen. Generieren Sie sichere Passwörter: openssl rand -hex 32


Cache und Aufgaben-Queue

Variable Standard Pflicht Beschreibung
REDIS_URL redis://localhost:6379/0 Ja Verbindungs-URL für Redis oder Valkey (Celery Broker und Backend-Cache)
WORKER_READY_FILE — Nein Nur Celery-Worker. Leer (Standard, Docker Compose) ändert nichts. Gesetzt — das Helm-Chart setzt /tmp/worker-ready — prüft der Worker beim Start seine ArangoDB-Anmeldung, versucht es bis zu 60 Sekunden lang und beendet sich, wenn sie nicht gelingt; er nimmt dann keine Aufgabe an. Läuft sein Consumer, schreibt er die Datei, beim Herunterfahren entfernt er sie. Startup- und Readiness-Probe des Charts prüfen sie.

Format: redis://[user]:[password]@[host]:[port]/[db]

Beispiele:

redis://localhost:6379/0                    # Lokal ohne Auth
redis://:meinpasswort@redis:6379/0          # Mit Passwort
rediss://user:pass@redis-host:6380/1        # TLS (rediss://)


Sicherheit und Authentifizierung

Variable Standard Pflicht Beschreibung
JWT_SECRET_KEY change-me-in-production-... Ja Geheimer Schlüssel für JWT-Signierung (HS256)
JWT_ALGORITHM HS256 Nein JWT-Signaturalgorithmus
ACCESS_TOKEN_EXPIRE_MINUTES 15 Nein Gültigkeitsdauer des JWT-Access-Tokens in Minuten
REFRESH_TOKEN_EXPIRE_DAYS 30 Nein Gültigkeitsdauer des Refresh-Tokens in Tagen
SESSION_TOKEN_EXPIRE_HOURS 24 Nein Gültigkeitsdauer serverseitiger Session-Tokens in Stunden.
FERNET_KEY — Ja Fernet-Schlüssel zum Verschlüsseln von OIDC-Provider-Secrets und Integrations-Tokens. Unabhängig davon, ob OIDC genutzt wird — der Startup-Gate verweigert den Produktionsstart bei leerem Wert (AP-4, INF-S5). Muss ein gültiger Fernet-Schlüssel sein: 32 Bytes, url-safe base64-kodiert (44 Zeichen) — erzeugt z. B. mit python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())". Denselben Gate prüft inzwischen auch der Celery-Worker beim Start und verweigert bei DEBUG=false ebenfalls den Start, wenn der Wert fehlt oder kein gültiger Fernet-Schlüssel ist — Backend und Celery-Worker müssen denselben Schlüssel verwenden. Wird ein gespeicherter Wert mit einem abweichenden Schlüssel entschlüsselt, schlägt das laut fehl (SecretKeyMismatchError), statt den Chiffretext unbemerkt als Zugangsdaten weiterzugeben. Verschlüsselt sind auch der Home-Assistant-Token, der Pl@ntNet-Key und die Apprise-URLs der Nutzer (#2113): Nach einem Schlüsselwechsel sind sie nicht mehr lesbar und müssen neu eingetragen werden.
REQUIRE_EMAIL_VERIFICATION true Nein E-Mail-Verifikation bei Registrierung erzwingen. Eine Installation ohne ausgehenden Mailversand setzt false ausdrücklich
REGISTRATION_MODE open Nein Wer ein Konto anlegen darf (lokal und erste OIDC-Anmeldung): open (jeder), invite_only (nur mit offener E-Mail-Einladung für die Adresse), closed (niemand, auch nicht mit Einladung). Bestehende Konten melden sich in jedem Modus an. Ein unbekannter Wert verweigert den Start
REGISTRATION_ALLOWED_DOMAINS — (leer) Nein Optionale kommagetrennte E-Mail-Domains (z. B. verein.example,garten.example), exakt, ohne Subdomains. Leer = jede Domain. Eine E-Mail-Einladung für die Adresse ist die Ausnahme; über OIDC zählt nur eine vom Anbieter bestätigte Adresse
TENANT_MAX_MEMBERS_CEILING 50 Nein Plattform-Obergrenze für das Mitgliederlimit jedes Mandanten (mindestens 1). Wirksam ist min(max_members, TENANT_MAX_MEMBERS_CEILING); max_members darüber wird abgelehnt (422), eine neue Organisation ohne Angabe erhält diesen Wert. Ein voller Mandant verweigert weitere Beitritte mit 422 MEMBER_LIMIT_REACHED. Senken entfernt niemanden
TENANT_MAX_SERVICE_ACCOUNTS 20 Nein Wie viele aktive Service Accounts ein Mandant haben darf (mindestens 1). Darüber antwortet das Anlegen mit 422 SERVICE_ACCOUNT_LIMIT_REACHED. Ein Service Account belegt zusätzlich einen Platz im Mitgliederlimit. Senken entfernt keinen
TENANT_MAX_ORGANIZATIONS_PER_USER 10 Nein Wie viele Organisationen ein Konto besitzen darf, gezählt in jedem Zustand außer deleted (mindestens 0). Darüber antwortet POST /tenants mit 422 ORGANIZATION_LIMIT_REACHED. 0 heißt: Kein Konto gründet selbst eine Organisation. Eine Grenze des Kontos, kein Plan-Kontingent
TENANT_INVITATION_EMAILS_PER_DAY 50 Nein Wie viele E-Mail-Einladungen ein Konto in 24 Stunden verschicken darf, über alle seine Mandanten zusammen (mindestens 1). Jede E-Mail-Einladung geht an eine Adresse, die der Einladende frei wählt; darüber antwortet POST /api/v1/tenants/{slug}/invitations/email mit 429 RATE_LIMIT_EXCEEDED, und es wird nichts angelegt und nichts verschickt. Gezählt wird aus den gespeicherten Einladungen (Issue #2162)
TENANT_EXPORTS_PER_DAY 3 Nein Wie viele Mandanten-Exporte ein Mandant in 24 Stunden anfordern darf, jeder Zustand gezählt, auch fehlgeschlagene (mindestens 1). Darüber antwortet POST /api/v1/tenants/{slug}/exports mit 429 RATE_LIMIT_EXCEEDED. Siehe Mandanten exportieren und importieren (Issue #2138)
TENANT_IMPORT_MAX_UPLOAD_MB 512 Nein Größtes Mandanten-Archiv in MiB, das POST /api/v1/admin/platform/tenant-imports annimmt (mindestens 1); entpackt darf es unabhängig davon höchstens 1 GiB sein. Darüber 413 (Issue #2138). Eigene Upload-Grenze dieser Route (+ 1 MiB) — jede andere Route bleibt bei STORAGE_MAX_FILE_SIZE_MB + 1 MiB — und höchstens UPLOAD_SPOOL_BUDGET_MB, weil der Upload vor der Route in /tmp landet; mit den Standardwerten etwa 51 MiB. Helm: tenantExport.importMaxUploadMb. Größe planen: Archivgröße und /tmp
TENANT_EXPORT_SIGNING_SECRET — (leer) Nein Geheimnis, mit dem Mandanten-Archive signiert und geprüft werden (HMAC-SHA256 über das Manifest). Leer: wie bisher aus JWT_SECRET_KEY abgeleitet. Gesetzt: mindestens 32 Zeichen (openssl rand -hex 32), sonst startet das Backend nicht; vorher signierte Archive gelten beim Import als nicht verifiziert. Backend und Celery-Worker brauchen denselben Wert; Helm liest ihn optional aus tenantExport.signingSecret (Standard kamerplanter-secrets, Schlüssel TENANT_EXPORT_SIGNING_SECRET). Nie in values.yaml (Issue #2138)
RATE_LIMIT_TENANT_IMPORT 10/hour Nein Mengenbegrenzung für POST /api/v1/admin/platform/tenant-imports, je Konto (Issue #2138)
HIBP_ENABLED false Nein "Have I Been Pwned"-Prüfung bei Passwortänderung aktivieren
COOKIE_SECURE true Nein Setzt das Secure-Flag auf dem Refresh-Token-Cookie. Nur für reine HTTP-E2E-Testumgebungen ohne TLS auf false setzen — in Produktion immer true belassen.

Seit es Pläne gibt (Issue #2142), sind TENANT_MAX_MEMBERS_CEILING, TENANT_MAX_SERVICE_ACCOUNTS, STORAGE_TENANT_QUOTA_MB, STORAGE_MAX_PHOTOS_PER_INSTANCE und die drei AI_BUDGET_* der Wert, den ein Plan mit leerem Kontingent übernimmt — beim Standardplan standard also für jeden Mandanten. Ein Plan kann davon abweichen; nur TENANT_MAX_MEMBERS_CEILING bleibt eine harte Obergrenze. Siehe API-Referenz: Pläne und Kontingente.

JWT_SECRET_KEY in Produktion ändern

Der Standardwert change-me-in-production-use-openssl-rand-hex-32 darf in produktiven Umgebungen nicht verwendet werden. Generieren Sie einen sicheren Wert:

openssl rand -hex 32
Änderungen des JWT_SECRET_KEY machen alle aktiven Tokens ungültig — alle Nutzer werden abgemeldet.


Datenschutz & DSGVO (REQ-025 / NFR-011)

Diese Variablen steuern die datenschutzrechtlich vorgeschriebene Löschung/Anonymisierung personenbezogener Daten (siehe Datenschutz (DSGVO)) und sind vom Betriebsmodus unabhängig — sie gelten sowohl im Light- als auch im Full-Modus.

Variable Standard Pflicht Beschreibung
ERASURE_TOMBSTONE_SALT — Ja Hochentropisches Geheimnis (mindestens 32 Zeichen) zur Pseudonymisierung gelöschter Nutzerkonten (Tombstone-Hash, Löschantrags-Schlüssel und Mandanten-Slug-Digest, NFR-011 §4). Dieser Wert darf nach der ersten Kontolöschung nie mehr geändert werden. Der Startup-Gate verweigert den Produktionsstart, wenn der Wert leer oder kürzer als 32 Zeichen ist — unabhängig vom Betriebsmodus. Erzeugen mit openssl rand -hex 32.
LOG_PSEUDONYM_SALT — Ja Hochentropisches Geheimnis (mindestens 32 Zeichen), das ausschließlich die Log-Pseudonyme verschlüsselt: die subject=-Referenzen (sub_…) und die email_sha256-Digests auf Protokollzeilen sowie das Herkunftsfeld requested_by_subject von Lösch- und Mandanten-Löschnachweisen (NFR-011 §3.4). Getrennt von ERASURE_TOMBSTONE_SALT, weil dieser Wert — anders als der Tombstone-Salt — rotiert werden darf. Der Startup-Gate verweigert den Produktionsstart für Backend und Celery-Worker, wenn der Wert leer oder kürzer als 32 Zeichen ist — unabhängig vom Betriebsmodus. Erzeugen mit openssl rand -hex 32; sollte sich von ERASURE_TOMBSTONE_SALT unterscheiden (wird nicht erzwungen).
PRIVACY_DATA_CONTROLLER_NAME Kamerplanter Operator Nein Name des datenschutzrechtlich Verantwortlichen, erscheint in Export- und Auskunftsdokumenten.
PRIVACY_DATA_CONTROLLER_EMAIL privacy@kamerplanter.example Nein Kontakt-E-Mail des Verantwortlichen für DSGVO-Anfragen.
RETENTION_SOFT_DELETE_RETENTION_DAYS 90 Nein Frist, nach der ein zur Löschung markiertes (soft-gelöschtes) Konto endgültig (Hard-Delete) entfernt wird (NFR-011 R-01). Minimum: 1, Maximum: 90. Älterer Name PRIVACY_HARD_DELETE_AFTER_DAYS wird weiterhin akzeptiert; sind beide gesetzt, gewinnt der neue Name.
RETENTION_UNVERIFIED_ACCOUNT_DAYS 7 Nein Anzahl Tage nach der Registrierung, nach denen ein nie bestätigtes Konto automatisch gelöscht wird (NFR-011 R-02). Minimum: 1, Maximum: 7.
RETENTION_UNVERIFIED_LOCAL_REAP_ENABLED false Nein Freigabe-Schalter für R-02: Ist er false, zählt die Bereinigung lokal registrierte, nie bestätigte Konten nur (Trockenlauf, local_registrations_pending); bei true löscht sie diese ebenfalls. Erst setzen, wenn du die gezählte Anzahl gesehen hast (NFR-011 R-02).
RETENTION_IP_ANONYMIZATION_DAYS 7 Nein Anzahl Tage nach Ausstellung einer Session, nach denen ihre IP-Adresse anonymisiert wird (IPv4: letztes Oktett → 0; NFR-011 R-03). Minimum: 1, Maximum: 7. Betrifft nur Session-IP-Adressen, nicht die IP-Adresse eines Consent Records.
RETENTION_EXPORT_FILE_RETENTION_HOURS 72 Nein Aufbewahrungsdauer eines generierten Datenexports (Art. 15/20 DSGVO), bevor die Datei automatisch gelöscht wird (NFR-011 R-05). Minimum: 1, Maximum: 72. Älterer Name PRIVACY_EXPORT_RETENTION_HOURS wird weiterhin akzeptiert; sind beide gesetzt, gewinnt der neue Name.
RETENTION_ERASURE_AUDIT_RETENTION_YEARS 3 Nein Anzahl Jahre, die ein abgeschlossener Löschungs-Antrag (erasure_requests, status=completed) als Rechenschaftsnachweis (Art. 5 Abs. 2 DSGVO) aufbewahrt wird, bevor er endgültig gelöscht wird (NFR-011 R-06). Gezählt in Kalenderjahren. Minimum: 1, Maximum: 3.
RETENTION_EMAIL_CHANGE_RETENTION_HOURS 24 Nein Gültigkeitsdauer des Bestätigungslinks bei einer E-Mail-Adressänderung (NFR-011 R-07); danach wird die Anfrage auf den Status expired gesetzt, ein Hard-Delete findet nicht statt. Minimum: 1, Maximum: 24. Älterer Name PRIVACY_EMAIL_CHANGE_TTL_HOURS wird weiterhin akzeptiert; sind beide gesetzt, gewinnt der neue Name.
RETENTION_EMAIL_CHANGE_REVERT_DAYS 7 Nein Gültigkeitsdauer des Rückgängig-Links, den die vorherige Adresse nach einer bestätigten E-Mail-Änderung erhält (NFR-011 R-07a). Danach nullt derselbe stündliche Retention-Task wie bei R-07 die Felder previous_email und den Hash des Rückgängig-Tokens. Minimum: 1, Maximum: 7.
RETENTION_CONSENT_RETENTION_YEARS 3 Nein Jahre nach dem Widerruf, nach denen ein Consent Record endgültig gelöscht wird (NFR-011 R-04). Minimum: 1, Maximum: 3. Ein höherer Wert verhindert den Start von Backend und Worker.
RETENTION_CONSENT_IP_ANONYMIZATION_DAYS 7 Nein Tage nach der Aufzeichnung, nach denen die IP-Adresse eines Consent Records anonymisiert wird (NFR-011 R-04a). Minimum: 1, Maximum: 7. Ein höherer Wert verhindert den Start von Backend und Worker.
RETENTION_INVITATION_RETENTION_DAYS 30 Nein Tage nach Ablauf, nach denen eine abgelaufene Einladung endgültig gelöscht wird (NFR-011 R-12). Minimum: 1, Maximum: 30. Ein höherer Wert verhindert den Start von Backend und Worker.
RETENTION_ERASURE_MEMBER_NOTICE_DAYS 7 Nein Tage, die die endgültige Löschung eines Kontos mit geteiltem persönlichem Garten nach der Benachrichtigung der anderen Mitglieder wartet, wenn diese erst nach der Terminierung des Löschantrags benachrichtigt wurden (Antrag aus der Zeit vor #1824 oder fehlgeschlagene erste E-Mail; NFR-011 R-01a). Eine Sofortlöschung durch einen Administrator wartet nie. Minimum: 1, Maximum: 7. Ein höherer Wert verhindert den Start von Backend und Worker.
RETENTION_TENANT_ERASURE_GRACE_DAYS 90 Nein Tage, um die eine angenommene Mandantenlöschung aufgeschoben wird (NFR-011 R-01b). In dieser Zeit ist der Mandant gesperrt, die Mitglieder sind mit Datum benachrichtigt, und die Verwaltung kann die Löschung abbrechen. 0 löscht sofort (Self-Hosted mit einem Haushalt). Minimum: 0, Maximum: 90. Ein höherer Wert verhindert den Start von Backend und Worker.
RETENTION_HARVEST_DATA_MIN_RETENTION_YEARS 5 Nein Anzahl Jahre ab harvest_date, nach denen die bei einer Mandantenlöschung aufbewahrten Erntedaten (Ernte, Qualitätsbewertung, Ertragskennzahl) endgültig gelöscht werden (NFR-011 R-16). Minimum: 5 (gesetzliche Mindestfrist nach CanG).
RETENTION_TREATMENT_MIN_RETENTION_YEARS 3 Nein Dasselbe für Behandlungsanwendungen, gezählt ab applied_at (NFR-011 R-17). Minimum: 3 (PflSchG §11).
RETENTION_INSPECTION_MIN_RETENTION_YEARS 3 Nein Dasselbe für Inspektionsprotokolle, gezählt ab inspected_at (NFR-011 R-18). Minimum: 3 (PflSchG §11).

Ein Wert oberhalb des Maximums bricht den Start von API und Worker ab (die Meldung nennt Variable und Grenzwert, nie den Wert). Das Maximum ist der NFR-011-Wert: Fristen dürfen zur Datenminimierung verkürzt, aber nicht verlängert werden. Wird ein älterer PRIVACY_*-Name gesetzt, schreibt der Start eine Warnung; sind beide Namen mit verschiedenen Werten gesetzt, eine Fehlermeldung (der RETENTION_*-Wert gilt).

Für RETENTION_SOFT_DELETE_RETENTION_DAYS, RETENTION_EXPORT_FILE_RETENTION_HOURS und RETENTION_EMAIL_CHANGE_RETENTION_HOURS waren die älteren PRIVACY_*-Namen zwar bereits dokumentiert, hatten aber keine Wirkung — der Code nutzte feste Werte. Sie funktionieren jetzt tatsächlich und bleiben zusätzlich als Alias gültig.

ERASURE_TOMBSTONE_SALT — Boot-Blocker in Produktion, darf nie wechseln

Anders als die meisten anderen Variablen auf dieser Seite ist ERASURE_TOMBSTONE_SALT kein optionales Feature-Flag: Das Backend startet in Produktion (DEBUG=false) grundsätzlich nicht, wenn dieser Wert fehlt oder zu kurz ist — unabhängig davon, ob DSGVO-Löschanfragen aktiv genutzt werden. Dieser Salt schlüsselt den Tombstone-Hash gelöschter Konten, den Löschantrags-Schlüssel und den Mandanten-Slug-Digest — niemals die Log-Pseudonyme (dafür ist LOG_PSEUDONYM_SALT zuständig). Er darf sich nach der ersten Kontolöschung nie mehr ändern, sonst werden bestehende Tombstones unlesbar. Auch der Celery-Worker beendet sich beim Start, wenn der Salt fehlt oder zu kurz ist; gib ihm denselben Wert und dieselbe DEBUG-Einstellung wie dem Backend.

Mit DEBUG=true startet das Backend auch ohne gültigen Wert, etwa im lokalen Entwicklungs-Stack. Die Kontolöschung verweigert dann trotzdem jeden Lauf, bevor sie etwas anfasst: Löscht ein Platform-Admin ein Konto, antwortet das Backend mit 503. Ein Löschantrag nach Art. 17 wird zwar angenommen, der tägliche Lauf führt ihn aber nicht aus. Der Antrag bleibt als partially_completed offen, bis du einen gültigen Salt setzt. Details zu allen unbedingt erforderlichen Secrets: Konfigurationsmatrix — Pflicht-Secrets je aktivierter Funktion.

LOG_PSEUDONYM_SALT — Boot-Blocker in Produktion, für Backend und Worker gleich

LOG_PSEUDONYM_SALT ist ebenfalls kein optionales Feature-Flag: Backend und Celery-Worker verweigern in Produktion (DEBUG=false) den Start, wenn dieser Wert fehlt oder kürzer als 32 Zeichen ist. Er schlüsselt ausschließlich die Log-Pseudonyme — die subject=-Referenz und den email_sha256-Digest auf Protokollzeilen sowie das Herkunftsfeld requested_by_subject von Lösch- und Mandanten-Löschnachweisen. Gib beiden Prozessen denselben Wert. Mit DEBUG=true starten beide auch ohne gültigen Wert; die Konto- und Mandantenlöschung verweigert dann aber jeden Lauf (503), weil ihr Nachweis sonst keine Referenz auf das auslösende Konto trüge.

Rotation. Im Gegensatz zu ERASURE_TOMBSTONE_SALT darf dieser Salt gewechselt werden: Setze den neuen Wert in Backend und Worker (Kubernetes-Secret kamerplanter-secrets bzw. .env bei Docker Compose) und starte beide neu. Danach korrelieren Protokollzeilen und requested_by_subject-Werte von vor dem Wechsel nicht mehr mit späteren — wer nur den neuen Salt kennt, kann alte Referenzen keinem Konto mehr zuordnen. Auch welches Admin-Konto eine gespeicherte Löschung ausgelöst hat, lässt sich danach nur noch mit dem alten Salt nachrechnen — bewahre ihn gesichert auf, solange der Löschnachweis aufbewahrt wird (ein Jahr), wenn diese Zuordnung prüfbar bleiben soll. Keine Abfrage hängt vom Log-Salt ab, eine Rotation hat also sonst keine Nebenwirkungen. Der Wert sollte sich von ERASURE_TOMBSTONE_SALT unterscheiden; das wird nicht erzwungen.


Betriebsmodus

Variable Standard Pflicht Beschreibung
KAMERPLANTER_MODE full Nein Betriebsmodus: full (Auth + Mandanten) oder light (kein Auth, lokale Einzelnutzung)
APPRISE_ALLOW_PRIVATE_TARGETS abhängig vom Modus: false in full, true in light Nein Ob Apprise-Ziele (Gotify, Matrix, ntfy-Server) in privaten Netzen liegen dürfen: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10, fc00::/7. Geprüft beim Speichern und beim Senden. Ungesetzt entscheidet KAMERPLANTER_MODE; true oder false überschreibt das in beide Richtungen. Im full-Modus erreicht ein privates Ziel die Dienste im Cluster (API-Server, ArangoDB, Valkey), daher aus. Anders als HA_ALLOW_PRIVATE_ENDPOINT trägt hier jeder Nutzer die Adresse ein. DEBUG lockert die Regel nicht; Loopback, Link-Local und Cloud-Metadaten bleiben immer gesperrt. Siehe Helm: Egress für Benachrichtigungskanäle.
DEBUG false Nein Debug-Logging aktivieren (verbose, nie in Produktion). Deaktiviert zusätzlich den Startup-Gate für Produktions-Secrets — niemals in Produktion setzen.
FRONTEND_URL http://localhost:5173 Nein URL des Frontends (wird für E-Mail-Links verwendet)
APP_BASE_URL http://localhost:5173 Nein Basis-URL für QR-Codes auf Pflanzen-Etiketten (Druckansichten, siehe Druckansichten & Export). In Produktion auf die öffentlich erreichbare Frontend-URL setzen, sonst zeigen gedruckte QR-Codes auf localhost. Bildet auch die Rückruf-URL der OAuth/OIDC-Anmeldung und der erneuten Anmeldung zur Bestätigung ({APP_BASE_URL}/api/v1/auth/oauth/{slug}/callback) — diese URL beim Identity-Provider hinterlegen. Bildet außerdem die Abo-URL der iCal-Feeds (ical_url, REQ-015, #2226); ein http://-Wert wird dort zu https:// angehoben, außer für localhost, 127.0.0.1 und ::1. Der Wert soll mit http:// oder https:// beginnen und einen Host nennen; sonst protokolliert das Backend beim Start die Warnung app_base_url_not_absolute_http, und nur die Abo-URL ergänzt das Schema selbst.

Light-Modus (KAMERPLANTER_MODE=light)

Im Light-Modus entfällt die Token-Authentifizierung. Die API ist ohne Anmeldung verwendbar. Dieser Modus ist für lokale Einzelinstallationen ohne Internet-Exposition gedacht.

Light-Modus nicht öffentlich exponieren

Der Light-Modus deaktiviert alle Authentifizierungsschichten. Niemals mit einem öffentlich erreichbaren Port betreiben.

Netzwerk-Freigabe unter Docker Compose

Diese Variable liest nicht das Backend, sondern Docker Compose (docker-compose.yml und docker-compose.release.yml) beim Start.

Variable Standard Pflicht Beschreibung
KAMERPLANTER_BIND_ADDRESS 127.0.0.1 Nein Host-Adresse, an die Compose die Benutzeroberfläche (8080) und die API (8000) bindet. Mit 0.0.0.0 erreichen andere Geräte im Netzwerk beide Ports, siehe Dauerbetrieb — Zugriff von anderen Geräten. Datenbanken, Valkey und Ollama bleiben unabhängig davon an 127.0.0.1 gebunden.

CORS-Konfiguration

Variable Standard Pflicht Beschreibung
CORS_ORIGINS ["http://localhost:3000","http://localhost:5173"] Nein JSON-Array erlaubter Origins für CORS

Format: Immer als JSON-Array im String-Format:

CORS_ORIGINS='["https://app.example.com","https://app2.example.com"]'


E-Mail

Variable Standard Pflicht Beschreibung
EMAIL_ADAPTER console Nein E-Mail-Adapter: console (Ausgabe im Log, Link nur mit DEBUG=true), smtp oder resend. Ein anderer Wert — etwa ein Tippfehler — wird beim Start abgelehnt: API und Worker starten dann gar nicht erst, statt still auf console zurückzufallen.
SMTP_HOST localhost Nein SMTP-Server-Hostname
SMTP_PORT 587 Nein SMTP-Port
SMTP_USERNAME — Nein SMTP-Benutzername
SMTP_PASSWORD — Nein SMTP-Passwort
SMTP_FROM_EMAIL noreply@kamerplanter.example Nein Absenderadresse für System-E-Mails über SMTP
SMTP_USE_TLS true Nein STARTTLS für SMTP aktivieren
RESEND_API_KEY — Bedingt API-Schlüssel für den Versand über Resend (Secret). Pflicht, wenn EMAIL_ADAPTER=resend gesetzt ist — ohne ihn lehnt die Anwendung den Start ab. Trage ihn über das Kubernetes-Secret kamerplanter-secrets bzw. deine .env-Datei ein, niemals über eine Helm-values-Datei.
RESEND_FROM_EMAIL noreply@kamerplanter.example Nein Absenderadresse für System-E-Mails über Resend. Die Domain dieser Adresse muss im Resend-Konto verifiziert sein, sonst schlägt der Versand fehl.

Im Entwicklungsmodus (EMAIL_ADAPTER=console) werden E-Mails nicht gesendet, sondern im Backend-Log ausgegeben. Den Bestätigungs- oder Passwort-Reset-Link enthält diese Protokollzeile aber nur, wenn zusätzlich DEBUG=true gesetzt ist — sein Token übernimmt sonst das Konto. Ohne DEBUG=true steht dort lediglich, dass keine E-Mail zugestellt wurde, ohne Token. Startet die API mit EMAIL_ADAPTER=console und DEBUG=false — der Fall bei einer produktiven Installation ohne SMTP- oder Resend-Konfiguration, da das Helm-Chart standardmäßig keinen Adapter setzt — schreibt sie beim Start eine Warnung ins Log (email_adapter_console_in_production). Für eine produktive Installation bleibt dann EMAIL_ADAPTER=smtp oder EMAIL_ADAPTER=resend zu konfigurieren, sonst lassen sich Registrierung und Passwort-Reset nicht abschließen. Auch E-Mail-Einladungen in einen Tenant gehen dann nicht raus: Die Einladung wird angelegt, die Antwort meldet delivered: false, und die Oberfläche zeigt dem Einladenden den Link zum Weitergeben (Issue #2162). Der Link zeigt auf FRONTEND_URL.

Resend: gehostete E-Mail-Zustellung über eine HTTP-API

EMAIL_ADAPTER=resend verschickt dieselben System-E-Mails (E-Mail-Bestätigung, Passwort-Reset, Bestätigungscode für erneute Anmeldung, Benachrichtigungs-E-Mails) wie der SMTP-Adapter, aber über die HTTP-API von Resend statt über eine SMTP-Verbindung. In den Logs erscheint die Empfängeradresse dabei nur als gesalzener Digest, niemals im Klartext; schlägt ein Versand fehl, protokolliert die Anwendung nur den Fehlertyp und den HTTP-Status, nie den Antworttext oder den API-Schlüssel. Jeder Versandversuch hat ein Zeitlimit von 10 Sekunden.

Wird auch vom Benachrichtigungssystem genutzt

Diese Variablen konfigurieren zugleich den E-Mail-Kanal des Benachrichtigungssystems — es gibt keine separate SMTP-Konfiguration für Benachrichtigungen.

Zwei verschiedene Voraussetzungen für föderierte Konten

Ein Konto ohne lokales Passwort mit einem OIDC-fähigen Anbieter (Google, generisches OIDC) bestätigt unumkehrbare Kontoaktionen und Zugangsdaten-Änderungen (Kontolöschung, erstes lokales Passwort, Mandantenlöschung, E-Mail-Änderung, seit Version 1.19 auch API-Key ausstellen, Gerätekopplung und Anmeldeweg entfernen) mit einer frischen Anmeldung bei diesem Anbieter — siehe API-Dokumentation: Authentifizierung. Dafür muss der Anbieter prompt=login/max_age und den Claim auth_time unterstützen, nicht SMTP. Nur ein Konto, dessen verknüpfte Anbieter ausschließlich GitHub und/oder Apple sind (beide können keine frische Anmeldung belegen), nutzt stattdessen den per E-Mail zugestellten Bestätigungscode — siehe Bestätigungscode per E-Mail anfordern. Läuft die Instanz für ein solches Konto mit EMAIL_ADAPTER=console und DEBUG=false, meldet die Anwendung, dass der Bestätigungscode nicht zugestellt werden kann (503), statt ihn stillschweigend verschwinden zu lassen — es kann sich trotzdem nicht löschen, kein erstes lokales Passwort setzen, keinen Mandanten löschen, die E-Mail-Adresse nicht ändern und auch keinen API-Key oder Kopplungscode ausstellen, bis SMTP konfiguriert ist. EMAIL_ADAPTER=smtp ist deshalb für den produktiven Betrieb mit GitHub/Apple-only-Konten Pflicht, nicht nur empfohlen.


Externe Datenanreicherung (REQ-011)

Variable Standard Pflicht Beschreibung
PERENUAL_API_KEY — Nein API-Schlüssel für Perenual-Pflanzendatenbank
TREFLE_API_KEY — Nein API-Schlüssel für Tréflé-Pflanzendatenbank
ENRICHMENT_HTTP_TIMEOUT 30 Nein HTTP-Timeout für externe API-Anfragen (Sekunden)

GBIF wird ohne API-Key verwendet (öffentliche API). Perenual und Tréflé erfordern kostenlose Registrierung.


Knowledge Service — Re-Ranking (optional)

Diese Variablen konfigurieren den optionalen Cross-Encoder-Re-Ranker des Knowledge Service. Ist RERANKER_URL leer, arbeitet der Knowledge Service im Hybrid-Search-only-Modus (Graceful Degradation). Siehe ADR-007.

Variable Standard Pflicht Beschreibung
RERANKER_URL `` (leer) Nein HTTP-URL des Reranker-Microservice, z. B. http://reranker-service:8081. Leer = Re-Ranking deaktiviert.
RERANKER_INITIAL_K 15 Nein Anzahl der Kandidaten aus dem Hybrid-Search-Schritt, die höchstens durch den Cross-Encoder neu bewertet werden (Bewertungs-Obergrenze, kein Limit für die Ergebnisanzahl — siehe KI-Architektur).
RERANKER_MAX_DOCUMENT_CHARS 500 Nein Maximale Zeichenzahl von titel\ninhalt je Kandidaten-Chunk, bevor er an den Reranker-Sidecar geschickt wird. Der an das LLM übergebene Chunk behält seinen vollständigen Text — nur die Reranker-Eingabe wird gekürzt.
RERANKER_TOP_K 5 Nein Anzahl der Chunks, die nach dem Re-Ranking an den LLM-Kontext übergeben werden.
RERANKER_MODEL bge-reranker-v2-m3 Nein ONNX-Modellname im Reranker-Service-Container (Verzeichnis unter /app/models/onnx/).

RERANKER_MODEL gehört zum Reranker-Service, nicht zum Knowledge Service

RERANKER_MODEL wird als Umgebungsvariable am reranker-service-Container gesetzt — nicht am knowledge-service. Die anderen vier Variablen (RERANKER_URL, RERANKER_INITIAL_K, RERANKER_MAX_DOCUMENT_CHARS, RERANKER_TOP_K) gehören zum Knowledge Service.

Ressourcenbedarf und Zeitbudget

Der Reranker-Service benötigt 1,5–4 GB RAM (je nach Modell) und addiert Latenz pro Anfrage. Bei bge-reranker-v2-m3 am 2-CPU-Limit des Helm-Charts kostet ein (Query, Dokument)-Paar mit 512 Tokens ca. 3 Sekunden; der Sidecar bricht eine Re-Ranking-Anfrage nach 25 Sekunden serverseitig ab (Fallback-Grund deadline, siehe KI-Architektur). Ein realer HTTP-Test über alle 100 Benchmark-Fragen zeigt: Mit den Standardwerten RERANKER_INITIAL_K=15 und RERANKER_MAX_DOCUMENT_CHARS=500 (Paar-Median 166 Tokens, maximal 222) waren alle 100 Anfragen erfolgreich — p50 12,9 Sekunden, p90 15,0 Sekunden, maximal 16,0 Sekunden, innerhalb des 25-Sekunden-Budgets. Ein CI-Guard prüft für die Standardwerte und für jede Deployment-Datei im Repository, die eine der beiden Variablen setzt, dass RERANKER_INITIAL_K × RERANKER_MAX_DOCUMENT_CHARS ≤ 7.500 gilt (RERANK_SCORED_CHARS_BUDGET in config.py) und RERANKER_INITIAL_K höchstens 50 beträgt (die Anfragegrenze des Sidecars). Die Standardwerte schöpfen das Budget mit 15 × 500 = 7.500 exakt aus. Werte, die du außerhalb des Repositorys setzt, sieht der Guard nicht. Wer einen der beiden Werte über dieses Produkt hinaus anheben will, muss die Latenz vorher neu messen. Für Raspberry Pi und ressourcenarme Umgebungen empfiehlt sich weiterhin, RERANKER_URL leer zu lassen.


KI-Assistent

Diese Variablen gehören zum Kamerplanter-Backend und steuern den dreistufigen Freischalt-Mechanismus sowie die Anbindung an den Knowledge Service (siehe KI-Assistent — Benutzerhandbuch). Die Provider-Auswahl (Ollama/Anthropic/OpenAI-kompatibel) ist eine separate Konfiguration am Knowledge Service selbst — siehe KI-Provider einrichten.

Variable Standard Pflicht Beschreibung
AI_FEATURES_ENABLED false Nein Stufe 1 des dreistufigen Freischalt-Mechanismus. false lässt sämtliche /ai/*-Endpunkte mit HTTP 404 antworten — die KI-API existiert dann faktisch nicht.
KNOWLEDGE_SERVICE_ENABLED false Nein Aktiviert die Anbindung an den Knowledge Service (wird sowohl vom älteren /api/v1/knowledge/*-Pfad als auch intern vom KI-Assistenten benötigt).
KNOWLEDGE_SERVICE_URL http://knowledge-service:8000 Nein Basis-URL des Knowledge-Service-Microservice.
AI_KNOWLEDGE_SERVICE_TIMEOUT_S 60 Nein HTTP-Timeout des KnowledgeServiceAdapter gegen den Knowledge Service (Sekunden).
AI_CIRCUIT_BREAKER_THRESHOLD 3 Nein Anzahl aufeinanderfolgender Fehler, ab der der Adapter den Knowledge Service als nicht erreichbar markiert.
AI_CIRCUIT_BREAKER_WINDOW_S 60 Nein Zeitfenster (Sekunden), in dem die Fehler für AI_CIRCUIT_BREAKER_THRESHOLD gezählt werden.
AI_CIRCUIT_BREAKER_COOLDOWN_S 60 Nein Wartezeit (Sekunden), bevor der Adapter nach dem Auslösen des Circuit Breakers erneut Anfragen an den Knowledge Service zulässt.
AI_PUBLIC_RATE_LIMIT_PER_MIN 10 Nein IP-Ratenbegrenzung für die Wissensfrage POST /api/v1/public/ai/ask (Anfragen pro Minute). Der Endpunkt existiert nur im Light-Modus und läuft dort als System-User.
AI_BUDGET_USER_CALLS_PER_DAY 50 Nein KI-Aufrufe, die ein Konto in einem Mandanten pro UTC-Tag starten darf (Tipps, Tagestipp, „Warum?“, Chat-Nachricht, Glossar-Erzeugung, KI-Diagnose zusammen). Darüber 429 AI_BUDGET_EXCEEDED mit Retry-After bis Tagesende. 0 schaltet das Budget ab.
AI_BUDGET_TENANT_CALLS_PER_DAY 500 Nein KI-Aufrufe aller Mitglieder eines Mandanten zusammen pro UTC-Tag. Ein Mitglied, das an seinem eigenen Budget abgewiesen wird, verbraucht dieses Budget nicht. 0 schaltet es ab.
AI_BUDGET_TENANT_TOKENS_PER_DAY 2000000 Nein LLM-Tokens (Prompt + Antwort, wie der Knowledge Service sie meldet), die ein Mandant pro UTC-Tag verbrauchen darf. Geprüft wird vor dem nächsten Aufruf; eine einzelne Antwort kann das Budget überschreiten. 0 schaltet es ab. Die Zähler liegen in Valkey; ist Valkey nicht erreichbar, läuft kein KI-Aufruf (503 AI_BUDGET_UNAVAILABLE).
INTERNAL_SERVICE_TOKEN — Bedingt Gemeinsames Geheimnis für cluster-interne M2M-Aufrufe (u. a. an den Knowledge Service). Pflicht, sobald KNOWLEDGE_SERVICE_ENABLED=true gesetzt ist — ohne Token verweigert der Startup-Gate den Start (AP-4).

Instanzweite Freischaltung reicht allein nicht aus

AI_FEATURES_ENABLED=true schaltet die KI-API nur instanzweit frei (Stufe 1). Damit ein konkreter Mandant (Garten) KI-Funktionen tatsächlich nutzen kann, muss zusätzlich tenant.settings.ai_features_enabled für diesen Mandanten gesetzt sein (Stufe 2) — dafür existiert aktuell weder eine Oberfläche noch ein eigener API-Endpunkt, siehe KI-Assistent — Für technische Nutzer / Self-Hoster.

Provider-Konfiguration liegt am Knowledge Service, nicht am Backend

LLM_PROVIDER, LLM_API_URL, LLM_API_KEY und LLM_MODEL sind Umgebungsvariablen des eigenständigen Knowledge-Service-Deployments (src/knowledge-service/), nicht dieses Backends. Details: KI-Provider einrichten.


MCP-Server

Diese Variablen steuern den MCP-Server — die Werkzeug-Schnittstelle, über die externe LLM-Clients (Claude Desktop, Claude Code, eigene Agenten) per Service-Account-API-Key auf Kamerplanter zugreifen können.

Variable Standard Pflicht Beschreibung
MCP_SERVER_ENABLED false Nein Gesamtschalter. Solange nicht true, antworten alle /mcp/*-Endpunkte mit HTTP 404 — die Schnittstelle existiert dann faktisch nicht.
MCP_IDEMPOTENCY_TTL_HOURS 24 Nein Gültigkeitsdauer eines idempotency_key für Schreibwerkzeuge — danach wird ein Wiederholungs-Aufruf als neue Aktion behandelt.
MCP_AUDIT_RETENTION_DAYS 90 Nein Aufbewahrungsdauer des mcp_audit_log (NFR-011) — ältere Einträge werden automatisch gelöscht.
MCP_MAX_IMAGE_PAYLOAD_MB 4 Nein Obergrenze der Gesamt-Nutzlast eines Aufrufs des Tagebuch-Werkzeugs get_diary_entry_photos in Megabyte (Base-64-kodiert). Wird sie überschritten, antwortet das Werkzeug mit dem Fehlercode payload.too_large, statt Fotos still wegzulassen — siehe MCP-Server — Tagebuch-Analyse.

Kein eigener Prozess, keine eigenen Verbindungsvariablen

Der MCP-Server läuft im bestehenden Backend-Prozess mit und nutzt dessen ArangoDB-/Redis-Verbindung mit — es gibt keine separate Host-, Port- oder Credential-Konfiguration.


Tagebuch — Umgebungs-Schnappschuss

Beim Anlegen eines Tagebuch-Eintrags liest Kamerplanter die Sensorwerte, die die Pflanze abdecken, und speichert sie getrennt von den handnotierten Messwerten mit — siehe Tagebuch — Die Umgebung wird automatisch mitgeschrieben.

Variable Standard Pflicht Beschreibung
DIARY_ENVIRONMENT_CAPTURE_ENABLED true Nein Gesamtschalter. Auf false wird jeder neue Eintrag mit dem Vermerk „nicht versucht" gespeichert — unterscheidbar von „gesucht und nichts gefunden".
DIARY_ENVIRONMENT_MAX_AGE_MINUTES 60 Nein Ein Messwert, der älter ist, wird gar nicht erfasst. Ein Eintrag, der einen gestrigen Sensorwert als aktuell ausweist, ist schlechteres Belegmaterial als ein Eintrag ohne Klimawerte.
DIARY_ENVIRONMENT_CAPTURE_TIMEOUT_SECONDS 3.0 Nein Harte Obergrenze für die gesamte Erfassung. Läuft sie ab, wird der Eintrag mit dem gespeichert, was rechtzeitig ankam — das Anlegen wartet nie länger auf einen Sensor.

Ein Ausfall verhindert nie den Eintrag

Nicht erreichbares Home Assistant, fehlende TimescaleDB oder ein abgelaufenes Zeitbudget führen zu einem Eintrag ohne (oder mit unvollständigen) Umgebungswerten, nie zu einer abgelehnten Anlage.


Metriken (Prometheus)

Variable Standard Pflicht Beschreibung
METRICS_PORT 0 Nein Port, auf dem das Backend /metrics im Prometheus-Format ausliefert. 0 startet keinen Listener. Das Helm-Chart setzt 9464, sobald monitoring.enabled an ist.
METRICS_BIND_ADDRESS 0.0.0.0 Nein Adresse, an die sich der Metrik-Listener bindet.

Die Metriken laufen auf einem eigenen Port, nie als Route der API auf Port 8000. Der Weg von außen (Ingress → Frontend-nginx → Backend :8000) erreicht sie dadurch nicht; im Cluster lässt die NetworkPolicy nur den konfigurierten Prometheus zu. Die Zeitreihen tragen weder Mandant noch Person: handler ist das Routenmuster (/api/v1/t/{tenant_slug}/…), nie der aufgerufene Pfad. Details: Helm — Metriken.

mDNS / Zeroconf Discovery

Variable Standard Pflicht Beschreibung
MDNS_ENABLED false Nein mDNS-Service-Announcement aktivieren (_kamerplanter._tcp.local.)
INSTANCE_ID (auto) Nein Eindeutige Instanz-ID (z. B. kp-abc123). Wird beim Start automatisch generiert, wenn leer.

Wenn aktiviert, annonciert das Backend einen _kamerplanter._tcp.local.-Service im lokalen Netzwerk. Home Assistant erkennt diesen Service automatisch und bietet die Einrichtung der Kamerplanter-Integration an.

Stabile Instanz-ID

Die INSTANCE_ID wird für die Duplikat-Erkennung in Home Assistant verwendet. Wenn sie leer bleibt, wird bei jedem Neustart eine neue ID generiert. Für stabile Discovery sollte ein fester Wert gesetzt werden, z. B. INSTANCE_ID=kp-mein-server.

mDNS und Kubernetes

mDNS basiert auf Multicast-UDP (Port 5353) im lokalen Layer-2-Netzwerk. In Standard-Kubernetes-Clustern funktioniert mDNS nicht, da:

  1. Overlay-Netzwerk blockiert Multicast — Standard-CNIs (Calico, Cilium, Flannel) routen nur L3-Traffic. Multicast-Pakete aus einem Pod erreichen das physische LAN nicht — Home Assistant sieht die Announcements nie.
  2. Pod-IP ist nicht LAN-erreichbar — Selbst bei funktionierendem Multicast wuerde die annoncierte Pod-IP (z. B. 10.42.x.x) von ausserhalb des Clusters nicht erreichbar sein.
Deployment MDNS_ENABLED Begruendung
Docker Compose / Bare Metal true Backend laeuft direkt im LAN — MDNS_ENABLED=true setzen
K3s / MicroK8s Single-Node + hostNetwork: true true Pod teilt Host-Netzwerk — Multicast erreicht das LAN
Standard K8s Cluster false Overlay-Netzwerk blockiert Multicast — manueller Config Flow in HA als Fallback
Cloud (AWS, GCP, Azure) false Kein lokales Netzwerk vorhanden

hostNetwork ist ein Trade-off

Mit hostNetwork: true teilt der Pod den Netzwerk-Namespace des Hosts. Multicast funktioniert, aber auf Kosten der Netzwerk-Isolation (Port-Konflikte möglich, keine NetworkPolicy-Enforcement). Nur für Homelab-/Raspberry-Pi-Szenarien empfohlen.

Im Helm-Chart ist MDNS_ENABLED standardmaessig auf false gesetzt. Der manuelle Config Flow in Home Assistant (URL-Eingabe) funktioniert in jedem Deployment-Szenario als Fallback.


Home Assistant Integration (REQ-005)

Variable Standard Pflicht Beschreibung
HA_URL — Nein Home-Assistant-Basis-URL, z. B. http://homeassistant.local:8123
HA_ACCESS_TOKEN — Nein Long-Lived Access Token aus Home Assistant
HA_TIMEOUT 10 Nein HTTP-Timeout für HA-Anfragen (Sekunden)
HA_ALLOW_PRIVATE_ENDPOINT false Nein SSRF-Opt-in: Home Assistant läuft üblicherweise im LAN über HTTP auf einer privaten/RFC1918-Adresse (homeassistant.local, 192.168.x.x) oder localhost. Ohne diese Freigabe blockiert der SSRF-Schutz Verbindungen zu solchen Adressen. Der Cloud-Metadaten-/Link-Local-Bereich (169.254.0.0/16) bleibt immer blockiert, unabhängig von dieser Variable.

Sind beide Variablen HA_URL/HA_ACCESS_TOKEN gesetzt, aktiviert das Backend zusätzlich den Home-Assistant-Kanal des Benachrichtigungssystems (persistente Notifications, Mobile Push, TTS).

Apprise-Kanal erfordert zusätzliches Python-Paket

Der apprise-Benachrichtigungskanal ist unabhängig von den Home-Assistant-Variablen immer aktiv, benötigt aber das optionale Python-Paket apprise im Backend-Image (pip install apprise) — dafür gibt es keine eigene Umgebungsvariable. Details siehe Benachrichtigungen — Apprise.


Zeitreihendaten (TimescaleDB, REQ-005)

Diese Variablen aktivieren die optionale TimescaleDB-Anbindung für hochfrequente Sensor-Zeitreihen mit automatischem Downsampling (siehe Sensorik). Ohne TIMESCALEDB_ENABLED=true werden manuelle und automatische Messwerte weiterhin in ArangoDB gespeichert — die App bleibt voll funktionsfähig, nur ohne automatisches mehrstufiges Downsampling.

Variable Standard Pflicht Beschreibung
TIMESCALEDB_ENABLED false Nein Gesamtschalter für die TimescaleDB-Anbindung.
TIMESCALEDB_HOST localhost Nein Hostname der TimescaleDB-Instanz.
TIMESCALEDB_PORT 5432 Nein TCP-Port.
TIMESCALEDB_DATABASE kamerplanter_sensors Nein Datenbankname.
TIMESCALEDB_USERNAME postgres Nein Datenbankbenutzer.
TIMESCALEDB_PASSWORD changeme Bedingt Datenbankpasswort. Pflicht in Produktion — der Startup-Gate verweigert den Start, wenn TIMESCALEDB_ENABLED=true gesetzt ist und dieser Wert unverändert changeme lautet (siehe Konfigurationsmatrix — Pflicht-Secrets je aktivierter Funktion).
TIMESCALEDB_POOL_MIN_SIZE 2 Nein Minimale Connection-Pool-Größe.
TIMESCALEDB_POOL_MAX_SIZE 10 Nein Maximale Connection-Pool-Größe.

Docker Compose: eigenes Profil

In der lokalen Docker-Compose-Umgebung startet TimescaleDB nur mit docker-compose --profile timescaledb up -d. In Kubernetes schaltest du ihn mit controllers.timescaledb.enabled: true ein; das Chart setzt dann TIMESCALEDB_ENABLED, TIMESCALEDB_HOST und die übrigen Werte selbst (siehe Helm Charts).


Umgebungssteuerung & Aktorik (REQ-018)

Diese Variable steuert die periodische Auswertung von Zeitplänen und Regeln, den stündlichen Override-Ablauf und den 5-Minuten-Online/Offline-Abgleich mit Home Assistant für Umgebungssteuerung & Aktorik.

Variable Standard Pflicht Beschreibung
ACTUATOR_CONTROL_LOOP_ENABLED false Nein Kill-Switch für die drei periodischen Aktor-Steuerungs-Tasks (evaluate_control_rules alle 30 s, expire_manual_overrides stündlich, sync_actuator_states alle 5 min). Ist die Variable deaktiviert, laufen Zeitpläne und Regeln nicht automatisch — Aktoren bleiben aber jederzeit über die REST-API steuerbar (direkter Befehl, Override, Notabschaltung).

Kein eigener HA-Schalter nötig

Anders als die anderen Home-Assistant-Funktionen benötigt die Aktor-Steuerung keine zusätzliche Freischaltung — solange HA_URL/HA_ACCESS_TOKEN gesetzt sind und ACTUATOR_CONTROL_LOOP_ENABLED=true ist, dispatcht das System Befehle an Home-Assistant-Aktoren automatisch.


InvenTree-Integration (REQ-016)

Diese Variablen aktivieren die optionale Anbindung an InvenTree. Ohne INVENTREE_ENABLED=true liefern alle InvenTree-Endpunkte den Fehler „Funktion deaktiviert" (HTTP 409), ohne die App zu blockieren.

Variable Standard Pflicht Beschreibung
INVENTREE_ENABLED false Nein Kill-Switch für die gesamte InvenTree-Integration.
INVENTREE_ALLOW_PRIVATE_ENDPOINT false Nein Erlaubt eine InvenTree-Instanz mit privater/LAN-Adresse (analog zu HA_ALLOW_PRIVATE_ENDPOINT). Ohne diese Freigabe blockiert der SSRF-Schutz Verbindungen zu internen Adressen.

Verbindung (inkl. API-Token) und Verknüpfungen richtest du anschließend über die REST-API ein — Details siehe Betriebsmittel & Inventar (InvenTree) — Für technische Nutzer / Self-Hoster.


Wettervorhersage & Frost-Frühwarnung

Diese Variablen steuern die Wettervorhersage-Abholung und die darauf aufbauende proaktive Frost-Frühwarnung. Ohne WEATHER_ENABLED=true bleiben beide Funktionen vollständig deaktiviert — Standorte ohne konfigurierte Wetterquelle sind davon unabhängig ebenfalls nicht betroffen.

Variable Standard Pflicht Beschreibung
WEATHER_ENABLED false Nein Kill-Switch für die gesamte Wetterfunktion (Quellenabholung + Frost-Frühwarnung). Details zur eigentlichen Quellenkonfiguration siehe Wetterquellen je Standort.
WEATHER_DEFAULT_PUBLIC_SOURCE open-meteo Nein Werkseitig voreingestellte öffentliche Wetterquelle für neue Standorte ohne explizite Auswahl.
OPEN_METEO_ENABLED true Nein Instanzweiter Default für die Quelle Open-Meteo (keyless, EU-Fokus). Vom Platform-Admin pro Instanz über die Wetterdienste-Verwaltung überschreibbar (siehe Wetterdienste konfigurieren) — diese Variable setzt nur den Ausgangswert.
DWD_ENABLED true Nein Instanzweiter Default für die Quelle DWD/Bright Sky (Deutscher Wetterdienst). Ebenfalls Platform-Admin-überschreibbar.
OPENWEATHERMAP_ENABLED true Nein Instanzweiter Default für die Quelle OpenWeatherMap. Ebenfalls Platform-Admin-überschreibbar.
FROST_FORECAST_HORIZON_DAYS 2 Nein Vorhersage-Zeitraum in Tagen ab heute (inklusive), der auf einen erwarteten Frosttag geprüft wird — Standard deckt heute plus den Folgetag ab.
FROST_FORECAST_THRESHOLD_CELSIUS 2.0 Nein Minimaltemperatur, ab der ein vorhergesagter Tag als Frosttag gilt. Bewusst getrennt vom reaktiven Schwellwert unten, mit einem etwas konservativeren (näher an 0 °C liegenden) Wert, da eine mehrtägige Vorhersage unsicherer ist als eine aktuelle Messung.

Zum Vergleich — der bestehende reaktive Frost-Schwellwert (aktuell gemessene Temperatur, unverändert durch diese Erweiterung):

Variable Standard Pflicht Beschreibung
FROST_WARNING_THRESHOLD_CELSIUS 3.0 Nein Schwellwert für die reaktive Frost-Warnung (binary_sensor.kp_{location}_frost_warning), basierend auf der zuletzt gemessenen Lufttemperatur.

Klimanormalen (NASA POWER)

Diese Variablen steuern die monatliche Hintergrund-Abholung der langjährigen Klima-Normalwerte (Abschnitt „Klima am Standort") über die keyless NASA-POWER-Reanalyse-Schnittstelle. Für die Abholung müssen sowohl WEATHER_ENABLED als auch NASA_POWER_CLIMATE_ENABLED aktiv sein.

Variable Standard Pflicht Beschreibung
NASA_POWER_CLIMATE_ENABLED true Nein Eigener Kill-Switch für den monatlichen Klimanormalen-Task, unabhängig vom allgemeinen WEATHER_ENABLED — beide müssen aktiv sein, damit der Task läuft.
NASA_POWER_BASE_URL https://power.larc.nasa.gov/api/temporal Nein Basis-URL der NASA-POWER-API. Nur für Self-Hoster mit abweichender Netzwerk-/Proxy-Konfiguration relevant.
NASA_POWER_CLIMATE_TTL_DAYS 180 Nein Klimanormalen ändern sich kaum; ein bereits abgeholter Datensatz wird erst nach Ablauf dieser TTL erneut abgeholt — hält den monatlichen Task idempotent und schont die NASA-POWER-API.
NASA_POWER_DATA_LATENCY_DAYS 7 Nein Betrifft die separate Tageswerte-Abholung (nicht die Klimanormalen): Anzahl Tage, die NASA POWER für die Qualitätskontrolle seiner jüngsten Tageswerte benötigt.
NASA_POWER_DAILY_DAYS_BACK 14 Nein Betrifft ebenfalls nur die Tageswerte-Abholung: Größe des Rückblick-Fensters in Tagen.

Betrifft nur Freiland- und Gewächshaus-Standorte mit GPS-Koordinaten

Klimanormalen werden ausschließlich für Standorte vom Typ Außenbereich oder Gewächshaus mit hinterlegten GPS-Koordinaten materialisiert — für Innenraum-Standorte sind sie ohne Nutzen und werden nicht abgeholt. NASA POWER ist keyless nutzbar; die Daten unterliegen der CC-BY-4.0-Lizenz (Attribution wird automatisch mit ausgeliefert, siehe Klima am Standort).


Winterhärtezonen (USDA)

Diese Variable steuert die vierteljährliche Hintergrund-Aktualisierung der automatisch aus den Klimanormalen abgeleiteten Winterhärtezone eines Standorts (siehe Klimazonen & Winterhärte). Die Ableitung baut auf den Klimanormalen auf — der zugehörige Task läuft daher nur, wenn zusätzlich sowohl WEATHER_ENABLED als auch NASA_POWER_CLIMATE_ENABLED aktiv sind.

Variable Standard Pflicht Beschreibung
HARDINESS_ZONE_REFRESH_ENABLED true Nein Eigener Kill-Switch für den vierteljährlichen Winterhärtezonen-Task (1. Januar/April/Juli/Oktober, 05:00 UTC), unabhängig von NASA_POWER_CLIMATE_ENABLED — beide müssen aktiv sein, damit der Task läuft. Manuell gesetzte Zonen (hardiness_zone_source: manual) werden vom Task nie überschrieben.

Betrifft nur Freiland- und Gewächshaus-Standorte mit GPS-Koordinaten und vorhandenen Klimanormalen

Wie die Klimanormalen selbst wird die Winterhärtezone nur für Standorte vom Typ Außenbereich oder Gewächshaus mit GPS-Koordinaten berechnet — und erst, sobald für diesen Standort bereits mindestens ein Klimanormalen-Datensatz mit verwertbarer Minimaltemperatur vorliegt. Ein sofortiges manuelles Auslösen (unabhängig von diesem Zeitplan) ist über die API möglich, siehe API-Referenz — Winterhärtezonen.


Bewässerungsbedarf (ET₀)

Diese Variablen steuern den täglichen Hintergrund-Task, der aus den Wetterdaten eines Freiland- oder Gewächshaus-Standorts die Referenz-Evapotranspiration (FAO-56, ET₀) und daraus den Netto-Bewässerungsbedarf je Pflanzdurchlauf berechnet. Der Task benötigt zusätzlich WEATHER_ENABLED=true — ohne abgeholte Wetterdaten gibt es nichts zu berechnen. Ergebnis und Verhalten für Endnutzer sind unter Gießprotokoll: Vorgeschlagene Gießmenge und Pflegeerinnerungen: Warum eine Erinnerung ausbleiben kann beschrieben.

Variable Standard Pflicht Beschreibung
IRRIGATION_DEMAND_ENABLED true Nein Eigener Kill-Switch für den täglichen compute_irrigation_demand-Task (06:15 Uhr), unabhängig vom allgemeinen WEATHER_ENABLED — beide müssen aktiv sein, damit der Task läuft.
IRRIGATION_ROOT_ZONE_DEPTH_MM 300.0 Nein Angenommene effektive Wurzelzonentiefe in Millimeter Boden. Wird verwendet, um die Wasserhaltekapazität eines Substrats (in Prozent) in eine Millimeter-Obergrenze für den Netto-Bewässerungsbedarf umzurechnen — verhindert eine rechnerisch zu hohe Tagesempfehlung bei sehr trockenen Ausgangsbedingungen.

Nur Freiland- und Gewächshaus-Standorte, keine neuen REST-Endpunkte

Der Bewässerungsbedarf wird ausschließlich für Standorte vom Typ Außenbereich oder Gewächshaus mit hinterlegten GPS-Koordinaten berechnet — Innenraum-Standorte bleiben beim intervallbasierten Gießplan (REQ-022). Es gibt keinen eigenen REST-Endpunkt dafür; das Ergebnis fließt über den bestehenden Gießmengen-Vorschlag (suggest_volume) und die Pflegeerinnerungs-Engine in die Oberfläche ein.

Berechnungsgrundlage: aquacropeto (BSD-3-Clause)

Die FAO-56-Penman-Monteith- und Hargreaves-Formeln für ET₀ werden über die Python-Bibliothek aquacropeto (PyPI-Paket aquacropeto, BSD-3-Clause-Lizenz) berechnet — keine ShareAlike-/Copyleft-Pflichten für den Kamerplanter-Code. Details siehe NOTICE.md im Projekt-Root.


Health-Endpunkt und Build-Kennung

Der unauthentifizierte Endpunkt GET /api/health kann beantworten, welcher Build gerade läuft. Weil er unauthentifiziert ist, ist diese Auskunft standardmäßig abgeschaltet und der Endpunkt mengenbegrenzt.

Variable Standard Pflicht Beschreibung
HEALTH_EXPOSE_BUILD_REVISION false (Helm-Chart setzt true) Nein Ob GET /api/health das Feld build_revision überhaupt ausliefert. Bei false fehlt der Schlüssel vollständig in der Antwort. Der Anwendungs-Standard bleibt false — helm/kamerplanter/values.yaml setzt für Kubernetes-Installationen seit #1236 true, damit der Auslieferungsstand prüfbar ist.
BUILD_REVISION (leer) Nein Der vollständige Git-Commit, aus dem das Image gebaut wurde. Wird beim Container-Build eingebacken (docker-publish.yml reicht ihn als Build-Argument in das Dockerfile); von Hand setzen musst du ihn nur, wenn du selbst baust.
RATE_LIMIT_HEALTH 60/minute Nein Mengenbegrenzung für GET /api/health, je Client-IP und Prozess — gezählt nur im Prozessspeicher, damit der Endpunkt nie auf Valkey wartet, auch nicht bei einem Ausfall. Die Kubernetes-Proben zeigen auf /api/v1/health/live und /api/v1/health/ready und sind davon nicht betroffen.

Warum die Build-Kennung standardmäßig fehlt

Öffentlich ist nicht der Commit-Hash — das Repository ist ohnehin offen —, sondern die Zuordnung dieser Host läuft auf jenem Commit. Aus ihr folgt der exakte Rückstand gegenüber dem Entwicklungsstand und damit die Liste der Fehlerbehebungen, die dieser Instanz fehlen. Aktiviere das Feld deshalb bewusst — etwa auf einer Instanz, die ohnehin nur im eigenen Netz erreichbar ist, oder für die Dauer einer Fehlersuche.

Drei unterscheidbare Antwortzustände, die nicht verwechselt werden dürfen:

Antwort Bedeutung
Der Schlüssel build_revision fehlt HEALTH_EXPOSE_BUILD_REVISION ist false. Bewusste Konfiguration, kein Defekt.
"unknown" Auskunft ist erlaubt, aber es ist keine Revision eingebacken (Entwicklungs-Image, ungestempelter Build).
Ein 7- bis 40-stelliger Hexadezimal-Wert Die echte Antwort. Ein von docker-publish.yml gebautes Image meldet den vollen 40-stelligen SHA; ein selbst gebautes Image mit BUILD_REVISION=$(git rev-parse --short HEAD) meldet entsprechend weniger.

Der Wert wird vor der Ausgabe gegen ^[0-9a-f]{7,40}$ geprüft (nach dem Abschneiden von Leerraum, damit ein in YAML umbrochener oder in der Shell gequoteter Wert überlebt). Alles andere wird zu "unknown" — nie zu einem erfundenen oder abgeleiteten Wert.

Betriebssignal, keine Attestierung

build_revision sagt, was die Instanz über sich behauptet. Wer das Deployment kompromittiert hat, kann sie jeden Hash melden lassen. Der belastbare Nachweis bleibt gh attestation verify zusammen mit dem Digest aus .status.containerStatuses[].imageID des Pods — siehe CI/CD — Prüfungen entlang der Auslieferungskette.


Rate Limiting

Variable Standard Pflicht Beschreibung
RATE_LIMIT_AUTH 20/minute Nein Rate-Limit für Authentifizierungsendpunkte
RATE_LIMIT_STORAGE_URL leer Nein Speicher, in dem die IP-Rate-Limits zählen (redis://…, rediss://…, redis+unix://… oder memory://; ein anderes Schema, ein Passwort mit unkodiertem /, # oder ? oder ein @ in Pfad, Query oder Fragment verhindert den Start — hier und in REDIS_URL —, die Meldung nennt den Wert nicht; ein @ im Passwort selbst ist zulässig). Leer bedeutet: der Valkey aus REDIS_URL, damit alle Replikas und Worker-Prozesse gemeinsam zählen. memory:// zählt je Prozess und passt nur, wenn ein Prozess die ganze Installation ist. Scheitert der Speicher mit einem Fehler des Redis-Clients, zählt jeder Prozess für sich weiter (nicht offen, kein Fehler, auch nicht für Anfragen, die beim Ausfall gerade warten); je Prozess kann ein frisches Zeitfenster gelten, eines je Fenster, nicht je Ausfall. Bei hängendem Speicher wartet nur die Anfrage, die den Ausfall bemerkt (und jede, die gerade wartet), einen Socket-Timeout von 0,5 s je Verbindungsversuch und Adresse (DNS, mehrere Adressen, Verbindungsaufbau plus Handshake und NOSCRIPT-Nachladen nicht abgedeckt), ebenso je Prozess eine Prüfanfrage in Abständen von 1, 2, 4, 8, 16, 32 s und danach alle 32 s; jeder Prozess zählt erst mit seiner eigenen nächsten Prüfanfrage wieder gemeinsam. Die Mail-Budgets der Anmelderouten nutzen dieselben Client-Optionen und denselben Prüfplan: Solange Valkey hängt, wartet nur der Speicheraufruf, der den Ausfall bemerkt, und je Prozess eine Prüfanfrage höchstens 0,5 s — siehe IP-Rate-Limits und Proxy-Tiefe.
TRUSTED_PROXY_HOPS 0 Ja, hinter zwei Proxys Wie viele Proxy-Adressen die eigene Infrastruktur an X-Forwarded-For anhängt, von rechts gezählt. 0 = Client → nginx → Backend (Lauf ohne Ingress, z. B. Docker Compose); 1 = Client → Ingress-Controller → nginx → Backend (das Helm-Chart setzt diesen Wert; in der Referenz-Installation ist der Ingress Contour/Envoy). Zu niedrig löst jeden Aufrufer auf den nächsten Proxy auf — der Device-Pairing-Lockout sperrt dann alle Nutzer gleichzeitig und IP-Allowlist-Service-Accounts scheitern; zu hoch liest Einträge, die ein Aufrufer fälschen kann. Bei 0 und einer X-Forwarded-For-Kette mit mehr als einem Eintrag loggt das Backend einmal je Prozess forwarded_chain_deeper_than_trusted_proxy_hops: entweder ein nicht gezählter Proxy oder ein Aufrufer, der den Header selbst schickt (dann ist 0 richtig); erhöhe den Wert nur bei wirklich mehr Proxys — siehe Login-Schutz und IP-Limits.
RATE_LIMIT_UPLOAD 30/minute Nein Budget je Konto und Route für Datei-Uploads: Anhänge, Pflanzen- und Aufgabenfotos, Schädlingsbilder, CSV-Import (Upload und Bestätigung). Darüber 429 mit Retry-After. Ohne angemeldetes Konto zählt die Client-IP.
RATE_LIMIT_INFERENCE 20/minute Nein Budget je Konto und Route für Routen, die ein Modell ausführen: CV-Diagnose, Schädlingserkennung, Pflanzenbestimmung, Referenzbeitrag und jede Route, die ein LLM anspricht (Tipps und Tagestipp erzeugen, „Warum?“, Chat-Nachricht, Glossar-Erzeugung, KI-Diagnose, Wissensfrage POST /api/v1/t/{slug}/ai/knowledge/ask). Die Tagesgrenzen von Bestimmung und Referenzbeitrag sowie die KI-Tagesbudgets (AI_BUDGET_*) gelten zusätzlich.
RATE_LIMIT_EXPORT 20/minute Nein Budget je Konto und Route für die PDF-Drucke unter /print (Nährstoffplan, Pflegeliste, Pflanzenetiketten).
RATE_LIMIT_RESEND_VERIFICATION 10/hour Nein Rate-Limit je Client-IP für POST /api/v1/auth/resend-verification. Begrenzt E-Mails an frei wählbare Adressen, deshalb weit unter RATE_LIMIT_AUTH. Die zusätzliche Grenze je Adresse (3 Anfragen, Auffüllung nach einer Stunde Ruhe) ist fest und nicht konfigurierbar.
RATE_LIMIT_CALENDAR_FEED 30/minute Nein Rate-Limit je Client-IP für den öffentlichen iCal-Abruf GET /api/v1/calendar/feeds/{key}/feed.ics (REQ-015 CF-004). Kalender-Apps rufen ein Abo alle 15 Minuten bis alle paar Stunden ab; darüber 429. Abgewiesene Abrufe mit falschem Token zählen mit.
RATE_LIMIT_CALENDAR_FEED_PER_TOKEN 60/hour Nein Zusätzliches Rate-Limit je vorgelegtem Feed-Token für denselben Abruf (REQ-015 CF-004, #2226). Begrenzt eine geleakte Abo-URL auch dann, wenn sie von vielen Adressen aus abgerufen wird; ein Abo auf mehreren Geräten (Handy, Laptop, Home Assistant) bleibt weit darunter. Der Bucket ist über einen Digest des Tokens geschlüsselt — ohne den Token kann niemand das Budget verbrauchen.
RATE_LIMIT_INVITATION_EMAIL 10/minute Nein Rate-Limit je Konto für POST /api/v1/tenants/{slug}/invitations/email: Jeder Aufruf verschickt eine E-Mail an eine frei gewählte Adresse. Die Tagesgrenze je Konto ist TENANT_INVITATION_EMAILS_PER_DAY (Issue #2162)
RATE_LIMIT_HEALTH 60/minute Nein Rate-Limit für GET /api/health — siehe Health-Endpunkt und Build-Kennung

Format: [anzahl]/[einheit] — Einheiten: second, minute, hour, day


Verschachtelte Konfiguration (GBIF)

GBIF-Einstellungen können über den Unterstrich-Doppelpunkt-Delimiter verschachtelt werden:

Variable Standard Beschreibung
GBIF__BASE_URL https://api.gbif.org/v1 GBIF-API-Basis-URL
GBIF__RATE_LIMIT_PER_MINUTE 60 Anfragen pro Minute an GBIF
GBIF__HTTP_TIMEOUT 30 Timeout für GBIF-Anfragen (Sekunden)

Foto-Identifikation (REQ-029)

Diese Variablen konfigurieren die optionale Pflanzenerkennung per Foto. Wenn keine der API-Schlüssel gesetzt ist, ist das Feature vollständig deaktiviert — alle Kamera-Schaltflächen sind ausgeblendet und es wird keine Einwilligung abgefragt.

Variable Standard Pflicht Beschreibung
PLANTNET_API_KEY — Nein API-Schlüssel für Pl@ntNet (Free-Tier: ≤ 500 Identifikationen/Tag). Registrierung unter my.plantnet.org.
PLANTNET_ENABLED true Nein Schaltet den Pl@ntNet-Adapter komplett ab, auch wenn PLANTNET_API_KEY gesetzt ist. Auf false setzen, um Pl@ntNet trotz vorhandenem Key zu deaktivieren (z. B. bei ausschließlicher Nutzung der selbst-gehosteten DINOv2-Erkennung).
PLANTNET_BASE_URL https://my-api.plantnet.org/v2 Nein Basis-URL der Pl@ntNet-API. Nur für Self-Hosting oder Test-Endpunkte ändern.
PLANT_ID_API_KEY — Nein API-Schlüssel für Plant.id (Kindwise) — ein zusätzlicher, rein Betreiber-initiierter Cloud-Adapter (niemals automatisch primär, anders als Pl@ntNet).
PLANT_ID_BASE_URL https://plant.id/api/v3 Nein Basis-URL der Plant.id-API.
INFERENCE_SERVICE_ENABLED false Nein Aktiviert den selbst-gehosteten DINOv2-Erkennungspfad (REQ-029-A). Muss auf Backend UND Celery-Worker identisch gesetzt sein, sobald Nutzer eigene Referenzbilder beitragen können — der Worker führt die planmäßige DSGVO-Art.-17-Löschung dieser Beiträge aus (Issue #1753); fehlt die Variable dort, hält der Worker fällige Löschungen als Konfigurationsfehler zurück und eine Mandantenlöschung antwortet mit HTTP 503. Details zur vollständigen Inbetriebnahme (VectorDB, Referenz-Index-Befüllung, Aktivierungsreihenfolge) siehe Bilderkennung in Betrieb nehmen.
INFERENCE_SERVICE_URL http://kamerplanter-recognition:8000 Nein Interne URL des Inferenz-Service. Ebenfalls auf Backend und Celery-Worker identisch setzen (siehe oben).
IDENTIFICATION_PRIMARY_ADAPTER plantnet Nein Bevorzugter Adapter. Mögliche Werte: plantnet, local_embedding (DINOv2, sobald INFERENCE_SERVICE_ENABLED=true). Ein anderer Wert — etwa ein Tippfehler — wird beim Start abgelehnt: API und Worker starten dann gar nicht erst, statt still einen anderen Anbieter zu verwenden.
IDENTIFICATION_HTTP_TIMEOUT 60 Nein HTTP-Timeout (Sekunden) für den externen Identifikations-Aufruf (Pl@ntNet-Upload + serverseitige ML-Inferenz kann den früheren 30-Sekunden-Standard unter Last überschreiten).
IDENTIFICATION_CONFIDENCE_AUTO_ACCEPT 0.85 Nein Übereinstimmungsschwelle (0–1), ab der ein Vorschlag als „sehr sicher" hervorgehoben wird.
IDENTIFICATION_CONFIDENCE_MIN_SHOW 0.10 Nein Mindest-Übereinstimmung (0–1) für die Anzeige eines Vorschlags. Ergebnisse darunter werden gefiltert.
IDENTIFICATION_MAX_IMAGE_SIZE_MB 5 Nein Maximale Bildgröße in Megabyte. Größere Bilder werden mit HTTP 400 abgelehnt.
IDENTIFICATION_MAX_IMAGE_DIMENSION 1024 Nein Längste Kante (px), auf die das Nutzerbild vor dem Upload an den Adapter herunterskaliert wird. Kleiner = schnellerer Upload und weniger Drittanbieter-Bandbreite.
IDENTIFICATION_RATE_LIMIT_PER_USER_DAY 50 Nein Maximale Anfragen pro Nutzer pro Tag (SEC-003-Untergrenze, verhindert dass ein einzelnes Konto das geteilte Free-Tier-Kontingent aufbraucht). 0 verwendet stattdessen das Adapter-Standard-Limit (500 bei Pl@ntNet).
IDENTIFICATION_EXTERNAL_IN_LIGHT_MODE false Nein Betreiber-Opt-in für den externen Erkennungspfad (Pl@ntNet) im Light-Modus. Im Light-Modus gibt es kein Einwilligungssystem — ein Foto an Dritte zu senden erfordert daher eine bewusste Betreiber-Entscheidung. Solange diese Variable false bleibt, ist im Light-Modus ausschließlich der selbst-gehostete local_embedding-Pfad nutzbar (sobald INFERENCE_SERVICE_ENABLED=true gesetzt ist).
REFERENCE_CONTRIBUTION_RATE_LIMIT_PER_USER_DAY 20 Nein Maximale Anzahl Referenzbild-Beiträge (POST /identification/reference) pro Nutzer pro Tag — schützt den Erkennungsindex vor Missbrauch/Flutung durch ein einzelnes Konto. 0 deaktiviert das Limit. Nur relevant, wenn die selbst-gehostete DINOv2-Erkennung aktiv ist (siehe Self-Hosted-Erkennung mit DINOv2).

Pl@ntNet nur für nicht-kommerzielle Nutzung

Der Pl@ntNet Free-Tier ist für nicht-kommerzielle Nutzung zugelassen. Für kommerzielle Instanzen die Nutzungsbedingungen unter my.plantnet.org prüfen.

Kubernetes Secrets

Der PLANTNET_API_KEY sollte als Kubernetes Secret hinterlegt werden:

apiVersion: v1
kind: Secret
metadata:
  name: kamerplanter-identification
type: Opaque
stringData:
  PLANTNET_API_KEY: "dein-api-schluessel"

Feature-Toggle-Logik

PLANTNET_API_KEY gesetzt?
  ├── Ja  → Pl@ntNet aktiv (Artbestimmung, ≤ 500 IDs/Tag)
  └── Nein → Feature vollständig deaktiviert
             (Kamera-Buttons ausgeblendet, kein Consent-Dialog)

Schädlingserkennung (REQ-044)

Diese Variablen konfigurieren die optionale bildbasierte Schädlingserkennung. Das Feature ist standardmäßig deaktiviert — ohne gesetztes PEST_DETECTION_ENABLED=true ist der Button „Auf Schädlinge prüfen" ausgeblendet und die App voll funktionsfähig.

Variable Standard Pflicht Beschreibung
PEST_DETECTION_ENABLED false Nein Gesamtschalter. Auf true setzen, um die Funktion zu aktivieren. Muss auf Backend UND Celery-Worker gesetzt sein (alternativ INFERENCE_SERVICE_ENABLED), sobald ein Platform-Admin ein beigetragenes Schädlingsfoto zur Erkennungsbasis freigegeben hat — der Worker führt die planmäßige DSGVO-Art.-17-Löschung dieser Beiträge aus (Issue #1759); fehlt beides dort, hält der Worker fällige Löschungen als Konfigurationsfehler zurück und eine Mandantenlöschung antwortet mit HTTP 503. Details: Bilderkennung in Betrieb nehmen.
PEST_DETECTION_SYMPTOM_ENABLED true Nein Schadbild-/Symptom-Erkennung (Modus 2) ein/aus. Aktiv wenn PEST_DETECTION_ENABLED=true.
PEST_DETECTION_DETECTOR_ENABLED false Nein Direkt-Detektor (Modus 1, Phase 2) ein/aus. Erfordert trainierten ONNX-Detektor.
PEST_DETECTION_DEMO_ENABLED false Nein Demo-Adapter (kein externer Service, kein echtes Modell). Zeigt den kompletten UI-Ablauf mit klar gekennzeichneten Platzhalter-Befunden, während das trainierte Backend extern blockiert ist. Nur zur Vorschau — nicht für echte Entscheidungen. Aktiv, wenn zusätzlich PEST_DETECTION_ENABLED=true.
PEST_DETECTION_CLOUD_ENABLED false Nein Cloud-Adapter (Kindwise) ein/aus. Erfordert PEST_DETECTION_CLOUD_API_KEY.
PEST_DETECTION_CLOUD_API_KEY — Nein API-Key für Kindwise (Cloud-Erkennung). Ohne Key ist der Cloud-Adapter deaktiviert.
PEST_DETECTION_PRIMARY_ADAPTER local_pest_symptom Nein Bevorzugter Adapter. Mögliche Werte: local_pest_symptom, local_pest_detector (Phase 2), kindwise_pest, demo_pest. Ein anderer Wert — etwa ein Tippfehler — wird beim Start abgelehnt: API und Worker starten dann gar nicht erst, statt still einen anderen Anbieter zu verwenden.
PEST_DETECTION_MAX_IMAGE_SIZE_MB 8 Nein Maximale Bildgröße in Megabyte. Größere Bilder werden mit HTTP 400 abgelehnt.

Self-Hosted-First

Der lokale Adapter (local_pest_symptom) benötigt keinen API-Key und erfordert keine Nutzereinwilligung. Cloud-Erkennung ist opt-in und einwilligungspflichtig (Consent-Zweck pest_detection_cloud).


CV-Krankheitsdiagnose (REQ-038)

Diese Variablen konfigurieren die optionale, self-hosted Foto-Diagnose für Krankheiten und Nährstoffmängel (abgegrenzt von der Schädlingserkennung oben). Das Feature ist standardmäßig deaktiviert; ohne CV_DIAGNOSIS_ENABLED=true bleibt der API-Endpunkt /status auf available: false, die App läuft uneingeschränkt weiter.

Variable Standard Pflicht Beschreibung
CV_DIAGNOSIS_ENABLED false Nein Gesamtschalter. Auf true setzen, um die Funktion zu aktivieren.
CV_CLASSIFIER_CONFIDENCE_SHOW 0.10 Nein Mindest-Konfidenz (0–1) für die Anzeige eines Treffers. Ergebnisse darunter werden verworfen.
CV_CLASSIFIER_CONFIDENCE_HIGHLIGHT 0.75 Nein Konfidenz-Schwelle (0–1), ab der ein Treffer visuell hervorgehoben wird. Löst kein automatisches Anlegen aus.
CV_PHENOTYPE_ENABLED true Nein PlantCV-Phänotyp-Kennzahlen (Blattfläche, Grün-Index, Verfärbungsanteil) im Inference-Service ein/aus.
CV_DIAGNOSIS_MAX_IMAGE_SIZE_MB 5 Nein Maximale Bildgröße in Megabyte. Größere Bilder werden mit HTTP 413 abgelehnt.

Der Klassifikator läuft im bestehenden Inference-Service und nutzt die dort bereits konfigurierte Anbindung (INFERENCE_SERVICE_URL, INTERNAL_SERVICE_TOKEN) — es sind keine zusätzlichen Verbindungsvariablen nötig.

Self-Hosted, kein Cloud-Adapter

Anders als bei der Schädlingserkennung gibt es für die CV-Krankheitsdiagnose (Stand dieser Version) keinen Cloud-Adapter — Fotos verlassen die Instanz nie. Die Einwilligung plant_diagnosis ist trotzdem erforderlich (Voll-Modus), weil ein Foto verarbeitet wird (siehe Datenschutz & DSGVO).

Lizenzhinweise

Das Modell wird auf dem CC-BY-4.0-lizenzierten PlantDoc-Datensatz fine-getunt; die Phänotyp-Pipeline nutzt PlantCV (MPL-2.0). Vollständige Attributionen: NOTICE.md.


Browser Push / PWA (VAPID)

Diese Variablen aktivieren den Browser-Push-Benachrichtigungskanal (channel_key: "pwa"). Sind alle drei Variablen leer, ist der Kanal deaktiviert — die Anwendung bleibt vollständig funktionsfähig, Nutzer sehen dann die Meldung "Nicht konfiguriert" in den Benachrichtigungseinstellungen.

Schritt-für-Schritt-Anleitung

Der Guide Browser-Push einrichten führt durch das Erzeugen des Schlüsselpaars, das Eintragen in Docker Compose bzw. Kubernetes und die Verifikation der Einrichtung.

Variable Standard Pflicht Beschreibung
VAPID_PUBLIC_KEY — Nein* VAPID-Public-Key (Base64url, 87 Zeichen). Wird an den Browser übermittelt und in der PWA-Subscription verwendet.
VAPID_PRIVATE_KEY — Nein* VAPID-Private-Key (Base64url oder PEM). Nur serverseitig — niemals im Frontend oder in Logs ausgeben.
VAPID_CONTACT_EMAIL — Nein* Kontakt-E-Mail für den Push-Service (Format: mailto:admin@example.com). Von den Push-Diensten (FCM, APNS, Mozilla) bei Problemen genutzt.
PWA_PUSH_ENDPOINT_ALLOWED_HOSTS — (leer) Nein SSRF-Härtung (SEC-001): Kommagetrennte Liste erlaubter Host-Suffixe für Web-Push-Endpunkte, z. B. fcm.googleapis.com,updates.push.services.mozilla.com. Leer (Standard) fällt auf eine HTTPS-Pflicht plus Ablehnung privater IP-Adressen zurück, sodass selbst gehostete Push-Server weiterhin funktionieren.

*Alle drei VAPID_*-Variablen müssen gesetzt sein, damit der Browser-Push-Kanal aktiv wird. Fehlt eine Variable, bleibt der Kanal deaktiviert. PWA_PUSH_ENDPOINT_ALLOWED_HOSTS ist unabhängig davon optional.

Schlüsselpaar generieren

npx web-push generate-vapid-keys

Ausgabe:

Public Key:
BNm...

Private Key:
8Kv...

Alternativ mit pywebpush (Python) — b64urlencode ist nötig, da v.public_key/v.private_key Schlüsselobjekte sind und erst die Serialisierung die Base64url-Strings liefert:

pip install pywebpush
python3 - <<'PY'
from py_vapid import Vapid
from py_vapid.utils import b64urlencode
from cryptography.hazmat.primitives import serialization

v = Vapid()
v.generate_keys()
pub_raw = v.public_key.public_bytes(
    serialization.Encoding.X962,
    serialization.PublicFormat.UncompressedPoint,
)
priv_raw = v.private_key.private_numbers().private_value.to_bytes(32, "big")
pub, priv = b64urlencode(pub_raw), b64urlencode(priv_raw)
assert pub_raw[0] == 0x04 and len(pub_raw) == 65 and len(pub) == 87, "invalid public key"
assert len(priv_raw) == 32 and len(priv) == 43, "invalid private key"
print("VAPID_PUBLIC_KEY =", pub)
print("VAPID_PRIVATE_KEY=", priv)
PY

Private Key serverseitig halten

Der VAPID_PRIVATE_KEY darf niemals im Frontend, in Logs oder in öffentlichen Konfigurationsdateien erscheinen. Speichere ihn als Kubernetes Secret oder Docker-Secret — analog zu JWT_SECRET_KEY.

Kubernetes Secret für VAPID

apiVersion: v1
kind: Secret
metadata:
  name: kamerplanter-vapid
type: Opaque
stringData:
  VAPID_PUBLIC_KEY: "BNm..."
  VAPID_PRIVATE_KEY: "8Kv..."
  VAPID_CONTACT_EMAIL: "mailto:admin@example.com"

Saison- & Überwinterungs-Automatik

Diese Variablen steuern die Schwellwerte der automatischen Saison-/Überwinterungserkennung (siehe Saison-Automatik). Sie betreffen nur die Live- und Klimatologie-Stufen der Erkennungs-Kaskade — der Kalender-Fallback ist davon unabhängig.

Variable Standard Pflicht Beschreibung
SEASON_PRE_WINTER_TEMP_C 5.0 Nein Temperaturschwelle (°C) für den Übergang von „Wachstumsphase" zu „Winter kündigt sich an".
SEASON_FROST_TEMP_C 2.0 Nein Temperaturschwelle (°C) für den Übergang in die Winterruhe.
SEASON_SPRING_TEMP_C 10.0 Nein Temperaturschwelle (°C) für den Übergang in die Frühjahrs-Rückholung.
SEASON_SIGNAL_THRESHOLD_DAYS 3 Nein Anzahl aufeinanderfolgender Signaltage, bevor ein Übergang ausgelöst wird (Oszillationsschutz).
SEASON_STATE_EVAL_ENABLED true Nein Schalter für den täglichen Auswertungs-Task. Auf false setzen, um die Saison-Automatik komplett zu deaktivieren.

Fehler-Tracking (optional)

Meldet Laufzeitfehler an einen Sentry-protokollkompatiblen Tracker (Referenz: GlitchTip). Ist SENTRY_DSN leer, passiert nichts — das SDK wird nie initialisiert, das Frontend lädt sein SDK-Bündel nicht einmal herunter. Mit DSN lädt das Frontend es erst, wenn die Person der Fehleranalyse zugestimmt hat. Ausführlich: Fehler-Tracking.

Variable Standard Pflicht Beschreibung
SENTRY_DSN — (leer) Nein Ingest-URL des Trackers, Form https://<public-key>@host/<projekt-id>. Enthält nur einen öffentlichen Schlüssel, kein Geheimnis. Leer = abgeschaltet.
SENTRY_ENVIRONMENT development Nein Stufe aus dem festen Vokabular development, e2e, staging, production. Alarmregeln filtern auf genau diese Werte; ein abweichender Wert meldet trotzdem, wird aber protokolliert.
SENTRY_RELEASE Komponente + Version Nein Image-Tag oder Commit-SHA. Ohne sie sind Regressionserkennung und die Zuordnung „welches Deployment war es" unmöglich.
SENTRY_SAMPLE_RATE 1.0 Nein Anteil der gemeldeten Ereignisse (0–1). 1.0 ist eine bewusste Entscheidung für dieses Aufkommen, keine unangetastete Voreinstellung. Unlesbare Werte fallen auf 1.0 zurück.

Alle vier Variablen gelten für Backend, Celery Worker und Beat, Inference- und Knowledge-Service sowie das Frontend. Im Frontend werden sie zur Laufzeit über runtime-config.js eingespeist, nicht ins Build gebacken — ein Image bedient damit alle Stufen.

Selbst gehosteter Tracker: NetworkPolicy nicht vergessen

Unter Kubernetes schließt die Egress-Regel des Backends die privaten Adressbereiche aus. Ein Tracker im eigenen Cluster oder LAN braucht deshalb eine zusätzliche Egress-Regel — sonst werden Ereignisse stillschweigend verworfen.

Vollständiges .env-Beispiel

# Datenbank
ARANGO_ROOT_PASSWORD=sicheres-root-passwort
ARANGODB_HOST=arangodb
ARANGODB_PORT=8529
ARANGODB_DATABASE=kamerplanter
ARANGODB_USERNAME=root
ARANGODB_PASSWORD=sicheres-root-passwort

# Cache / Queue
REDIS_URL=redis://valkey:6379/0

# Sicherheit (alle vier sind Pflicht-Secrets, Startup-Gate in Produktion)
JWT_SECRET_KEY=erzeugen-mit-openssl-rand-hex-32
FERNET_KEY=erzeugen-mit-Fernet.generate_key
ERASURE_TOMBSTONE_SALT=erzeugen-mit-openssl-rand-hex-32
LOG_PSEUDONYM_SALT=erzeugen-mit-openssl-rand-hex-32
# Default true; nur ohne ausgehenden Mailversand auf false setzen
REQUIRE_EMAIL_VERIFICATION=true
# Wer ein Konto anlegen darf: open | invite_only | closed (Default open)
REGISTRATION_MODE=open

# CORS
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]

# Betriebsmodus
KAMERPLANTER_MODE=full
DEBUG=false

# E-Mail (Entwicklung)
EMAIL_ADAPTER=console

# mDNS Discovery (LAN-only, opt-in)
# MDNS_ENABLED=false
# INSTANCE_ID=

# Optionale externe APIs
PERENUAL_API_KEY=
HA_URL=
HA_ACCESS_TOKEN=

# Knowledge Service — Re-Ranking (leer = deaktiviert)
RERANKER_URL=
RERANKER_INITIAL_K=15
RERANKER_MAX_DOCUMENT_CHARS=500
RERANKER_TOP_K=5

# KI-Assistent (instanzweit deaktiviert, solange nicht explizit aktiviert)
AI_FEATURES_ENABLED=false
KNOWLEDGE_SERVICE_ENABLED=false
KNOWLEDGE_SERVICE_URL=http://knowledge-service:8000

# Foto-Identifikation (leer = Feature deaktiviert)
# PLANTNET_API_KEY=
# IDENTIFICATION_RATE_LIMIT_PER_USER_DAY=50

# Browser Push / PWA (leer = Kanal deaktiviert)
# VAPID_PUBLIC_KEY=
# VAPID_PRIVATE_KEY=
# VAPID_CONTACT_EMAIL=mailto:admin@example.com

Object Storage (NFR-013)

Diese Variablen konfigurieren den Storage-Adapter für Binärdaten (Fotos, Importe, Exporte). Das aktive Backend wird durch STORAGE_BACKEND bestimmt. Standardmäßig ist local-fs aktiv — keine weitere Konfiguration nötig.

Weitere Hintergrundinformationen: Speicher konfigurieren (Object Storage) und Helm Charts — Storage-Konfiguration.

Allgemeine Storage-Einstellungen

Variable Standard Pflicht Beschreibung
STORAGE_BACKEND local-fs Nein Aktives Backend: local-fs oder s3. Ein anderer Wert wird beim Start abgelehnt.
STORAGE_MAX_FILE_SIZE_MB 25 Nein Maximale Upload-Größe in Megabyte (gilt für alle Kategorien, überschreibbar per Kategorie)
STORAGE_PRESIGN_TTL_SECONDS 900 Nein Gültigkeitsdauer von Pre-Signed URLs in Sekunden (max. 3600)
UPLOAD_SPOOL_BUDGET_MB 52 Nein Wie viele MiB gleichzeitige Uploads zusammen in /tmp zwischenspeichern dürfen. Gezählt werden die Bytes, die tatsächlich angekommen sind. Muss mindestens STORAGE_MAX_FILE_SIZE_MB + 1 sein, sonst startet das Backend nicht. Halte den Wert unter der Größe von /tmp im Backend-Pod (im Chart 64 MiB). Das Helm-Chart setzt ihn auf 52 oder, bei größerer Upload-Grenze, auf storage.maxFileSizeMb + 1. Er begrenzt auch die Upload-Grenze des Mandanten-Imports (TENANT_IMPORT_MAX_UPLOAD_MB): Für Archive bis N MiB hebst du ihn auf mindestens N + 1 an, zusammen mit /tmp und dem Speicherlimit (Issue #2138).
UPLOAD_SPOOL_WAIT_SECONDS 30 Nein Wie lange ein Upload auf freies Budget wartet. Danach antwortet das Backend mit 503 (UPLOAD_CAPACITY_EXHAUSTED) und Retry-After.
STORAGE_ALLOWED_MIME_TYPES (Liste) Nein Kommagetrennte globale Whitelist erlaubter MIME-Types
STORAGE_ALLOWED_MIME_TYPES_<CATEGORY> (pro Kategorie) Nein Kategorie-spezifische Whitelist, z. B. STORAGE_ALLOWED_MIME_TYPES_IMPORT=text/csv
STORAGE_VIRUS_SCAN_ENABLED false Nein Virenscan via ClamAV-REST-Wrapper aktivieren
STORAGE_VIRUS_SCAN_ENDPOINT (leer) Nein URL des ClamAV-REST-Wrappers
STORAGE_STRIP_EXIF true Nein Entfernt EXIF-/GPS-Metadaten aus Bild-Uploads global beim Speichern (NFR-013 §5.1). Es gibt keine Kategorie-spezifische Override-Variable — anders als bei den MIME-Whitelists ist dies ein einzelner, globaler Schalter. Solange er aktiv ist, lehnt der Upload Bildtypen ab, deren Metadaten die Bereinigung nicht entfernen kann (HEIC/HEIF, GIF), auch wenn eine MIME-Whitelist sie zulässt. Bilder über 40 Megapixel werden unabhängig von diesem Schalter mit 413 abgelehnt.
STORAGE_TENANT_QUOTA_MB 2048 Nein Speicherkontingent pro Mandant in Megabyte. 0 deaktiviert das Kontingent (unbegrenzt).
STORAGE_MAX_PHOTOS_PER_INSTANCE 50 Nein Maximale Anzahl Galerie-Fotos je Pflanzeninstanz (REQ-034). 0 deaktiviert das Limit.
STORAGE_TASK_PHOTO_ORPHAN_HOURS 0 Nein Wie lange ein Aufgabenfoto unreferenziert liegen darf, bevor der nächtliche Aufräum-Task es löscht (#1393). 0 schaltet ihn ab — und ist der Auslieferungswert: Der Job löscht Daten über eine Referenz-Historie, die jede Client-Version und eine manuelle Migration umfasst, und geht in seinem ersten Release bewusst nicht scharf. Zum Einschalten eine positive Stundenzahl setzen; jeder Upload ist kurzzeitig unreferenziert (der Anhang wird geschrieben, bevor das Formular ihn verknüpft), der Wert ist also ein weiter Sicherheitsabstand und kein Feineinstellungs-Regler — 48 ist die Größe, um die herum gebaut und getestet wurde.
STORAGE_RECONCILE_DELETE_ENABLED false Nein Schaltet das Löschen des nächtlichen Objekt-Abgleichs ein (#1834). Der Task reconcile_orphaned_storage_objects (täglich 04:10) listet den Object Store unter t/ und sucht Objekte, die kein Anhang-Datensatz mehr hält — Reste eines fehlgeschlagenen Storage-Aufrufs nach einer Löschung, eines Datensatzes im Fenster einer Kontolöschung oder einer Schädlingsreferenz in einem verlassenen Mandanten. Ausgelieferter Wert false: Der Lauf berichtet nur. Im Worker-Log steht je Lauf die Zeile reconcile_orphaned_storage_objects_audit mit scanned, found (kein Datensatz hält das Objekt), young (jünger als die Marge, bleibt), undated (kein lesbares Alter, bleibt), eligible, would_delete, deleted, failed, freed_bytes und truncated; sie enthält nie Mandanten-, Nutzerschlüssel oder Objektpfade. Erst wenn would_delete über mehrere Läufe nur Objekte beschreibt, die du erwartest, true setzen; dann löscht der Lauf genau diese Objekte samt Vorschaubildern und nie eines, das ein Datensatz hält.
STORAGE_RECONCILE_MIN_AGE_HOURS 24 Nein Mindestalter eines Objekts ohne Datensatz, bevor der Abgleich es beurteilt (#1834). Ein Upload schreibt das Objekt vor dem Datensatz, ein frisches Objekt ohne Datensatz ist also kurzzeitig normal. Die Untergrenze ist 1 (Request plus die Wiederholungen des Thumbnail-Tasks); kleinere Werte lehnt der Start ab. Ein Sicherheitsabstand, kein Regler.
STORAGE_RECONCILE_MAX_OBJECTS_PER_RUN 200000 Nein Höchstzahl der Objekte, die ein Lauf des Abgleichs prüft (Untergrenze 1000). Ein größerer Bestand wird über mehrere Nächte abgedeckt: truncated: true im Bericht, der nächste Lauf setzt an der gemerkten Stelle fort (Token in Valkey; fehlt er, beginnt der Lauf von vorn).
STORAGE_RECONCILE_MAX_ORPHAN_FRACTION 0.5 Nein Notbremse des Objekt-Abgleichs (#1834): Ist auf einer Seite (ab 20 Objekten) mehr als dieser Anteil der Objekte ohne Datensatz, löscht der Lauf dort nichts und meldet brake_tripped: true. Das fängt einen Katalog ab, der gültig, aber leer antwortet — falsche Datenbank über einem bestehenden Bucket, ein Restore, der älter als der Speicher ist, zwei Installationen auf einem Bucket —, bei dem keine Abfrage fehlschlägt und jedes Objekt „verwaist“ aussähe. Wertebereich 0 < x <= 1. Löst die Bremse aus, den Katalog prüfen, bevor du das Löschen weiter eingeschaltet lässt.

Standard-MIME-Whitelist pro Kategorie:

Kategorie Erlaubte MIME-Types Max-Größe
diary, ipm, harvest, post_harvest, task, id_recognition, plant image/jpeg, image/png, image/webp 25 MB
import text/csv, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 50 MB
export application/pdf, text/csv, application/zip 200 MB
tenant_export application/zip 5 GB

Backend: Lokales Dateisystem (local-fs)

Variable Standard Pflicht Beschreibung
STORAGE_LOCAL_FS_ROOT /data/attachments Nein Mount-Pfad innerhalb des Containers
STORAGE_LOCAL_FS_PUBLIC_BASE_URL (leer) Ja* Vollständige URL des Token-Download-Endpunkts, z. B. https://api.kamerplanter.example.com/api/v1/attachments/token. Muss auf https://<host>/api/v1/attachments/token zeigen.
STORAGE_LOCALFS_SIGNING_SECRET (ephemer) Ja** Geheimer Schlüssel für Token-Signaturen. Pflicht bei mehr als einer Replica, sonst können Tokens von anderen Pods nicht validiert werden.

*Pflicht, damit local-fs Token-Downloads einlösen kann. **Pflicht bei Multi-Replica-Betrieb.

Signing-Secret als Kubernetes-Secret speichern

Der STORAGE_LOCALFS_SIGNING_SECRET ist ein kryptographisches Secret und darf nicht im Klartext in values.yaml oder Git committet werden. Anlegen als Kubernetes Secret:

kubectl create secret generic kamerplanter-storage-signing \
  --from-literal=STORAGE_LOCALFS_SIGNING_SECRET="$(openssl rand -hex 32)" \
  --namespace kamerplanter

Backend: S3-kompatibel (s3)

Variable Standard Pflicht Beschreibung
STORAGE_S3_ENDPOINT_URL (leer) Ja Vollständige Endpunkt-URL, z. B. https://s3.eu-central-1.amazonaws.com
STORAGE_S3_REGION (leer) Ja Region, z. B. eu-central-1 (auch bei MinIO erforderlich)
STORAGE_S3_BUCKET (leer) Ja Bucket-Name (muss vorab angelegt sein)
STORAGE_S3_ACCESS_KEY_ID (leer) Ja Access Key (aus External Secrets Operator — niemals im Klartext in Git)
STORAGE_S3_SECRET_ACCESS_KEY (leer) Ja Secret Access Key (aus External Secrets Operator — niemals im Klartext in Git)
STORAGE_S3_USE_PATH_STYLE false Nein true für MinIO und die meisten Nicht-AWS-Anbieter
STORAGE_S3_FORCE_TLS true Nein Plain-HTTP verbieten; in Dev-Umgebungen auf false setzen
STORAGE_S3_KMS_KEY_ID (leer) Nein Optionaler Customer-Managed Key für serverseitige Verschlüsselung (SSE-KMS)
STORAGE_S3_ALLOW_PRIVATE_ENDPOINT false Nein true für in-Cluster MinIO, das nicht öffentlich erreichbar ist

S3-Credentials niemals in Git oder values.yaml

STORAGE_S3_ACCESS_KEY_ID und STORAGE_S3_SECRET_ACCESS_KEY sind Secrets und werden ausschließlich über den External Secrets Operator (ESO) oder Kubernetes Secrets bereitgestellt. Weitere Details: Helm Charts — Storage-Konfiguration.

Beispielkonfigurationen

STORAGE_BACKEND=s3
STORAGE_S3_ENDPOINT_URL=https://s3.eu-central-1.amazonaws.com
STORAGE_S3_REGION=eu-central-1
STORAGE_S3_BUCKET=mein-kamerplanter-bucket
STORAGE_S3_ACCESS_KEY_ID=<aus Secret>
STORAGE_S3_SECRET_ACCESS_KEY=<aus Secret>
STORAGE_S3_USE_PATH_STYLE=false
STORAGE_S3_FORCE_TLS=true
STORAGE_BACKEND=s3
STORAGE_S3_ENDPOINT_URL=http://minio.kamerplanter.svc:9000
STORAGE_S3_REGION=us-east-1
STORAGE_S3_BUCKET=kamerplanter
STORAGE_S3_ACCESS_KEY_ID=<aus Secret>
STORAGE_S3_SECRET_ACCESS_KEY=<aus Secret>
STORAGE_S3_USE_PATH_STYLE=true
STORAGE_S3_FORCE_TLS=false
STORAGE_S3_ALLOW_PRIVATE_ENDPOINT=true
STORAGE_BACKEND=s3
STORAGE_S3_ENDPOINT_URL=https://fsn1.your-objectstorage.com
STORAGE_S3_REGION=eu-central
STORAGE_S3_BUCKET=mein-kamerplanter-bucket
STORAGE_S3_ACCESS_KEY_ID=<aus Secret>
STORAGE_S3_SECRET_ACCESS_KEY=<aus Secret>
STORAGE_S3_USE_PATH_STYLE=false
STORAGE_S3_FORCE_TLS=true

Häufige Fragen

Kann ich Umgebungsvariablen in Kubernetes als Secrets hinterlegen?

Ja. Verwenden Sie Kubernetes Secrets für sensible Werte (ARANGODB_PASSWORD, JWT_SECRET_KEY) und referenzieren Sie sie im Deployment-Manifest über valueFrom.secretKeyRef.

Wo kann ich prüfen, welche Werte das Backend tatsächlich verwendet?

Mit DEBUG=true loggt das Backend beim Start alle geladenen Einstellungen. Alternativ im Container:

docker compose exec backend python -c "from app.config.settings import settings; print(settings.model_dump())"
Passwörter und Secrets werden dabei nicht im Klartext angezeigt.


Siehe auch