nessie

Установка 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.

Требования к железу, то есть минимальные процессор, память и диск на заданное число людей, в репозитории нигде не указаны, поэтому и на этой странице их нет.

Компоненты и то, как они связаны между собой

КомпонентЧто этоДоступ извне
api/HTTP/WebSocket API на Fastify. Все клиенты обращаются только к нему.Да
worker/Оркестрация агентов и фоновые запуски. Опрашивает очередь задач и порт не открывает.Нет
admin/Интерфейс продукта — статическая сборка, которую собирает Vite и отдаёт nginx.Да
web/Публичный сайт. Статический, необязательный и не связанный с работой продукта.Да
PostgreSQLБаза данных, очередь задач и шина реального времени — всё три сразу.Нет
Объектное хранилищеВложения, аватары и файлы базы знаний. Совместимое с S3; в продакшене используется MinIO.Нет
gateway/Необязательный ретранслятор push-уведомлений для APNs и FCM. Выключен, пока вы сами его не включите.Да

API и воркер — это один образ. Два пакета тесно связаны, поэтому Dockerfile.app собирает весь проект один раз, а контейнер воркера переопределяет команду на node worker/dist/index.js. В режиме local отдельного контейнера воркера нет вовсе: API запускает воркер внутри собственного процесса.

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 в корне репозитория: её читает скрипт разработки API. Затем примените миграции и один раз соберите воркер, потому что в режиме local API загружает воркер из собранной папки dist:

DATABASE_URL=postgresql://nessie:nessie@localhost:55432/nessie

pnpm --filter @nessie/api prisma:migrate:deploy
pnpm --filter @nessie/worker build
pnpm dev

pnpm dev запускает API и админку параллельно с горячей перезагрузкой, на портах 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 по очереди распределяет запросы между двумя базами.

Конфигурация

Настройки задаются в несколько слоёв: файл nessie.config.json, смонтированный в API и воркер только для чтения, и переменные окружения поверх него. Соответствие между ними описано в ConfigEnvMap в packages/config, это и есть авторитетный список.

NESSIE_MODE задаёт вид развёртывания: local (по умолчанию, это путь разработчика), hosted или selfHosted. Последний выбирает самостоятельная установка. Он отключает вход для разработки и требует список разрешённых источников CORS.

Эти настройки нужны. Без них развёртывание selfHosted не запустится или не будет работать:

ПеременнаяЗачем
DATABASE_URL / NESSIE_DB_URLСтрока подключения к Postgres.
NESSIE_AUTH_SECRET32 байта в hex (openssl rand -hex 32). Подписывает сессии и токены первоначальной настройки. Без неё запуск завершается ошибкой вне режима local. Значение должно совпадать на всех репликах и между развёртываниями, иначе при каждом релизе обрываются все сессии.
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSIONJSON-набор ключей и метка, которая выбирает активный корневой ключ. Обязательны вне режима local. Это отдельный корень, не тот, которым подписываются сессии: набор ключей шифрует все долговременные секреты, которые хранит Nessie. Не используйте один и тот же ключ для обоих.
NESSIE_STORAGE_PROVIDERДолжно быть s3. filesystem — значение по умолчанию, но в режимах hosted и selfHosted оно не принимается. Поэтому развёртывание без этой настройки не запустится, а не станет писать загрузки на диск, до которого не дотянется ни один другой контейнер.
NESSIE_CORS_ORIGINSИсточник админки. Админка и API работают на разных источниках, поэтому 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 создаёт одноразовый токен первоначальной настройки и записывает в лог ссылку на неё. Токен хранится в 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, никакой первоначальной настройки нет вовсе. Режим первоначальной настройки отключается, как только включён внешний провайдер авторизации, иначе он перехватил бы экран входа. Первый, кто войдёт через SSO, создаст команду по умолчанию и станет её владельцем.

Настройка инференса

Чат работает через API, совместимый с 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. Адреса админки и API не взаимозаменяемы. Dockerfile.admin встраивает VITE_API_BASE_URL в сборку на этапе сборки. Поэтому каждая собранная админка, в том числе настольная сборка, которая её содержит, должна собираться с адресом API. Если собрать её с адресом самой админки, то /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 отклоняет все запросы, пока он не задан. Для развёртывания, которое этой функцией не пользуется, это правильное значение по умолчанию.

Проверки работоспособности

