Nessie installieren
Betreiben Sie Nessie auf einer Infrastruktur Ihrer Wahl, vom einzelnen Rechner bis zu einer verwalteten Cloud.
Was Sie vor dem Start brauchen
Nessie ist ein pnpm-Monorepo aus Node-Diensten und zwei statischen Bundles. Egal, auf welchem Weg Sie es betreiben: Dieselben vier Dinge müssen zuerst vorhanden sein.
- PostgreSQL mit
pgvector. Nicht optional und nicht austauschbar. Die Migrationen führenCREATE EXTENSION vectoraus und legen Spalten vom Typvector(1024)an. Die Produktion läuft mitpgvector/pgvector:pg17. - Node 22 und pnpm. Jedes Dockerfile in
infrastructure/docker/baut aufnode:22-slimauf, und das Repository legt seinen Paketmanager in der Root-package.jsonfest (pnpm@10.22.0). - Ein OpenAI-kompatibler Modell-Endpunkt und ein Schlüssel dafür. Agenten und Embeddings laufen beide darüber. Siehe „Inferenz konfigurieren“ weiter unten.
- S3-kompatibler Objektspeicher für jede Bereitstellung, die nicht die lokale Entwicklungsumgebung ist. Uploads auf der lokalen Festplatte werden außerhalb des Modus
localabgelehnt, und diese Ablehnung ist ein Fehler beim Start statt eines eingeschränkten Starts. Siehe „Ein Rechner oder eine verwaltete Cloud“.
Docker und ein Reverse Proxy werden für den weiter unten beschriebenen Container-Weg benötigt, und eine Domain mit TLS für alles, was aus dem Internet erreichbar ist.
Die Hardware-Dimensionierung, also Mindestwerte für CPU, Arbeitsspeicher und Speicherplatz für eine bestimmte Zahl von Personen, steht nirgends im Repository. Deshalb nennt auch diese Seite keine Werte.
Die Komponenten und ihr Zusammenspiel
| Komponente | Was sie ist | Eingehender Verkehr |
|---|---|---|
api/ | Die HTTP-/WebSocket-API auf Basis von Fastify. Jeder Client kommuniziert ausschließlich mit ihr. | Ja |
worker/ | Agenten-Orchestrierung und Hintergrundläufe. Fragt die Job-Warteschlange ab und bindet keinen Port. | Nein |
admin/ | Die Produktoberfläche: ein statisches Bundle, mit Vite gebaut und von nginx ausgeliefert. | Ja |
web/ | Die öffentliche Website. Statisch, optional und unabhängig vom Betrieb des Produkts. | Ja |
| PostgreSQL | Die Datenbank, die Job-Warteschlange und der Echtzeit-Bus, alle drei in einem. | Nein |
| Objektspeicher | Anhänge, Avatare und Dateien der Wissensdatenbank. S3-kompatibel; in der Produktion läuft MinIO. | Nein |
gateway/ | Ein optionales Push-Relay für APNs und FCM. Es ist ausgeschaltet, sofern Sie es nicht bewusst aktivieren. | Ja |
API und Worker sind ein Image. Die beiden Pakete hängen eng zusammen, deshalb baut Dockerfile.app den gesamten Workspace einmal, und der Worker-Container überschreibt den Befehl mit node worker/dist/index.js. Im Modus local gibt es gar keinen Worker-Container: Die API führt den Worker in ihrem eigenen Prozess aus.
Es gibt kein Redis. Die Job-Warteschlange und der Echtzeit-Transport laufen beide auf Postgres. Das ist eine bewusste Entscheidung, kein Versäumnis. Der Google-Cloud-Zweig entfernt aus demselben Grund seine Pub/Sub- und Redis-Module.
Ports
Bei der lokalen Entwicklung liegt die API standardmäßig auf 5454, die Admin-Oberfläche auf 5455. Ein Checkout kann ein eigenes Paar verwenden, indem Sie NESSIE_API_PORT und NESSIE_ADMIN_PORT in der Umgebung oder in der .env des Repositorys setzen; jedes Werkzeug im Repository, auch die Browser-Testsuiten, liest dieselben beiden Werte. Wählen Sie das Paar vor dem Start und behalten Sie es bei: Wird ein laufender Dienst auf einen anderen Port verschoben, spricht die Admin-Oberfläche mit der falschen API.
Für die Produktion gilt etwas anderes, und die Regel oben bleibt dort unverändert. Der API-Container lauscht intern auf 5554 (festgelegt mit NESSIE_API_PORT), das Push-Relay auf 5556. Beide liegen hinter dem Reverse Proxy.
Auf einem Rechner betreiben, zur Entwicklung
Das Repository enthält eine Compose-Datei, die nur eine Datenbank umfasst. Mehr braucht die lokale Entwicklungsschleife nicht.
pnpm install
docker compose -f infrastructure/compose/docker-compose.yml up -d postgresDieser Container stellt Postgres auf 55432 bereit; Benutzer, Passwort und Datenbank heißen alle nessie. Legen Sie den Verbindungsstring in einer .env im Root des Repositorys ab, denn das Entwicklungsskript der API liest sie. Wenden Sie danach die Migrationen an und bauen Sie den Worker einmal, denn im Modus local lädt die API den Worker aus ihrem gebauten dist:
DATABASE_URL=postgresql://nessie:nessie@localhost:55432/nessie
pnpm --filter @nessie/api prisma:migrate:deploy
pnpm --filter @nessie/worker build
pnpm devpnpm dev startet API und Admin-Oberfläche parallel mit Hot Reload auf 5454 und 5455. Prüfen Sie mit GET /api/health, ob die API antwortet.
Postgres und die pgvector-Anforderung
Drei Erweiterungen legen die Migrationen selbst an: vector, pg_trgm und pgcrypto. Deshalb muss die Rolle, die sie ausführt, Erweiterungen anlegen dürfen. Bei einem verwalteten Postgres ist das der erste Punkt, den Sie prüfen sollten.
Die Breite der Vektoren ist Teil des Schemas, keine Einstellung. thoughts.embedding, thought_recalls.query_embedding und knowledge_page_chunks.embedding sind Spalten vom Typ vector(N). N steht einmal als EMBEDDING_DIMENSIONS in packages/schemas/src/embedding.ts, derzeit 1024, der native Wert des Embedding-Modells, das die Produktion verwendet. Jede Embedding-Anfrage sendet diese Breite mit. Ein Anbieter, der mit einer anderen Breite antwortet, schlägt deshalb laut fehl, statt Vektoren zu liefern, die die Datenbank ablehnt. Auf ein Embedding-Modell mit anderer Breite zu wechseln heißt: die Konstante ändern, eine Migration schreiben, die die drei Spalten neu typisiert, und neu einbetten. Vektoren unterschiedlicher Breite lassen sich nicht umwandeln. Die Migration setzt sie deshalb auf null, statt sie zu kürzen.
Wenn Sie Nessie neben anderen Anwendungen in einem gemeinsamen Docker-Netzwerk betreiben, geben Sie dessen Datenbank-Container einen eigenen Namen. Der Compose-Dienst des Projekts heißt nessie-postgres und niemals postgres. Der Dienstname wird im gemeinsamen Netzwerk zum DNS-Alias, und bei einer Kollision verteilt postgres:5432 die Anfragen im Round-Robin-Verfahren auf zwei Datenbanken.
Konfiguration
Die Laufzeitkonfiguration ist geschichtet: Eine Datei nessie.config.json, schreibgeschützt in API und Worker eingebunden, und darüber Umgebungsvariablen, die Vorrang haben. Die Zuordnung zwischen beiden ist ConfigEnvMap in packages/config, und sie ist die maßgebliche Liste.
NESSIE_MODE legt die Form der Bereitstellung fest: local (der Standard und der Weg für Entwickler), hosted oder selfHosted, den Wert, den eine selbst gehostete Installation setzt. Er deaktiviert den Dev-Login und verlangt eine CORS-Allowlist.
Ohne diese Einstellungen startet eine selfHosted-Bereitstellung nicht oder funktioniert nicht:
| Variable | Warum |
|---|---|
DATABASE_URL / NESSIE_DB_URL | Der Verbindungsstring für Postgres. |
NESSIE_AUTH_SECRET | 32 Byte als Hex (openssl rand -hex 32). Signiert Sessions und Bootstrap-Token. Außerhalb des Modus local bricht der Start ohne ihn ab. Der Wert muss auf jedem Replikat und über alle Deployments hinweg derselbe sein, sonst endet bei jedem Release jede Session. |
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSION | Ein JSON-Schlüsselring und das Label, das die aktive Root auswählt. Außerhalb des Modus local ist beides erforderlich. Dies ist eine eigene Root, getrennt vom Signaturgeheimnis: Der Schlüsselring verschlüsselt jedes dauerhafte Geheimnis, das Nessie speichert. Verwenden Sie das eine nicht als das andere. |
NESSIE_STORAGE_PROVIDER | Muss s3 sein. filesystem ist der Standard und wird in den Modi hosted und selfHosted abgelehnt. Eine Bereitstellung, die hier nichts setzt, startet deshalb nicht, statt Uploads auf eine Festplatte zu schreiben, die kein anderer Container lesen kann. |
NESSIE_CORS_ORIGINS | Der Ursprung der Admin-Oberfläche. Admin und API sind getrennte Ursprünge, und die API muss den Ursprung, den sie bedient, auf ihre Allowlist setzen. |
NESSIE_API_PUBLIC_URL, NESSIE_ADMIN_PUBLIC_URL | Dient dazu, OAuth-Redirect-URIs außerhalb einer HTTP-Anfrage zu erzeugen. Ohne den ersten Wert wirft die API zur Laufzeit einer Anfrage einen Fehler. |
NESSIE_MODEL_PROVIDER, _BASE_URL, _API_KEY | Wohin die Inferenz geht. Siehe unten. |
infrastructure/compose/.env.prod.example zeigt die genaue Form aller Einstellungen; die optionalen Blöcke für Objektspeicher, Secret-Vault, SSO und angebundene E-Mail sind an ihrer Stelle auskommentiert. Den vollständigen Katalog mit allen optionalen Einstellungen finden Sie in docs/deployment/configuration.md.
Deployment mit Docker Compose
Kopieren Sie .env.prod.example als .env neben docker-compose.prod.yml und füllen Sie sie aus. Starten Sie dann die Datenbank, bauen, migrieren und starten Sie. Die Reihenfolge ist wichtig: Migrationen und Seeds laufen gegen eine Datenbank, die bereits läuft, bevor irgendein Dienst Anfragen beantwortet.
COMPOSE="docker compose -f infrastructure/compose/docker-compose.prod.yml"
bash infrastructure/compose/ensure-encryption-key-ring.sh infrastructure/compose/.env
$COMPOSE up -d nessie-postgres
$COMPOSE build nessie-api nessie-admin nessie-web
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api prisma:migrate:deploy
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api seed:connectors
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api seed:apps
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api reconcile
$COMPOSE up -dDie Produktions-Dockerfiles führen vor dem Bauen das Lint jedes Pakets aus. Ein Lint-Fehler ist deshalb ein Build-Fehler.
Der Schritt reconcile ist nicht optional
reconcile legt für jede Organisation die Standard-Policy-Regeln an und ergänzt nachträglich die Freigaben für geschützte MCP-Tools und die Tools des Personal Assistant. Früher erledigte die API das beim Start auf jedem Replikat. Jetzt verbindet sie sich, lauscht und tut sonst nichts. Ohne diesen Schritt hat die erste Organisation keine Policy-Regeln, und jede Agentenbindung wird verweigert. Der Schritt ist idempotent, daher kostet es bei jedem Deployment nichts, ihn auszuführen. Ein von Hand eingespieltes Upgrade muss ihn ebenfalls ausführen.
Das erste Inhaberkonto
Eine frische selfHosted-Installation hat keine Benutzer. Deshalb erzeugt die API ein einmal gültiges Bootstrap-Token und protokolliert eine Einrichtungs-URL. Das Token liegt in Postgres und nicht in einem Prozess, damit alle Replikate sich darüber einig sind.
docker compose -f infrastructure/compose/docker-compose.prod.yml logs nessie-api 2>&1 | grep bootstrapÖffnen Sie https://<your-admin-host>/bootstrap?token=<token> und legen Sie das Inhaberkonto an. Das Token läuft nach 15 Minuten ab. Starten Sie den API-Dienst neu, um ein neues zu erzeugen.
Wenn Sie SSO konfigurieren, entfällt der Bootstrap-Schritt vollständig. Der Bootstrap-Modus wird unterdrückt, sobald ein externer Auth-Provider aktiv ist, sonst würde er die Anmeldeseite übernehmen. Die erste Person, die sich über SSO anmeldet, legt das Standard-Team an und wird dessen Inhaber.
Inferenz konfigurieren
Der Chat ist OpenAI-kompatibel. NESSIE_MODEL_BASE_URL ist der Endpunkt, NESSIE_MODEL_API_KEY der Bearer-Schlüssel, und NESSIE_MODEL_PROVIDER benennt den Adapter. Nessie bringt Adapter für openai, kimi und deepseek mit. Alles andere läuft über den generischen OpenAI-kompatiblen Connector. NESSIE_MODEL_NAME legt das Standardmodell der Bereitstellung fest, das die Modellwahl eines einzelnen Agenten weiterhin übersteuert.
Embeddings werden getrennt geleitet, denn der Chat-Anbieter bietet sie unter Umständen gar nicht an. DeepSeek etwa hat keinen Embedding-Endpunkt. Jedes nicht gesetzte Feld NESSIE_EMBEDDING_* übernimmt den Chat-Anbieter, daher berechnet eine Bereitstellung, die keines davon setzt, Embeddings über den Chat-Endpunkt. Setzen Sie NESSIE_EMBEDDING_PROVIDER, NESSIE_EMBEDDING_MODEL und bei einem Proxy, der nach Dienst routet, das Dienstsegment, wenn der Chat-Anbieter keine Embeddings erzeugen kann. Ein Fehler hier schlägt eher leise als laut fehl: Der Abruf aus dem Gedächtnis und die Suche in der Wissensdatenbank fallen auf einen lexikalischen Kanal zurück und laufen weiter, wobei sie kb_search: query embedding failed protokollieren.
Die eigene Ausgabenabrechnung von Nessie, also das Token-Ledger pro Organisation und das Budget-Gate, hängt nicht davon ab, welchen Endpunkt Sie angeben. Sie funktioniert gleich, ob eine signierte Zuordnung pro Aufruf konfiguriert ist oder nicht.
Anmelden
Nessie authentifiziert Personen über UnlikeOtherAI, das auch die Struktur von Organisationen und Teams verwaltet. Die Anmeldeseite der Admin-Oberfläche zeigt eine einzige Schaltfläche: „Anmelden / Registrieren“.
Die Integration ist ein Config-JWT-Ablauf und kein Standard-OIDC. Die API liefert ein signiertes RS256-Konfigurationsdokument unter GET /api/auth/sso/config und das passende JWKS unter GET /.well-known/jwks.json, beides auf dem API-Host. Die Einrichtung ist einmalig und besteht aus diesen Schritten: Erzeugen Sie ein RSA-2048-Schlüsselpaar und setzen Sie UOA_CONFIG_JWT_PRIVATE_KEY_B64, UOA_CONFIG_JWT_KID, UOA_DOMAIN, UOA_CONFIG_URL, UOA_JWKS_URL, UOA_REDIRECT_URL und UOA_CONTACT_EMAIL. Klicken Sie einmal auf die Schaltfläche, das stellt eine Integrationsanfrage. Lassen Sie sie genehmigen. Daraufhin geht ein einmaliger Link an die Kontakt-Adresse, der das Client-Geheimnis enthält. Setzen Sie UOA_CLIENT_SECRET und starten Sie die API neu. Die kid muss pro Domain eindeutig sein.
Hinter einer Domain, mit TLS
Eine Bereitstellung bedient drei Namen: die öffentliche Website, die Admin-Oberfläche und die API. Die Ursprünge von Admin und API sind nicht austauschbar. Dockerfile.admin bettet VITE_API_BASE_URL zur Build-Zeit in das Bundle ein. Daher muss jedes gebaute Admin-Artefakt, auch ein Desktop-Build, der es enthält, gegen den API-Ursprung gebaut werden. Bauen Sie es gegen den eigenen Ursprung der Admin-Oberfläche, löst /api/auth/providers auf die HTML-Hülle der Admin-Oberfläche auf, und die Anmeldung bleibt bei „Anbieter werden geladen…“ hängen.
In der Produktion übernimmt Caddy die TLS-Terminierung. Er holt pro Hostname Zertifikate über HTTP-01 und braucht dafür kein Plugin eines DNS-Anbieters. Richten Sie den Proxy auf die Container von Admin und Website auf Port 80 und auf die API auf deren internen Port.
Setzen Sie NESSIE_API_TRUSTED_PROXY_HOPS auf die Zahl der Proxys vor der API, bei einem einzelnen Reverse Proxy also auf 1. Der Standard ist 0, dann wird X-Forwarded-For vollständig ignoriert. Dieser Wert ist eine Entscheidung mit zwei Folgen: Er bestimmt auch die Schlüssel der Ratenbegrenzung für die Anmeldung. Ist er zu niedrig, teilen sich alle Clients das Kontingent des Proxys. Ist er zu hoch, kann ein Client seine Adresse selbst fälschen. Setzen Sie ihn lieber niedrig an und messen Sie nach.
Jede Organisation und jedes Team unter einer eigenen Subdomain zu betreiben, ist möglich und standardmäßig ausgeschaltet. Gesteuert wird das über NESSIE_TEAM_HOST_BASE_DOMAIN. Ist er nicht gesetzt, werden Mandanten gar nicht über den Hostnamen geleitet. Das Gate für die Zertifikatsausstellung, NESSIE_TLS_CHECK_KEY, lehnt alles ab, solange er nicht gesetzt ist. Das ist der richtige Standard für eine Bereitstellung, die diese Funktion nicht nutzt.
Health-Checks
| Endpunkt | Beantwortete Frage |
|---|---|
GET /api/health | Lebt dieser Prozess noch? Schlägt erst fehl, wenn das geordnete Herunterfahren begonnen hat. |
GET /api/health/ready | Darf dieses Replikat neue Anfragen annehmen? Dies ist der Endpunkt, auf den Sie einen Load Balancer richten sollten. |
GET /api/ops/health | Wie geht es der Bereitstellung? Ein Bericht für Menschen mit Worker-Heartbeats, Warteschlangentiefe und toten Jobs, keine Sonde. |
Bei SIGTERM fährt die API geordnet herunter, statt an Ort und Stelle zu sterben. NESSIE_SHUTDOWN_TIMEOUT_MS begrenzt dieses Herunterfahren und steht standardmäßig auf 25 Sekunden. Bleiben Sie unter der Nachfrist, die Ihr Orchestrator dem Container gibt. Sonst beendet die Laufzeit den Prozess vorher, und das geordnete Herunterfahren bringt nichts.
Aktualisieren
Ein Upgrade besteht aus prisma migrate deploy gegen die bestehende Datenbank, danach den Seeds und danach reconcile. Dieselben vier Befehle wie beim ersten Deployment.
Der Weg ist für die Strecke getestet, die ein Selbst-Hoster tatsächlich nimmt, nicht gegen eine frische Datenbank. CI stellt einen Snapshot wieder her, der zwanzig Migrationen zurückliegt, migriert ihn auf den aktuellen Stand, prüft das Schema und führt einen Smoke-Check über die Kerntabellen aus. Jedes Release innerhalb dieses Fensters ist eine nachgewiesene Upgrade-Quelle. Migrationsordner sind nach dem Commit unveränderlich. Ein Lint-Schritt lässt den Build fehlschlagen, wenn einer umbenannt, neu nummeriert, gelöscht oder geändert wird, denn all das bricht migrate deploy für eine Datenbank, die die alte Zeile bereits gespeichert hat.
Eine fehlgeschlagene Migration blockiert jedes spätere Deployment. migrate deploy hält beim ersten Fehler an und verweigert jede spätere Migration mit P3009. Die Installation bleibt dann auf dem alten Release, bis jemand das von Hand löst. Die eigene Produktion des Projekts hat auf diese Weise einmal einen ganzen Tag lang jedes Deployment abgelehnt. Zum Lösen muss man bestätigen, dass die fehlgeschlagene Migration tatsächlich zurückgerollt wurde: Keines der Objekte, die sie anlegt, darf existieren, und was sie verändert, muss noch in seiner ursprünglichen Form vorliegen. Erst dann wird sie als zurückgerollt markiert. Eine halb angewendete Migration ist kein Kandidat dafür.
Backup und Wiederherstellung für das selbst gehostete Compose-Deployment sind im Repository nicht dokumentiert. Deshalb beschreibt diese Seite dafür kein Verfahren.
Ein Rechner oder eine verwaltete Cloud
Der oben beschriebene Single-Host-Weg mit Docker Compose ist der bewährte. Genau damit läuft die Produktion des Projekts: auf einem einzelnen Server, mit Datenbank, Objektspeicher, API, Worker, Admin-Oberfläche und Website als Container hinter einem gemeinsamen Reverse Proxy.
Die Skalierung über einen Rechner hinaus ändert drei Dinge, und alle drei sind erzwungen, nicht nur empfohlen. Außerhalb des Modus local laufen API und Worker als getrennte Prozesse, jeweils in mehreren Kopien. Deshalb ist jede Funktion verboten, die annimmt, dass ein einzelner Prozess die Festplatte des Rechners besitzt: Objektspeicher auf dem Dateisystem (ein Fehler beim Start), Vorlagen für Ausführungsumgebungen mit dem Provider docker (wirkungslos, und die Bereitstellung schlägt laut fehl) und die eingebauten Tools file_read, file_write und file_glob (ein fehlgeschlagenes Tool-Ergebnis, das der Agent lesen kann). Jede Ablehnung nennt die Einstellung, den Modus und das, was stattdessen zu verwenden ist.
Für eine verwaltete Cloud enthält das Repository einen Terraform-Baum in infrastructure/terraform/ für Google Cloud. Er umfasst die API als Cloud-Run-Dienst mit mindestens einer Instanz, denn sie hält einen dauerhaften Postgres-LISTEN-Client und führt Wartungs-Sweeps aus. Eine Skalierung auf null stoppt daher die Echtzeitzustellung. Der Worker läuft als Cloud-Run-Worker-Pool, weil er keinen Port bindet und ein Dienst nie bereit würde. Hinzu kommen ein Migrationsjob, der jedem Rollout vorgeschaltet ist, Cloud SQL für PostgreSQL 17 und ein GCS-Bucket, der über das S3-kompatible Backend angesprochen wird.
Dieser Weg ist geschrieben, aber nicht erprobt. Das Repository sagt das deutlich, und diese Seite ebenso. Es gibt kein Google-Cloud-Projekt dafür, im Baum ist nichts gegen ein echtes Projekt geplant oder angewendet, der mehrteilige Upload-Pfad zu GCS ist gegen einen echten Bucket nicht getestet, der Ressourcentyp des Worker-Pools ist nicht validiert, und die Anzahl der vertrauenswürdigen Proxy-Hops darin ist ein Platzhalter, der noch zu messen ist, keine Messung. Die Admin-Oberfläche und die öffentliche Website sind dort gar nicht modelliert. Betrachten Sie das als Ausgangspunkt, den Sie selbst prüfen, nicht als unterstützte Bereitstellung.
Wo die Daten einer Bereitstellung liegen, was die Software speichert und was sie bewusst nicht speichert, steht auf einer eigenen Seite: In der EU gebaut, und wo Ihre Daten liegen.