Встановлення Nessie
Запускайте Nessie на інфраструктурі, яку ви обираєте: від одного комп’ютера до керованої хмари.
Що потрібно перед початком роботи
Nessie — це монорепозиторій на pnpm із сервісами на Node і двома статичними бандлами. Незалежно від того, як ви його запускаєте, спершу мають бути наявні ті самі чотири речі.
- PostgreSQL з
pgvector. Це не необов’язкова вимога, і замінити її не можна — міграції виконуютьCREATE EXTENSION vectorі створюють колонкиvector(1024). Продакшн працює наpgvector/pgvector:pg17. - Node 22 і pnpm. Кожен Dockerfile у
infrastructure/docker/збирається на базіnode:22-slim, а репозиторій фіксує версію менеджера пакетів у кореневомуpackage.json(pnpm@10.22.0). - Сумісна з OpenAI кінцева точка моделі та ключ до неї. Агенти й ембедінги працюють через неї. Див. «Налаштування інференсу» нижче.
- Об’єктне сховище, сумісне з S3, для будь-якого розгортання, яке не є локальним розгортанням для розробника. Завантаження на локальний диск відхиляються поза режимом
local, і ця відмова є помилкою під час запуску, а не запуском зі зниженою функціональністю — див. «Одна машина або керована хмара».
Docker і зворотний проксі потрібні для шляху з контейнерами, описаного далі, а домен і TLS — для всього, що доступне з інтернету.
Апаратні вимоги — мінімальні CPU, пам’ять і диск для певної кількості людей — ніде в репозиторії не вказані, тож на цій сторінці їх також не наведено.
Компоненти та те, як вони взаємодіють
| Компонент | Що це | Ingress |
|---|---|---|
api/ | HTTP/WebSocket API на Fastify. Кожен клієнт звертається лише до нього. | Так |
worker/ | Оркестрація агентів і фонові запуски. Опитує чергу завдань; не відкриває жодного порту. | Ні |
admin/ | Інтерфейс продукту — статичний бандл, зібраний Vite і відданий через nginx. | Так |
web/ | Публічний сайт. Статичний, необов’язковий і не пов’язаний із запуском продукту. | Так |
| PostgreSQL | База даних, черга завдань і канал реального часу — усі три разом. | Ні |
| Об’єктне сховище | Вкладення, аватари та файли бази знань. Сумісне з S3; у продакшні — MinIO. | Ні |
gateway/ | Необов’язковий ретранслятор push-повідомлень для APNs і FCM. Вимкнений, доки ви свідомо його не увімкнете. | Так |
API і worker — це один образ. Два пакети тісно пов’язані між собою, тож Dockerfile.app збирає весь код один раз, а контейнер worker перевизначає команду на node worker/dist/index.js. У режимі local окремого контейнера worker немає взагалі: API запускає worker у власному процесі.
Redis немає. Черга завдань і транспорт реального часу працюють на Postgres — це свідоме рішення, а не недогляд. З тієї самої причини дерево Google Cloud не містить модулів Pub/Sub і Redis.
Порти
У локальній розробці API за замовчуванням працює на 5454, адмін-панель — на 5455. Робоча копія репозиторію може мати власну пару портів, якщо задати NESSIE_API_PORT і NESSIE_ADMIN_PORT у середовищі або в .env репозиторію; усі інструменти репозиторію, зокрема набори браузерних тестів, читають ті самі два значення. Обирайте пару до запуску й не змінюйте її: перенесення запущеного сервісу на інший порт призводить до того, що адмін-панель звертається не до того API.
Продакшн має інше правило, і воно не змінюється. Контейнер API слухає всередині на 5554 (закріплено через NESSIE_API_PORT), а ретранслятор push — на 5556; обидва працюють за зворотним проксі.
Запуск на одній машині для розробки
Репозиторій містить файл Compose лише з базою даних — цього достатньо для локального циклу роботи.
pnpm install
docker compose -f infrastructure/compose/docker-compose.yml up -d postgresЦей контейнер публікує Postgres на порту 55432, а користувач, пароль і база даних мають назву nessie. Помістіть рядок підключення у .env в корені репозиторію — його читає dev-скрипт API — а потім один раз застосуйте міграції та зберіть worker, бо в режимі local API завантажує worker із його зібраного dist:
DATABASE_URL=postgresql://nessie:nessie@localhost:55432/nessie
pnpm --filter @nessie/api prisma:migrate:deploy
pnpm --filter @nessie/worker build
pnpm devpnpm dev запускає API й адмін-панель паралельно з hot reload, на портах 5454 і 5455. Перевірте, що API відповів на GET /api/health.
Postgres і вимога pgvector
Три розширення створюються самими міграціями — vector, pg_trgm і pgcrypto, — тож роль, яка їх запускає, має мати право створювати розширення. На керованому Postgres це перше, що варто перевірити.
Розмірність векторів є частиною схеми, а не налаштуванням. Стовпці thoughts.embedding, thought_recalls.query_embedding і knowledge_page_chunks.embedding мають тип vector(N), а N вказано один раз як EMBEDDING_DIMENSIONS у packages/schemas/src/embedding.ts — зараз це 1024, рідна розмірність моделі ембедінгів, яку використовує продакшн. Кожен запит на ембедінг передає цю розмірність, тож провайдер, який відповів би іншою, видасть явну помилку, а не записуватиме вектори, які база відхилить. Щоб перейти на модель ембедінгів іншої розмірності, потрібно змінити цю константу, написати міграцію, яка змінює тип трьох стовпців, і перерахувати ембедінги: вектори різних розмірностей не конвертуються, тому міграція обнуляє їх, а не обрізає.
Якщо ви запускаєте Nessie поруч з іншими застосунками в спільній мережі Docker, дайте контейнеру бази даних окрему назву. Власний сервіс Compose проєкту називається nessie-postgres, а не postgres, бо ім’я сервісу стає DNS-аліасом у спільній мережі, і конфлікт змушує postgres:5432 по черзі (round-robin) вказувати на дві різні бази даних.
Конфігурація
Конфігурація під час виконання багаторівнева: файл nessie.config.json монтується в API і worker лише для читання, а поверх нього діють змінні середовища. Відповідність між ними задає ConfigEnvMap у packages/config — це і є авторитетний перелік.
NESSIE_MODE визначає тип розгортання: local (типовий, шлях для розробника), hosted або selfHosted — саме це встановлює self-hosted інсталяція. Цей режим вимикає вхід для розробки та вимагає CORS-списку дозволених origin-ів.
Це налаштування, без яких розгортання selfHosted не запуститься або не працюватиме:
| Змінна | Навіщо |
|---|---|
DATABASE_URL / NESSIE_DB_URL | Рядок підключення до Postgres. |
NESSIE_AUTH_SECRET | 32 байти в hex (openssl rand -hex 32). Підписує сесії та bootstrap-токени. Без нього запуск завершується помилкою поза режимом local. Значення має бути однаковим на кожній репліці та між розгортаннями, інакше всі сесії завершуватимуться з кожним релізом. |
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSION | JSON-набір ключів і мітка, що вибирає активний корінь; обов’язкові поза режимом local. Це окремий корінь від секрету підпису: набір ключів шифрує кожен постійний секрет, який зберігає Nessie. Не використовуйте один як інший. |
NESSIE_STORAGE_PROVIDER | Має бути s3. filesystem — значення за замовчуванням, і воно відхиляється в режимах hosted і selfHosted, тож розгортання, яке нічого тут не задає, не запуститься, а не писатиме завантаження на диск, до якого не має доступу жоден інший контейнер. |
NESSIE_CORS_ORIGINS | Origin адмін-панелі. Адмін-панель і API працюють на різних origin-ах, тому API має додати до списку дозволених той, який обслуговує. |
NESSIE_API_PUBLIC_URL, NESSIE_ADMIN_PUBLIC_URL | Використовується для створення URI перенаправлення OAuth поза HTTP-запитом. Без першого значення API видає помилку під час запиту. |
NESSIE_MODEL_PROVIDER, _BASE_URL, _API_KEY | Куди йде інференс. Див. нижче. |
У infrastructure/compose/.env.prod.example наведено точну структуру всіх них, з необов’язковими блоками — об’єктним сховищем, сховищем секретів, SSO, підключеною поштою — закоментованими на своєму місці. Повний каталог, включно з усіма необов’язковими налаштуваннями, — у docs/deployment/configuration.md.
Розгортання з Docker Compose
Скопіюйте .env.prod.example у .env поруч із docker-compose.prod.yml і заповніть його, потім запустіть базу даних, зберіть проєкт, застосуйте міграції та стартуйте сервіси. Порядок важливий: міграції та початкові дані виконуються проти вже запущеної бази даних, до того як будь-який сервіс почне відповідати.
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 -dПродакшн-Dockerfile-и запускають лінтер кожного пакета перед збіркою, тож помилка лінтингу — це помилка збірки.
Крок reconcile не є необов’язковим
reconcile заповнює типові правила політики кожної організації та дозаповнює гранти інструментів для захищених MCP і персонального асистента. Раніше API робив це під час запуску на кожній репліці; тепер він лише підключається, слухає й більше нічого не робить, тож без цього кроку у першої організації немає правил політики, і кожне прив’язування агента відхиляється. Крок ідемпотентний, тож запускати його під час кожного розгортання безкоштовно, а оновлення, застосоване вручну, теж має його виконати.
Обліковий запис першого власника
Нова інсталяція selfHosted не має користувачів, тож API створює одноразовий bootstrap-токен і записує в лог URL для налаштування. Токен зберігається в Postgres, а не в процесі, тож усі репліки погоджуються щодо нього.
docker compose -f infrastructure/compose/docker-compose.prod.yml logs nessie-api 2>&1 | grep bootstrapВідкрийте https://<your-admin-host>/bootstrap?token=<token> і створіть обліковий запис власника. Токен спливає через 15 хвилин; щоб отримати новий, перезапустіть сервіс API.
Якщо ви налаштовуєте SSO, кроку bootstrap немає взагалі. Режим bootstrap вимикається, щойно ввімкнено зовнішнього постачальника автентифікації — інакше він перехопив би екран входу, — і перша людина, яка увійде через SSO, створює типову команду та стає її власником.
Налаштування інференсу
Чат сумісний з OpenAI. NESSIE_MODEL_BASE_URL — це кінцева точка, NESSIE_MODEL_API_KEY — bearer-токен, а NESSIE_MODEL_PROVIDER називає адаптер: Nessie має вбудовані адаптери для openai, kimi і deepseek, а до всього іншого звертається через загальний конектор, сумісний з OpenAI. NESSIE_MODEL_NAME задає типову модель розгортання, яку переважає вибір моделі окремого агента.
Ембедінги маршрутизуються окремо, бо чат-провайдер може взагалі не обслуговувати їх — наприклад, у DeepSeek немає кінцевої точки для ембедінгів. Кожне незадане поле NESSIE_EMBEDDING_* успадковує чат-провайдера, тож розгортання, яке не задає жодного з них, обчислює ембедінги через кінцеву точку чату. Задайте NESSIE_EMBEDDING_PROVIDER, NESSIE_EMBEDDING_MODEL і, на проксі, що маршрутизує за сервісом, сегмент сервісу, якщо чат-провайдер не може обчислювати ембедінги. Помилка в цьому місці проявляється непомітно, а не голосно: пошук у пам’яті та в базі знань переходить на лексичний канал і працює далі, записуючи в лог kb_search: query embedding failed.
Власний облік витрат Nessie — журнал токенів для кожної організації та бюджетний ліміт — не залежить від того, на яку кінцеву точку ви спрямовуєте запити, і працює однаково незалежно від того, чи налаштовано підписану атрибуцію кожного виклику.
Вхід
Nessie автентифікує людей через UnlikeOtherAI, який також керує структурою організацій і команд. На сторінці входу в адмін-панель є одна кнопка «Login / Register».
Інтеграція — це потік config-JWT, а не стандартний OIDC: API віддає підписаний документ конфігурації RS256 за адресою GET /api/auth/sso/config і відповідний JWKS за адресою GET /.well-known/jwks.json, обидва на хості API. Налаштування — одноразова послідовність: згенеруйте пару ключів RSA-2048 і задайте UOA_CONFIG_JWT_PRIVATE_KEY_B64, UOA_CONFIG_JWT_KID, UOA_DOMAIN, UOA_CONFIG_URL, UOA_JWKS_URL, UOA_REDIRECT_URL і UOA_CONTACT_EMAIL; один раз натисніть кнопку, що створить запит на інтеграцію; дочекайтеся, поки його схвалять — це надішле одноразове посилання з клієнтським секретом на контактну адресу; задайте UOA_CLIENT_SECRET і перезапустіть API. Значення kid має бути унікальним для кожного домену.
Розміщення за доменом із TLS
Розгортання обслуговує три імені: публічний сайт, адмін-панель і API. Origin адмін-панелі та origin API не взаємозамінні. Dockerfile.admin вбудовує VITE_API_BASE_URL у бандл під час збірки, тож кожен зібраний артефакт адмін-панелі — зокрема десктопна збірка, яка його містить, — мусить бути зібраний з origin-ом API. Якщо зібрати його з власним origin-ом адмін-панелі, то /api/auth/providers відповідатиме HTML-оболонкою адмінки, і вхід зависає на «Loading providers...».
Продакшн завершує TLS на Caddy, який отримує сертифікати для кожного імені хоста через HTTP-01 і для цього не потребує плагіна DNS-провайдера. Спрямуйте проксі на контейнери адмін-панелі та сайту через порт 80, а на API — на його внутрішній порт.
Установіть NESSIE_API_TRUSTED_PROXY_HOPS рівним кількості проксі перед API — 1 за одним зворотним проксі. За замовчуванням це 0, що повністю ігнорує X-Forwarded-For. Це одне рішення з двома наслідками: воно також визначає, за чим прив’язуються обмеження частоти автентифікації. Занадто мале значення означає, що всі клієнти ділять одну квоту проксі, занадто велике — що клієнт може підробити власну адресу. Якщо сумніваєтеся, занижуйте значення і перевірте його вимірюванням.
Обслуговувати кожну організацію та команду на власному піддомені можна, але це вимкнено за замовчуванням. Це керується NESSIE_TEAM_HOST_BASE_DOMAIN: якщо її не задано, орендарі взагалі не маршрутизуються за іменем хоста, а шлюз видачі сертифікатів NESSIE_TLS_CHECK_KEY відхиляє все, поки вона не задана. Це правильне значення за замовчуванням для розгортання, яке не використовує цю функцію.
Перевірки стану (health checks)
| Кінцева точка | Питання, на яке вона відповідає |
|---|---|
GET /api/health | Чи живий цей процес? Відмовляє лише після початку коректного завершення роботи. |
GET /api/health/ready | Чи може ця репліка приймати нові запити? Саме на неї варто спрямувати балансувальник навантаження. |
GET /api/ops/health | Який стан розгортання? Звіт для людини — пульс worker-ів, глибина черги, мертві завдання, — а не проба. |
Під час SIGTERM API коректно завершує роботу, а не зупиняється на місці. NESSIE_SHUTDOWN_TIMEOUT_MS обмежує це завершення і за замовчуванням становить 25 секунд. Тримайте його меншим за пільговий період, який оркестратор дає контейнеру, інакше середовище виконання вб’є процес раніше, і коректне завершення нічого не дасть.
Оновлення
Оновлення — це prisma migrate deploy проти наявної бази даних, потім початкові дані, потім reconcile. Ті самі чотири команди, що й під час першого розгортання.
Шлях перевірено для маршруту, яким справді йде self-hosted користувач, а не на свіжій базі даних: CI відновлює знімок, зроблений на двадцять міграцій назад, мігрує його до поточного head, перевіряє схему та запускає димову перевірку основних таблиць. Будь-який реліз, що входить до цього останнього вікна, є перевіреним джерелом оновлення. Папки міграцій незмінні після коміту, а крок лінтингу провалює збірку, якщо одну з них перейменовано, перенумеровано, видалено або змінено, бо все це ламає migrate deploy для бази даних, яка вже записала старий рядок.
Одна невдала міграція блокує кожне розгортання після неї. migrate deploy зупиняється на першій невдалій міграції і відмовляється застосовувати всі наступні з помилкою P3009, тож інсталяція лишається на старому релізі, доки хтось вручну не очистить стан. Власний продакшн проєкту цілий день відхиляв кожне розгортання саме так. Очищення вимагає переконатися, що невдала міграція справді відкотилася: жоден з об’єктів, які вона створює, не має існувати, а те, що вона змінює, має лишатися у початковому вигляді — і лише після цього її можна позначити як відкочену. Міграція, що застосувалася частково, не є кандидатом.
Резервне копіювання та відновлення для self-hosted розгортання через Compose в репозиторії не описані, тож ця сторінка не описує для них жодної процедури.
Одна машина або керована хмара
Шлях із Docker Compose на одному хості, описаний вище, — єдиний перевірений: саме так працює власний продакшн проєкту на одному сервері, де база даних, об’єктне сховище, API, worker, адмін-панель і сайт працюють як контейнери за спільним зворотним проксі.
Масштабування понад одну машину змінює три речі, і всі вони забезпечуються примусово, а не рекомендуються. Поза режимом local Nessie запускає API і worker як окремі процеси, кілька копій кожного, тож будь-яка можливість, яка передбачає, що один процес володіє диском машини, відхиляється: файлове об’єктне сховище (помилка запуску), шаблони середовища виконання з провайдером docker (неактивні, а провізіонування гучно падає), а також вбудовані інструменти file_read, file_write і file_glob (невдалий результат інструмента, який агент може прочитати). Кожна відмова називає налаштування, режим і те, що використати натомість.
Для керованої хмари в репозиторії є дерево Terraform у infrastructure/terraform/, орієнтоване на Google Cloud: API як сервіс Cloud Run з мінімумом в один екземпляр — він тримає постійний клієнт LISTEN для Postgres і виконує регламентні проходи, тож масштабування до нуля зупиняє доставку в реальному часі; worker як пул worker-ів Cloud Run, бо він не слухає порт і звичайний сервіс ніколи не стане готовим; завдання міграції, яке контролює кожне розгортання; Cloud SQL для PostgreSQL 17; і бакет GCS, до якого звертаються через S3-сумісний бекенд.
Цей шлях написано, але ще не перевірено на практиці. Репозиторій прямо про це каже, і ця сторінка теж: для нього не існує проєкту Google Cloud, нічого в дереві не спланувало й не застосовувалося до реального проєкту, шлях завантаження multipart до GCS не перевірявся на реальному бакеті, тип ресурсу пулу worker-ів не валідовано, а кількість довірених стрибків проксі в ньому — заглушка, яку треба виміряти, а не вимірювання. Адмін-панель і публічний сайт там взагалі не змодельовані. Сприймайте це як відправну точку, яку ви перевіряєте, а не як підтримуване розгортання.
Де живуть дані розгортання, що саме зберігає програмне забезпечення і чого свідомо не зберігає, описано на окремій сторінці: Створено в ЄС і де зберігаються ваші дані.