ЭндпоинтНа какой вопрос отвечает
GET /api/healthЖив ли этот процесс? Ответ «нет» появляется, только когда он начал завершаться.
GET /api/health/readyМожет ли эта реплика принимать новые запросы? Именно на неё стоит направлять балансировщик нагрузки.
GET /api/ops/healthКак дела у развёртывания? Это отчёт для человека: сигналы активности воркеров, длина очереди, упавшие задачи. Это не проверка.

По SIGTERM API не обрывает работу на месте, а корректно завершается. NESSIE_SHUTDOWN_TIMEOUT_MS ограничивает это завершение и по умолчанию равен 25 секундам. Держите его меньше льготного периода, который оркестратор даёт контейнеру. Иначе среда остановит процесс раньше, и корректное завершение ничего не даст.

Обновление

Обновление — это prisma migrate deploy на существующей базе, затем начальные данные и затем reconcile. Те же четыре команды, что и при первом развёртывании.

Путь проверен на том маршруте, которым идёт самостоятельная установка, а не на чистой базе. CI восстанавливает снимок, сделанный на двадцать миграций назад, применяет к нему миграции до текущей версии, проверяет схему и запускает смоук-проверку основных таблиц. Любой релиз из этого скользящего окна — подтверждённый источник обновления. Папки миграций неизменяемы после коммита, и шаг линтера падает, если папку переименовали, перенумеровали, удалили или изменили. Всё это ломает migrate deploy для базы, которая уже записала старую строку.

Одна неудачная миграция блокирует все последующие развёртывания. migrate deploy останавливается на первой ошибке и отклоняет каждую следующую миграцию с P3009. Поэтому установка остаётся на старом релизе, пока кто-нибудь не снимет блокировку вручную. Собственный продакшен проекта однажды целый день отклонял каждое развёртывание таким образом. Снять блокировку можно, только убедившись, что неудачная миграция действительно откатилась: ни один созданный ею объект не должен существовать, а всё, что она изменила, должно вернуться к исходному виду. Только после этого её можно пометить как откаченную. Миграция, которая применилась наполовину, для этого не подходит.

Резервное копирование и восстановление для самостоятельной установки на Docker Compose в репозитории не описаны, поэтому на этой странице процедуры для них тоже нет.

Один сервер или управляемое облако

Проверенный вариант — один сервер с Docker Compose, описанный выше. На нём работает продакшен проекта: база, объектное хранилище, API, воркер, админка и сайт запущены контейнерами за общим обратным прокси.

Переход на несколько серверов меняет три вещи, и все они обеспечиваются строго, а не рекомендуются. Вне режима local Nessie запускает API и воркер как отдельные процессы, и каждый из них — в нескольких копиях. Поэтому любая возможность, которая предполагает, что один процесс владеет диском машины, отклоняется. Это файловое объектное хранилище (ошибка при запуске), шаблоны окружений выполнения с провайдером docker (они не работают, а подготовка завершается явной ошибкой) и встроенные инструменты file_read, file_write и file_glob (неудачный результат инструмента, который агент может прочитать). Каждый отказ называет настройку, режим и то, что использовать вместо неё.

Для управляемого облака в репозитории есть дерево Terraform в infrastructure/terraform/ под Google Cloud. В нём API — это сервис Cloud Run с минимумом в один экземпляр: он держит постоянное подключение Postgres LISTEN и выполняет плановые проверки, поэтому масштабирование до нуля останавливает доставку в реальном времени. Воркер — это пул воркеров Cloud Run, потому что он не открывает порт и сервис никогда не стал бы готовым. Есть также задание миграции, которое должно пройти перед каждым выпуском, Cloud SQL for PostgreSQL 17 и корзина GCS, доступная через совместимый с S3 бэкенд.

Этот путь написан, но ещё не проверен на практике. Репозиторий прямо об этом говорит, и эта страница тоже. Проекта в Google Cloud для него не существует. Ничего в дереве не было спланировано или применено к реальному проекту. Путь многочастной загрузки (multipart) в GCS не тестировался на реальной корзине, тип ресурса пула воркеров не проверен, а количество доверенных прокси-хопов там — заглушка, которую нужно измерить, а не измеренное значение. Админка и публичный сайт там не описаны вовсе. Считайте это отправной точкой, которую вы проверяете сами, а не поддерживаемой конфигурацией.

Где хранятся данные развёртывания, что программа хранит и чего намеренно не хранит, описано на отдельной странице: Сделано в ЕС и где хранятся ваши данные.