Selbst hosten
Node und ein beschreibbares Verzeichnis sind die ganze Voraussetzung. Die folgenden Aussagen stammen aus den daneben genannten Dateien; was nicht bestätigt werden konnte, steht nicht hier.
Einstiegspunkte
- package.json verlangt Node >= 22.5. Der SQLite-Speicher ist Nodes eigenes node:sqlite-Modul.
- Es gibt überhaupt keine Abhängigkeitsliste: Apps, Pakete, Skripte und Tests laufen mit Nodes Standardbibliothek, und das Dockerfile installiert nichts.
- Drei Prozesse existieren; jeder öffnet die Datenbank und führt beim Start die Migration aus.
node apps/web/server/main.mjs # hub + console + embedded API
API_EMBED=false node apps/api/src/main.mjs # API only, no pages
node apps/worker/src/run.mjs # probes, latency, rollup, housekeeping
Datenspeicher
DATABASE_URL=sqlite:./data/llmeu.db braucht keinen Server; Verzeichnis und Datei werden beim ersten Öffnen angelegt. Das Handle läuft im WAL-Modus mit aktivierten Fremdschlüsseln und 5 s busy_timeout.
packages/shared/src/db/schema.sql ist das Schema. Es ist ausschließlich anlegend und idempotent, erneutes Ausführen ist also sicher; eine Spalte, die einer bestehenden Tabelle hinzugefügt wird, muss zusätzlich im additiven Schritt in packages/shared/src/db/index.mjs stehen.
Eine postgres://-URL nimmt einen separaten Adapterzweig. Dieser Zweig benötigt das Paket pg, das keine deklarierte Abhängigkeit ist, und der Kopf von schema.sql sagt, der Postgres-Pfad sei als ungeprüft zu behandeln, bis ein echter Treiber und eine getestete Migration existieren. SQLite ist der Pfad, den dieses Repository ausführt.
Betreiber-CLI
node scripts/db.mjs liest DATABASE_URL und braucht keine Dienste. Befehle:
| Befehl | Wirkung |
|---|---|
| node scripts/db.mjs migrate | Wendet schema.sql an und meldet die Anzahl der Anweisungen. |
| node scripts/db.mjs seed | Fügt Katalog und Demo-Organisation ein oder aktualisiert sie und schreibt den Demo-Schlüssel mit Modus 0600 nach data/demo-key.txt. |
| node scripts/db.mjs reset | Nur für SQLite: entfernt die Datenbankdatei samt -wal und -shm, migriert und befüllt dann. Bei Postgres verweigert es. |
| node scripts/db.mjs status | Gibt die Engine, die Tabellenzahl und die Zeilenzahl jeder nicht leeren Tabelle aus. |
| node scripts/db.mjs demo-key | Stellt einen neuen Schlüssel für die Demo-Organisation aus und gibt ihn einmal aus. |
| node scripts/db.mjs passwd <email> | Setzt das Passwort eines Nutzers vom Server aus und löscht jede Sitzung dieses Nutzers. Das ist der Wiederherstellungsweg, wenn kein Mail-Transport konfiguriert ist. Ohne --password wird eines erzeugt und einmal ausgegeben. |
Umgebung
Gruppiert wie in .env.example.
| Gruppe | Variablen | Hinweise |
|---|---|---|
| URLs und Ports | APP_URL, API_URL, WEB_PORT, API_PORT, API_EMBED, PUBLIC_API_URL | Der API-Port ist standardmäßig 8088, nicht 8080. API_EMBED=false gilt für eine Bereitstellung, die die API als eigenen Prozess betreibt; PUBLIC_API_URL ist die in Doku-Beispielen gezeigte Basis. |
| Datenspeicher | DATABASE_URL, REDIS_URL | DATABASE_URL wählt den Speicher; REDIS_URL ist für die Job-Queue reserviert und wird vom v1-Codepfad nicht gelesen. |
| Inferenz | INFERENCE_MOCK, GPU_ENABLED, VLLM_BASE_URL, VLLM_REGION, VLLM_MODEL_VERSION, VLLM_BASE_URL_<REGION>, DEFAULT_REGION, PARTNER_* | Siehe den Inferenzabschnitt unten; PARTNER_* umfasst Partner-Basis-URL, Region, Souveränitätsklasse und API-Schlüssel. |
| Abrechnung | FX_EUR_USD, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET | Das Ledger wird in USD-Mikros geführt; EUR wird zur Anzeige mit FX_EUR_USD abgeleitet. |
| MAIL_FROM, MAIL_SMTP_HOST, MAIL_SMTP_PORT, MAIL_SMTP_SECURE, MAIL_SMTP_USER, MAIL_SMTP_PASS, MAIL_TIMEOUT_MS, MAIL_API_URL, MAIL_API_KEY | MAIL_SMTP_HOST für SMTP setzen, oder MAIL_API_URL mit MAIL_FROM für ein HTTP-Relay, oder nichts davon. | |
| Sicherheit | NODE_ENV, LLMEU_IP_SALT, LLMEU_DEMO_PASSWORD, LLMEU_KEY_ENV | NODE_ENV=production markiert Sitzungs-Cookies als Secure. LLMEU_IP_SALT salzt die gehashte Client-IP und sollte je Bereitstellung geändert werden. LLMEU_KEY_ENV stellt generierten Schlüsseln llmeu_live_ oder llmeu_test_ voran. |
| Sonstiges | LOG_LEVEL | LOG_LEVEL setzt die minimale Log-Stufe. |
Docker
Compose und das Image sind optional: Die Testsuite des Repositories braucht kein Docker, und der primäre Weg ist ein lokaler Prozess ohne Dienste.
- Das Standardprofil startet web: Ports 3000 und 8088, SQLite auf dem Volume llmeu-data unter /app/data, die eingebettete API und einen Healthcheck auf /healthz.
- Dasselbe Image startet den Worker mit node apps/worker/src/run.mjs, wartet auf die Gesundheit von web und nutzt dasselbe Volume llmeu-data.
- Zwei zusätzliche Profile sind standardmäßig aus: postgres (postgres:16-alpine) und redis (redis:7-alpine, reserviert und vom v1-Codepfad nicht gelesen).
- Das Profil gpu startet vllm/vllm-openai mit NVIDIA-Gerätereservierung und bindet Gewichte schreibgeschützt vom Host ein; es ist nicht standardmäßig aktiv, denn EU-Inferenz ohne GPU zu behaupten wäre falsch.
- Das Dockerfile ist FROM node:24-alpine mit tini, ohne Build-Schritt und ohne Abhängigkeitsinstallation, läuft als Benutzer node, schreibt nur unter /app/data, exponiert 3000 und 8088 und startet apps/web/server/main.mjs.
Backups
node scripts/backup.mjs ohne Flags schreibt ./backups/llmeu-<stamp>.db. SQLite wird mit VACUUM INTO kopiert, nie mit cp, denn eine laufende WAL-Datenbank kann mit cp einen zerrissenen oder veralteten Stand liefern.
node scripts/backup.mjs # ./backups/llmeu-<stamp>.db
node scripts/backup.mjs --out /srv/backups/ # explicit destination
node scripts/backup.mjs --verify ./backups/llmeu-<stamp>.db
node scripts/backup.mjs --restore ./backups/llmeu-<stamp>.db --yes
node scripts/backup.mjs --list
Jedes SQLite-Backup wird geprüft, bevor das Skript Erfolg meldet: Es wird in eine temporäre Kopie wiederhergestellt, PRAGMA integrity_check muss ok liefern, das Schema wird erneut angewendet und die Zeilenzahlen für models, organizations und users werden ausgegeben.
Bei einer postgres://-URL nutzt das Skript pg_dump --format=custom und braucht pg_dump im PATH; ein Dump lässt sich nicht als Datenbank öffnen, seine Zeilenzahlen brauchen also eine Testwiederherstellung.
Transaktionsmail — Passwort-Zurücksetzen, Adressbestätigung, Einladungen — nutzt MAIL_SMTP_* oder MAIL_API_URL mit MAIL_FROM. Der Transport wird einmal beim Start gewählt und auf jeder Seite gemeldet, die Mail braucht.
Auch ohne beides ist ein unterstützter Zustand: Registrierung, Anmeldung, Passwortänderung, Teams und Einladungen funktionieren, aber Mail zum Zurücksetzen und zur Bestätigung kann die Bereitstellung nicht bearbeiten. Die Seiten sagen das, und node scripts/db.mjs passwd <email> ist der Wiederherstellungsweg.
Inferenz-Backends
Eine Endpunktzeile nennt Runtime und Region; die Backend-Registry wählt den Adapter nach Region, ein Endpunkt kann also nur von einer Maschine in der genannten Region beantwortet werden.
- INFERENCE_MOCK=true behält das deterministische Mock-Backend, das keine GPU und keine Gewichte braucht.
- GPU_ENABLED=false hält verwaltete Backends heraus; mit GPU_ENABLED=true gilt VLLM_BASE_URL für die eine Region, die VLLM_REGION nennt.
- Eine Bereitstellung mit mehreren Regionen setzt eine Variable je Region, benannt nach dem Regions-Slug (zum Beispiel VLLM_BASE_URL_DE_FRA). Ein Endpunkt in einer Region ohne konfiguriertes Backend wird abgelehnt statt von einer Maschine in einer anderen Region bedient.
- Partner-Backends stammen aus PARTNER_BASE_URL_<REGION> und übernehmen PARTNER_SOVEREIGNTY und PARTNER_API_KEY; der Server erlaubt Partner-Nutzung, wenn PARTNER_BASE_URL gesetzt ist.
Ohne GPU antwortet das Mock-Backend. Seine Region ist dev-mock, und seine Ausgabe sagt, dass es das Mock-Backend ist, dass keine echten Gewichte liefen und dass First-Party-Inferenz VLLM_BASE_URL und das Flag gpu_enabled braucht. Ein Mock-Erfolg, der echt aussieht, wäre das eine, was dieses Produkt nie ausliefern darf.