Installare Nessie
Esegui Nessie sull’infrastruttura che scegli, da una sola macchina fino al cloud gestito.
Cosa ti serve prima di iniziare
Nessie è un monorepo pnpm di servizi Node più due bundle statici. Qualunque modo tu scelga per eseguirlo, prima devono esistere le stesse quattro cose.
- PostgreSQL con
pgvector. Non è facoltativo e non è sostituibile: le migrazioni eseguonoCREATE EXTENSION vectore creano colonnevector(1024). La produzione usapgvector/pgvector:pg17. - Node 22 e pnpm. Ogni Dockerfile in
infrastructure/docker/si basa sunode:22-slim, e il repository fissa il package manager nelpackage.jsonradice (pnpm@10.22.0). - Un endpoint di modello compatibile con OpenAI e una chiave per usarlo. Agenti ed embedding passano entrambi da lì. Vedi «Configurare l’inferenza» più sotto.
- Object storage compatibile con S3, per qualunque deployment che non sia quello di sviluppo locale. Gli upload su disco locale sono rifiutati fuori dalla modalità
local, e quel rifiuto è un errore di avvio e non un avvio in stato degradato: vedi «Una macchina, o un cloud gestito».
Docker e un reverse proxy servono per il percorso con container descritto più avanti, e un dominio con TLS serve per tutto ciò che è raggiungibile da internet.
Il dimensionamento hardware (CPU, memoria e disco minimi per un dato numero di persone) non è indicato da nessuna parte nel repository, quindi nemmeno questa pagina lo indica.
I componenti e come si incastrano
| Componente | Cos’è | Ingress |
|---|---|---|
api/ | L’API HTTP/WebSocket basata su Fastify. Ogni client parla con questa e con nessun’altra. | Sì |
worker/ | Orchestrazione degli agenti ed esecuzioni in background. Interroga la coda dei job e non apre alcuna porta. | No |
admin/ | L’interfaccia del prodotto: un bundle statico compilato con Vite e servito da nginx. | Sì |
web/ | Il sito pubblico. È statico, facoltativo e non è legato al funzionamento del prodotto. | Sì |
| PostgreSQL | Il database, la coda dei job e il bus in tempo reale: tutti e tre in un unico servizio. | No |
| Object storage | Allegati, avatar e file della knowledge base. Compatibile con S3; in produzione si usa MinIO. | No |
gateway/ | Un relay push opzionale per APNs e FCM. È disattivato, a meno che tu non lo abiliti deliberatamente. | Sì |
L’API e il worker sono una sola immagine. I due pacchetti sono strettamente collegati, quindi Dockerfile.app compila l’intero workspace una volta sola, e il container del worker sostituisce il comando con node worker/dist/index.js. In modalità local non esiste alcun container worker: l’API esegue il worker incorporato nel proprio processo.
Non c’è Redis. La coda dei job e il trasporto in tempo reale sono entrambi basati su Postgres, per una scelta deliberata e non per dimenticanza: anche l’infrastruttura Google Cloud elimina i propri moduli Pub/Sub e Redis per lo stesso motivo.
Porte
In sviluppo locale l’API è sulla porta 5454 e l’admin sulla 5455 per impostazione predefinita. Un checkout può prendere una coppia propria impostando NESSIE_API_PORT e NESSIE_ADMIN_PORT, nell’ambiente o nel file .env del repository; ogni strumento del repository, compresi i test del browser, legge gli stessi due valori. Scegli la coppia prima di avviare e mantienila: spostare un servizio in esecuzione su un’altra porta lascia l’admin a parlare con l’API sbagliata.
La produzione è un’altra cosa e non è cambiata da quella regola. Il container dell’API ascolta internamente sulla 5554 (fissata con NESSIE_API_PORT) e il relay push sulla 5556, entrambi dietro il reverse proxy.
Eseguirlo su una macchina, per lo sviluppo
Il repository include un file Compose con dentro solo un database, ed è tutto ciò che serve al ciclo di sviluppo locale.
pnpm install
docker compose -f infrastructure/compose/docker-compose.yml up -d postgresQuel container pubblica Postgres sulla 55432, con utente, password e database tutti chiamati nessie. Inserisci la stringa di connessione in un file .env nella radice del repository (lo legge lo script di sviluppo dell’API), poi applica le migrazioni e compila il worker una volta, perché in modalità local l’API carica il worker dal suo dist compilato:
DATABASE_URL=postgresql://nessie:nessie@localhost:55432/nessie
pnpm --filter @nessie/api prisma:migrate:deploy
pnpm --filter @nessie/worker build
pnpm devpnpm dev avvia in parallelo l’API e l’admin con hot reload, sulle porte 5454 e 5455. Verifica che l’API risponda con GET /api/health.
Postgres e il requisito pgvector
Le migrazioni stesse creano tre estensioni (vector, pg_trgm e pgcrypto), quindi il ruolo che le esegue deve poter creare estensioni. Su un Postgres gestito è la prima cosa da controllare.
La dimensione dei vettori fa parte dello schema, non è un’impostazione. thoughts.embedding, thought_recalls.query_embedding e knowledge_page_chunks.embedding sono colonne vector(N), e N è dichiarato una sola volta come EMBEDDING_DIMENSIONS in packages/schemas/src/embedding.ts: oggi vale 1024, la dimensione nativa del modello di embedding usato in produzione. Ogni richiesta di embedding invia quella dimensione, quindi un provider che risponda con un’altra fallisce subito, invece di scrivere vettori che il database rifiuta. Passare a un modello di embedding con una dimensione diversa significa modificare quella costante, scrivere una migrazione che ridefinisce il tipo delle tre colonne e ricalcolare gli embedding: vettori di dimensioni diverse non sono convertibili, quindi la migrazione li azzera invece di troncarli.
Se esegui Nessie accanto ad altre applicazioni su una rete Docker condivisa, dai al suo container del database un nome distinto. Il servizio Compose del progetto si chiama nessie-postgres e mai postgres, perché il nome del servizio diventa un alias DNS sulla rete condivisa, e una collisione fa sì che postgres:5432 alterni tra due database.
Configurazione
La configurazione runtime è a strati: un file nessie.config.json montato in sola lettura nell’API e nel worker, con le variabili d’ambiente sopra di esso. La corrispondenza tra i due è ConfigEnvMap in packages/config, che è l’elenco autorevole.
NESSIE_MODE sceglie la forma del deployment: local (il valore predefinito e il percorso dello sviluppatore), hosted oppure selfHosted, che è ciò che imposta un’installazione self-hosted. Disattiva il login di sviluppo e richiede un elenco di origini CORS consentite.
Sono le impostazioni senza le quali un deployment selfHosted non si avvia, o non funziona:
| Variabile | Perché |
|---|---|
DATABASE_URL / NESSIE_DB_URL | La stringa di connessione a Postgres. |
NESSIE_AUTH_SECRET | 32 byte in esadecimale (openssl rand -hex 32). Firma le sessioni e i token di bootstrap. Fuori dalla modalità local l’avvio va in errore senza questo valore, che deve essere lo stesso su ogni replica e tra un deploy e l’altro: altrimenti ogni sessione muore a ogni rilascio. |
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSION | Un key ring JSON e l’etichetta che seleziona la radice attiva, obbligatori fuori dalla modalità local. È una radice separata dal segreto di firma: il key ring cifra ogni segreto durevole che Nessie memorizza. Non riusare l’una come l’altra. |
NESSIE_STORAGE_PROVIDER | Deve essere s3. filesystem è il valore predefinito ed è rifiutato nelle modalità hosted e selfHosted, quindi un deployment che non imposta nulla qui non si avvia, invece di scrivere i file caricati su un disco a cui nessun altro container può accedere. |
NESSIE_CORS_ORIGINS | L’origine dell’admin. Admin e API sono origini separate, e l’API deve inserire nell’elenco delle origini consentite quella che serve. |
NESSIE_API_PUBLIC_URL, NESSIE_ADMIN_PUBLIC_URL | Serve a generare gli URI di redirect OAuth fuori da una richiesta HTTP. Senza questo valore l’API va in errore al momento della richiesta. |
NESSIE_MODEL_PROVIDER, _BASE_URL, _API_KEY | Dove va l’inferenza. Vedi sotto. |
infrastructure/compose/.env.prod.example ha la forma esatta di tutte, con i blocchi opzionali (object storage, vault dei segreti, SSO, posta collegata) commentati al loro posto. Il catalogo completo, con ogni impostazione opzionale, è in docs/deployment/configuration.md.
Deploy con Docker Compose
Copia .env.prod.example in .env accanto a docker-compose.prod.yml e compilalo, poi avvia il database, compila, applica le migrazioni e avvia i servizi. L’ordine conta: migrazioni e seed vengono eseguiti su un database già attivo, prima che un servizio risponda.
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 -dI Dockerfile di produzione eseguono il lint di ogni pacchetto prima di compilarlo, quindi un errore di lint è un errore di build.
Il passaggio di reconcile non è facoltativo
reconcile inserisce le regole di policy predefinite di ogni organizzazione e completa le autorizzazioni mancanti agli strumenti MCP protetti e a quelli dell’assistente personale. L’API lo faceva all’avvio su ogni replica; ora si connette, si mette in ascolto e basta, quindi senza questo passaggio la prima organizzazione non ha regole di policy e ogni collegamento di un agente viene negato. È idempotente, quindi eseguirlo a ogni deploy non costa nulla, e un aggiornamento applicato a mano deve eseguirlo anch’esso.
Il primo account proprietario
Un’installazione selfHosted appena fatta non ha utenti, quindi l’API genera un token di bootstrap monouso e registra un URL di configurazione. Il token vive in Postgres e non in un processo, così tutte le repliche concordano su di esso.
docker compose -f infrastructure/compose/docker-compose.prod.yml logs nessie-api 2>&1 | grep bootstrapApri https://<your-admin-host>/bootstrap?token=<token> e crea l’account proprietario. Il token scade dopo 15 minuti: riavvia il servizio API per generarne uno nuovo.
Se configuri l’SSO, non c’è affatto un passaggio di bootstrap. La modalità bootstrap viene soppressa nel momento in cui è attivo un provider di autenticazione esterno, altrimenti dirotterebbe la schermata di login, e la prima persona che accede tramite SSO crea il team predefinito e ne diventa proprietaria.
Configurare l’inferenza
La chat è compatibile con OpenAI. NESSIE_MODEL_BASE_URL è l’endpoint, NESSIE_MODEL_API_KEY il bearer token e NESSIE_MODEL_PROVIDER indica l’adattatore: Nessie include adattatori per openai, kimi e deepseek, e raggiunge qualunque altro provider tramite il connettore generico compatibile con OpenAI. NESSIE_MODEL_NAME imposta il modello predefinito del deployment, che però può essere sovrascritto dalla scelta del modello del singolo agente.
Gli embedding sono instradati separatamente, perché il provider di chat potrebbe non offrirli affatto: DeepSeek, per esempio, non ha un endpoint di embedding. Ogni campo NESSIE_EMBEDDING_* non impostato eredita il provider di chat, quindi un deployment che non ne imposta nessuno calcola gli embedding tramite l’endpoint di chat. Imposta NESSIE_EMBEDDING_PROVIDER, NESSIE_EMBEDDING_MODEL e, su un proxy che instrada per servizio, il segmento del servizio, quando il provider di chat non sa generare embedding. Sbagliare questa parte fallisce in silenzio invece che in modo evidente: il richiamo della memoria e la ricerca nella knowledge base ripiegano su un canale lessicale e vanno avanti, registrando kb_search: query embedding failed nel frattempo.
La contabilità di spesa di Nessie (il registro dei token per organizzazione e il controllo del budget) non dipende dall’endpoint a cui punti, e funziona allo stesso modo sia che sia configurata o no l’attribuzione firmata per ogni chiamata.
Accesso
Nessie autentica le persone tramite UnlikeOtherAI, che gestisce anche la struttura di organizzazioni e team. La pagina di login dell’admin mostra un solo pulsante «Accedi / Registrati».
L’integrazione è un flusso config-JWT e non OIDC standard: l’API serve un documento di configurazione firmato RS256 su GET /api/auth/sso/config e il JWKS corrispondente su GET /.well-known/jwks.json, entrambi sull’host dell’API. Configurarla è una sequenza da fare una volta sola: genera una coppia di chiavi RSA-2048 e imposta UOA_CONFIG_JWT_PRIVATE_KEY_B64, UOA_CONFIG_JWT_KID, UOA_DOMAIN, UOA_CONFIG_URL, UOA_JWKS_URL, UOA_REDIRECT_URL e UOA_CONTACT_EMAIL; clicca una volta il pulsante, che apre una richiesta di integrazione; fai approvare la richiesta, il che invia a un indirizzo di contatto un link monouso con il client secret; imposta UOA_CLIENT_SECRET e riavvia l’API. Il kid deve essere unico per dominio.
Mettere tutto dietro un dominio, con TLS
Un deployment serve tre nomi: il sito pubblico, l’admin e l’API. Le origini dell’admin e dell’API non sono intercambiabili. Dockerfile.admin incorpora VITE_API_BASE_URL nel bundle al momento della build, quindi ogni artefatto admin compilato, incluso un build desktop che lo incorpora, deve essere compilato contro l’origine dell’API. Se lo compili contro l’origine dell’admin, /api/auth/providers risolve nella pagina HTML dell’admin e il login resta bloccato su «Caricamento dei provider…».
In produzione TLS termina su Caddy, che ottiene certificati per ogni hostname tramite HTTP-01 e a questo scopo non richiede alcun plugin per il provider DNS. Punta il proxy ai container admin e web sulla porta 80, e all’API sulla sua porta interna.
Imposta NESSIE_API_TRUSTED_PROXY_HOPS al numero di proxy davanti all’API: 1 dietro un singolo reverse proxy. Il valore predefinito è 0, che ignora del tutto X-Forwarded-For. È una sola decisione con due conseguenze: determina anche la chiave dei limiti di frequenza dell’autenticazione. Se il valore è troppo basso, tutti i client condividono il contatore del proxy; se è troppo alto, un client può falsificare il proprio indirizzo. Meglio impostarlo basso e misurare.
Servire ogni organizzazione e ogni team al proprio sottodominio è possibile e disattivato di default. È governato da NESSIE_TEAM_HOST_BASE_DOMAIN: se non è impostato, i tenant non vengono instradati affatto per hostname, e il gate di emissione dei certificati NESSIE_TLS_CHECK_KEY rifiuta tutto finché non è impostato. È il valore giusto per un deployment che non usa questa funzione.
Health check
| Endpoint | Domanda a cui risponde |
|---|---|
GET /api/health | Questo processo è vivo? Fallisce solo dopo l’inizio del drenaggio. |
GET /api/health/ready | Questa replica può accettare nuove richieste? È questo il valore a cui puntare il load balancer. |
GET /api/ops/health | Come sta il deployment? È un report per una persona (heartbeat dei worker, profondità della coda, job morti), non una sonda. |
Alla ricezione di SIGTERM l’API svuota le richieste in corso invece di terminare sul posto. NESSIE_SHUTDOWN_TIMEOUT_MS limita quel drenaggio e il valore predefinito è 25 secondi: tienilo al di sotto del periodo di grazia che il tuo orchestratore concede al container, altrimenti il runtime termina il processo prima che il drenaggio serva a qualcosa.
Aggiornamento
Un aggiornamento è prisma migrate deploy sul database esistente, poi i seed, poi reconcile: gli stessi quattro comandi del primo deploy.
Il percorso è testato sulla strada che chi lo installa da sé percorre davvero, non su un database nuovo: la CI ripristina uno snapshot preso venti migrazioni indietro, lo migra alla head corrente, ne convalida lo schema ed esegue un controllo di funzionamento sulle tabelle principali. Ogni release compresa in questa finestra precedente è un punto di partenza per l’aggiornamento verificato. Le cartelle di migrazione sono immutabili una volta committate, e uno step di lint fa fallire la build se una di esse viene rinominata, rinumerata, eliminata o modificata, perché tutti questi casi rompono migrate deploy per un database che ha già registrato la vecchia riga.
Una migrazione fallita blocca ogni deploy successivo. migrate deploy si ferma alla prima migrazione fallita e rifiuta tutte quelle successive con P3009, quindi l’installazione resta sulla release precedente finché qualcuno non lo sblocca a mano. La produzione del progetto stesso ha passato una giornata a rifiutare ogni deploy in questo modo. Sbloccarlo significa confermare che la migrazione fallita è davvero stata annullata (nessuno degli oggetti che crea dovrebbe esistere, e ciò che modifica dovrebbe avere ancora la forma originale) e solo allora segnarla come annullata. Una migrazione applicata a metà non è un candidato.
Backup e ripristino per il deployment self-hosted con Compose non sono documentati nel repository, quindi questa pagina non descrive una procedura per farlo.
Una macchina, o un cloud gestito
Il percorso con Docker Compose su singolo host descritto sopra è quello collaudato: è ciò che gira in produzione per il progetto, su un singolo server, con database, object storage, API, worker, admin e sito come container dietro un reverse proxy condiviso.
Scalare oltre una macchina cambia tre cose, tutte imposte e non solo consigliate. Fuori dalla modalità local Nessie esegue API e worker come processi separati, in più copie di ciascuno, quindi ogni funzione che presuppone che un processo possieda il disco della macchina viene rifiutata: l’object storage su filesystem (un errore di avvio), i template degli ambienti di esecuzione con il provider docker (inerti, e il provisioning fallisce in modo evidente) e gli strumenti integrati file_read, file_write e file_glob (un risultato di strumento fallito che l’agente può leggere). Ogni rifiuto indica l’impostazione, la modalità e cosa usare al suo posto.
Per un cloud gestito il repository contiene un albero Terraform in infrastructure/terraform/ che punta a Google Cloud: l’API come servizio Cloud Run con un minimo di un’istanza (mantiene un client LISTEN persistente su Postgres ed esegue le operazioni periodiche di manutenzione, quindi scalarla a zero interrompe la consegna in tempo reale); il worker come worker pool di Cloud Run, perché non apre alcuna porta e un Service non diventerebbe mai pronto; un job di migrazione che precede ogni rilascio; Cloud SQL per PostgreSQL 17 e un bucket GCS raggiunto tramite il backend compatibile con S3.
Quel percorso è scritto, non ancora percorso. Il repository lo dice chiaramente, e così questa pagina: non esiste ancora un progetto Google Cloud per esso, nulla nell’albero è stato pianificato o applicato su un progetto reale, il percorso di upload multipart verso GCS non è testato su un bucket reale, il tipo di risorsa worker pool non è stato validato, e il numero di hop del proxy fidato è un segnaposto da misurare, non una misura. Admin e sito pubblico non sono modellati affatto. Va considerato un punto di partenza da verificare, non un deployment supportato.
Dove vivono i dati di un deployment, cosa conserva il software e cosa non conserva deliberatamente, è trattato in una pagina a parte: Realizzato in UE, e dove stanno i tuoi dati.