Installer Nessie
Faites fonctionner Nessie sur l’infrastructure de votre choix, d’une seule machine à un cloud géré.
Ce dont vous avez besoin avant de commencer
Nessie est un monorepo pnpm de services Node, auxquels s’ajoutent deux bundles statiques. Quelle que soit la façon de le lancer, les mêmes quatre éléments doivent d’abord exister.
- PostgreSQL avec
pgvector. Indispensable, et sans équivalent : les migrations exécutentCREATE EXTENSION vectoret créent des colonnesvector(1024). La production utilisepgvector/pgvector:pg17. - Node 22 et pnpm. Chaque Dockerfile de
infrastructure/docker/part denode:22-slim, et le dépôt fixe la version de son gestionnaire de paquets dans lepackage.jsonracine (pnpm@10.22.0). - Un point de terminaison de modèle compatible OpenAI, et une clé pour y accéder. Les agents comme les embeddings passent par lui. Voir « Configurer l’inférence » ci-dessous.
- Stockage objet compatible S3, pour tout déploiement qui n’est pas l’installation locale de développement. Les envois sur disque local sont refusés hors du mode
local, et ce refus fait échouer le démarrage au lieu de laisser un service dégradé : voir « Une seule machine ou un cloud géré ».
Docker et un proxy inverse sont nécessaires pour la voie conteneurs décrite plus bas, ainsi qu’un nom de domaine et TLS pour tout ce qui est accessible depuis Internet.
Le dimensionnement matériel (le minimum de processeur, de mémoire et de disque pour un nombre donné de personnes) n’est indiqué nulle part dans le dépôt : cette page ne le précise donc pas non plus.
Les composants, et leur articulation
| Composant | Rôle | Accès entrant |
|---|---|---|
api/ | L’API HTTP/WebSocket Fastify. Chaque client communique avec elle, et avec rien d’autre. | Oui |
worker/ | Orchestration des agents et exécutions en arrière-plan. Interroge la file de tâches ; n’écoute sur aucun port. | Non |
admin/ | L’interface du produit : un bundle statique généré par Vite et servi par nginx. | Oui |
web/ | Le site public. Statique, facultatif, et sans lien avec le fonctionnement du produit. | Oui |
| PostgreSQL | La base de données, la file de tâches et le bus temps réel : les trois à la fois. | Non |
| Stockage objet | Pièces jointes, avatars et fichiers de la base de connaissances. Compatible S3 ; MinIO en production. | Non |
gateway/ | Un relais push optionnel pour APNs et FCM. Désactivé, sauf si vous l’activez explicitement. | Oui |
L’API et le worker forment une seule image. Les deux paquets sont étroitement liés : Dockerfile.app compile l’ensemble de l’espace de travail une seule fois, et le conteneur worker remplace la commande par node worker/dist/index.js. En mode local, il n’existe aucun conteneur worker : l’API exécute le worker intégré dans son propre processus.
Il n’y a pas de Redis. La file de tâches et le transport temps réel reposent tous deux sur Postgres, par choix et non par omission. L’arborescence Google Cloud supprime ses modules Pub/Sub et Redis pour la même raison.
Ports
En développement local, l’API écoute par défaut sur 5454 et l’admin sur 5455. Une copie de travail peut prendre sa propre paire en définissant NESSIE_API_PORT et NESSIE_ADMIN_PORT, dans l’environnement ou dans le fichier .env du dépôt ; tous les outils du dépôt, y compris les suites de tests du navigateur, lisent ces deux mêmes valeurs. Choisissez la paire avant de démarrer et gardez-la : déplacer un service en cours d’exécution vers un autre port laisse l’admin parler à la mauvaise API.
La production est un autre cas, que cette règle ne modifie pas. Le conteneur API écoute en interne sur 5554 (fixé par NESSIE_API_PORT) et le relais push sur 5556, tous deux derrière le proxy inverse.
Exécuter sur une seule machine, pour le développement
Le dépôt fournit un fichier Compose qui ne contient qu’une base de données, ce qui suffit à la boucle de développement locale.
pnpm install
docker compose -f infrastructure/compose/docker-compose.yml up -d postgresCe conteneur publie Postgres sur 55432, avec un utilisateur, un mot de passe et une base nommés tous nessie. Placez la chaîne de connexion dans un .env à la racine du dépôt (le script de développement de l’API le lit), puis appliquez les migrations et compilez le worker une fois : en mode local, l’API charge le worker depuis son dist compilé :
DATABASE_URL=postgresql://nessie:nessie@localhost:55432/nessie
pnpm --filter @nessie/api prisma:migrate:deploy
pnpm --filter @nessie/worker build
pnpm devpnpm dev lance l’API et l’admin en parallèle, avec rechargement à chaud, sur les ports 5454 et 5455. Vérifiez que l’API répond sur GET /api/health.
Postgres, et la nécessité de pgvector
Trois extensions sont créées par les migrations elles-mêmes — vector, pg_trgm et pgcrypto — le rôle qui exécute les migrations doit donc être autorisé à créer des extensions. Sur un Postgres géré, c’est le premier point à vérifier.
La largeur des vecteurs fait partie du schéma, et non d’un réglage. thoughts.embedding, thought_recalls.query_embedding et knowledge_page_chunks.embedding sont des colonnes vector(N), et N est défini une seule fois sous le nom EMBEDDING_DIMENSIONS dans packages/schemas/src/embedding.ts : actuellement 1024, la largeur native du modèle d’embeddings utilisé en production. Chaque requête d’embedding envoie cette largeur : un fournisseur qui répondrait avec une autre échoue bruyamment, au lieu d’écrire des vecteurs que la base rejetterait. Passer à un modèle d’embeddings d’une autre largeur implique de modifier cette constante, d’écrire une migration qui redéfinit les trois colonnes, puis de recalculer tous les embeddings. Des vecteurs de largeurs différentes ne se convertissent pas : la migration les remet donc à NULL au lieu de les tronquer.
Si vous faites tourner Nessie à côté d’autres applications sur un réseau Docker partagé, donnez à son conteneur de base de données un nom distinct. Le service Compose du projet s’appelle nessie-postgres, jamais postgres : le nom de service devient un alias DNS sur le réseau partagé, et une collision fait alterner postgres:5432 entre deux bases de données (round-robin).
Configuration
La configuration d’exécution est en couches : un fichier nessie.config.json monté en lecture seule dans l’API et le worker, sur lequel les variables d’environnement se superposent. La correspondance entre les deux est définie par ConfigEnvMap dans packages/config, qui fait référence.
NESSIE_MODE choisit le type de déploiement : local (par défaut, et chemin du développeur), hosted ou selfHosted, que définit une installation auto-hébergée. Ce mode désactive la connexion de développement et impose une liste d’origines CORS autorisées.
Voici les réglages sans lesquels un déploiement selfHosted ne démarre pas, ou ne fonctionne pas :
| Variable | Pourquoi |
|---|---|
DATABASE_URL / NESSIE_DB_URL | La chaîne de connexion à Postgres. |
NESSIE_AUTH_SECRET | 32 octets en hexadécimal (openssl rand -hex 32). Signe les sessions et les jetons d’amorçage. Le démarrage échoue sans lui hors du mode local. Il doit avoir la même valeur sur chaque réplica et d’un déploiement à l’autre, faute de quoi toutes les sessions tombent à chaque mise en production. |
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSION | Un trousseau de clés au format JSON, et le libellé qui désigne la racine active, obligatoires hors du mode local. Il s’agit d’une racine distincte du secret de signature : le trousseau chiffre chaque secret durable que Nessie conserve. Ne réutilisez pas l’un comme l’autre. |
NESSIE_STORAGE_PROVIDER | Doit valoir s3. filesystem est la valeur par défaut et est refusée en modes hosted et selfHosted : un déploiement qui ne définit rien ici échoue au démarrage, au lieu d’écrire les envois sur un disque auquel aucun autre conteneur n’a accès. |
NESSIE_CORS_ORIGINS | L’origine de l’admin. L’admin et l’API sont deux origines distinctes, et l’API doit autoriser celle qu’elle sert. |
NESSIE_API_PUBLIC_URL, NESSIE_ADMIN_PUBLIC_URL | Sert à générer les URI de redirection OAuth en dehors d’une requête HTTP. Sans la première d’entre elles, l’API lève une erreur au moment de la requête. |
NESSIE_MODEL_PROVIDER, _BASE_URL, _API_KEY | Destination de l’inférence. Voir ci-dessous. |
infrastructure/compose/.env.prod.example montre la forme exacte de chacun, les blocs facultatifs (stockage objet, coffre à secrets, SSO, messagerie connectée) étant commentés à leur place. Le catalogue complet, y compris tous les réglages facultatifs, se trouve dans docs/deployment/configuration.md.
Déployer avec Docker Compose
Copiez .env.prod.example en .env à côté de docker-compose.prod.yml et complétez-le, puis démarrez la base de données, compilez, appliquez les migrations et lancez les services. L’ordre compte : les migrations et les données initiales s’exécutent sur une base déjà démarrée, avant que le moindre service ne réponde.
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 -dLes Dockerfiles de production exécutent le lint de chaque paquet avant de le compiler : un échec de lint est donc un échec de compilation.
L’étape de réconciliation n’est pas facultative
reconcile initialise les règles de politique par défaut de chaque organisation et complète les autorisations d’outils MCP protégés et de l’assistant personnel. L’API le faisait autrefois au démarrage, sur chaque réplica ; elle se contente désormais de se connecter, d’écouter, et ne fait rien d’autre. Sans cette étape, la première organisation n’a aucune règle de politique, et chaque liaison d’agent est refusée. Elle est idempotente : la lancer à chaque déploiement ne coûte rien, et une mise à niveau appliquée à la main doit aussi la lancer.
Le premier compte propriétaire
Une installation selfHosted neuve n’a aucun utilisateur : l’API génère donc un jeton d’amorçage à usage unique et journalise une URL de configuration. Le jeton est conservé dans Postgres et non dans un processus, afin que tous les réplicas s’accordent sur sa valeur.
docker compose -f infrastructure/compose/docker-compose.prod.yml logs nessie-api 2>&1 | grep bootstrapOuvrez https://<your-admin-host>/bootstrap?token=<token> et créez le compte propriétaire. Le jeton expire au bout de 15 minutes ; redémarrez le service API pour en générer un nouveau.
Si vous configurez le SSO, il n’y a aucune étape d’amorçage. Le mode d’amorçage est désactivé dès qu’un fournisseur d’authentification externe est activé, sinon il détournerait l’écran de connexion. La première personne qui se connecte par SSO crée l’équipe par défaut et en devient propriétaire.
Configurer l’inférence
Le chat est compatible OpenAI. NESSIE_MODEL_BASE_URL indique le point de terminaison, NESSIE_MODEL_API_KEY le jeton d’accès (bearer), et NESSIE_MODEL_PROVIDER nomme l’adaptateur : Nessie fournit des adaptateurs pour openai, kimi et deepseek, et passe par le connecteur générique compatible OpenAI pour tout autre fournisseur. NESSIE_MODEL_NAME définit le modèle par défaut du déploiement, mais le choix de modèle propre à chaque agent reste prioritaire.
Les embeddings sont routés séparément, car le fournisseur de chat peut ne pas en proposer du tout : DeepSeek, par exemple, n’a pas de point de terminaison d’embeddings. Chaque champ NESSIE_EMBEDDING_* non défini hérite du fournisseur de chat : un déploiement qui n’en définit aucun calcule donc ses embeddings via le point de terminaison de chat. Définissez NESSIE_EMBEDDING_PROVIDER, NESSIE_EMBEDDING_MODEL et, sur un proxy qui route par service, le segment de service, lorsque le fournisseur de chat ne sait pas calculer d’embeddings. Une erreur de configuration échoue discrètement plutôt que bruyamment : le rappel de mémoire et la recherche dans la base de connaissances se rabattent sur un canal lexical et continuent de fonctionner, en journalisant kb_search: query embedding failed au passage.
La comptabilité de consommation propre à Nessie, à savoir le registre des jetons par organisation et le contrôle budgétaire, ne dépend pas du point de terminaison que vous choisissez. Elle fonctionne de la même manière, que l’attribution d’appel signée soit configurée ou non.
Se connecter
Nessie authentifie les personnes via UnlikeOtherAI, qui gère aussi la structure des organisations et des équipes. La page de connexion de l’admin n’affiche qu’un seul bouton « Connexion / Inscription ».
L’intégration repose sur un flux de JWT de configuration plutôt que sur OIDC standard : l’API sert un document de configuration signé en RS256 à GET /api/auth/sso/config, et le JWKS correspondant à GET /.well-known/jwks.json, les deux sur l’hôte de l’API. La mise en place est une séquence unique : générez une paire de clés RSA-2048 et définissez UOA_CONFIG_JWT_PRIVATE_KEY_B64, UOA_CONFIG_JWT_KID, UOA_DOMAIN, UOA_CONFIG_URL, UOA_JWKS_URL, UOA_REDIRECT_URL et UOA_CONTACT_EMAIL ; cliquez une fois sur le bouton, ce qui crée une demande d’intégration ; faites-la approuver, ce qui envoie à l’adresse de contact un lien à usage unique contenant le secret client ; définissez UOA_CLIENT_SECRET et redémarrez l’API. Le kid doit être unique par domaine.
Derrière un domaine, avec TLS
Un déploiement sert trois noms : le site public, l’admin et l’API. Les origines de l’admin et de l’API ne sont pas interchangeables. Dockerfile.admin intègre VITE_API_BASE_URL dans le bundle au moment de la compilation : chaque artefact admin compilé, y compris une version bureau qui l’embarque, doit donc être compilé pour l’origine de l’API. Compilé pour l’origine propre à l’admin, /api/auth/providers renvoie la coquille HTML de l’admin, et la connexion reste bloquée sur « Chargement des fournisseurs... ».
En production, TLS est terminé par Caddy, qui obtient des certificats par nom d’hôte via HTTP-01 et n’a pas besoin de plugin de fournisseur DNS pour cela. Dirigez le proxy vers les conteneurs admin et web sur le port 80, et vers l’API sur son port interne.
Réglez NESSIE_API_TRUSTED_PROXY_HOPS sur le nombre de proxys placés devant l’API : 1 derrière un seul proxy inverse. Sa valeur par défaut est 0, qui ignore complètement X-Forwarded-For. Ce réglage relève d’une seule décision aux deux conséquences : il détermine aussi les clés des limites de débit d’authentification. Trop bas, chaque client partage le compartiment du proxy ; trop haut, un client peut falsifier sa propre adresse. Préférez une valeur basse, puis mesurez.
Servir chaque organisation et chaque équipe sur son propre sous-domaine est possible, mais désactivé par défaut. Cela est régi par NESSIE_TEAM_HOST_BASE_DOMAIN : non défini, les locataires ne sont pas routés par nom d’hôte du tout. Le garde-fou de délivrance des certificats, NESSIE_TLS_CHECK_KEY, refuse tout tant qu’il n’est pas défini, ce qui est le bon choix par défaut pour un déploiement qui n’utilise pas cette fonctionnalité.
Contrôles de santé
| Point de terminaison | Question à laquelle il répond |
|---|---|
GET /api/health | Ce processus est-il vivant ? N’échoue qu’une fois l’arrêt progressif commencé. |
GET /api/health/ready | Ce réplica peut-il accepter de nouvelles requêtes ? C’est celui vers lequel pointer un répartiteur de charge. |
GET /api/ops/health | Comment le déploiement se porte-t-il ? Un rapport destiné à une personne : heartbeats des workers, profondeur de la file, tâches en échec. Ce n’est pas une sonde. |
À la réception de SIGTERM, l’API s’arrête proprement au lieu de disparaître sur-le-champ. NESSIE_SHUTDOWN_TIMEOUT_MS borne cet arrêt progressif et vaut 25 secondes par défaut. Gardez-le sous le délai de grâce accordé au conteneur par votre orchestrateur, sinon l’environnement d’exécution tue le processus avant la fin, et l’arrêt progressif n’apporte rien.
Mise à niveau
Une mise à niveau consiste à lancer prisma migrate deploy sur la base existante, puis les données initiales, puis reconcile. Ce sont les mêmes quatre commandes que pour le premier déploiement.
Ce chemin est testé pour la voie qu’emprunte réellement un auto-hébergeur, et non sur une base neuve : la CI restaure un instantané pris vingt migrations en retard, le migre jusqu’à la version actuelle, valide le schéma et lance un test de fumée sur les tables principales. Toute version comprise dans cette fenêtre glissante est une source de mise à niveau éprouvée. Les dossiers de migration sont immuables une fois validés, et une étape de lint fait échouer la compilation si l’un est renommé, renuméroté, supprimé ou modifié, car ces opérations cassent toutes migrate deploy pour une base qui a déjà enregistré l’ancienne ligne.
Une migration qui échoue bloque tous les déploiements suivants. migrate deploy s’arrête à la première erreur et refuse toutes les migrations suivantes avec P3009 : l’installation reste sur l’ancienne version jusqu’à ce que quelqu’un la débloque à la main. La production du projet lui-même a passé une journée à rejeter chaque déploiement de cette façon. Pour la débloquer, vérifiez d’abord que la migration en échec a bien été annulée : aucun des objets qu’elle crée ne doit exister, et ce qu’elle modifie doit avoir gardé sa forme d’origine. Seulement ensuite marquez-la comme annulée. Une migration appliquée à moitié n’est pas un candidat.
La sauvegarde et la restauration du déploiement Docker Compose auto-hébergé ne sont pas documentées dans le dépôt : cette page ne décrit donc aucune procédure pour cela.
Une seule machine ou un cloud géré
Le chemin Docker Compose sur une seule machine, décrit plus haut, est celui qui est éprouvé : c’est celui que fait tourner la production du projet, sur un seul serveur, avec la base de données, le stockage objet, l’API, le worker, l’admin et le site en conteneurs derrière un proxy inverse partagé.
Passer à plusieurs machines change trois choses, toutes imposées et non simplement conseillées. Hors du mode local, Nessie exécute l’API et le worker comme processus distincts, en plusieurs copies de chacun : toute fonctionnalité qui suppose qu’un seul processus possède le disque de la machine est donc refusée. C’est le cas du stockage objet sur système de fichiers (échec au démarrage), des modèles d’environnement d’exécution avec le fournisseur docker (sans effet, et le provisionnement échoue bruyamment), et des outils intégrés file_read, file_write et file_glob (un résultat d’outil en échec que l’agent peut lire). Chaque refus nomme le réglage, le mode et ce qu’il faut utiliser à la place.
Pour un cloud géré, le dépôt contient une arborescence Terraform dans infrastructure/terraform/ ciblant Google Cloud : l’API en tant que service Cloud Run, avec un minimum d’une instance, car elle maintient un client LISTEN persistant vers Postgres et exécute des balayages de maintenance ; la mettre à zéro interromprait la diffusion temps réel. Le worker est un pool de workers Cloud Run, car il n’ouvre aucun port et un Service ne deviendrait jamais prêt. S’y ajoutent une tâche de migration qui conditionne chaque déploiement, Cloud SQL pour PostgreSQL 17, et un bucket GCS atteint par le backend compatible S3.
Cette voie est écrite, mais pas parcourue. Le dépôt le dit sans détour, et cette page aussi : aucun projet Google Cloud n’existe pour elle, rien dans l’arborescence n’a été planifié ni appliqué sur un vrai projet, le chemin d’envoi multipart vers GCS n’a jamais été testé sur un vrai bucket, le type de ressource pool de workers n’est pas validé, et le nombre de sauts de proxy de confiance qu’elle contient est une valeur indicative à mesurer, non une mesure. L’admin et le site public n’y sont pas du tout modélisés. Traitez-la comme un point de départ à vérifier, et non comme un déploiement pris en charge.
L’emplacement des données d’un déploiement, ce que le logiciel conserve et ce qu’il ne conserve délibérément pas, fait l’objet d’une page distincte : Conçu dans l’UE, et où se trouvent vos données.