nessie

Nessie 설치하기

단일 서버부터 관리형 클라우드까지, 원하는 인프라에서 Nessie를 실행하세요.

시작하기 전에 준비할 것

Nessie는 Node 서비스와 두 개의 정적 번들로 이루어진 pnpm 모노레포입니다. 실행 방식과 관계없이 다음 네 가지를 먼저 갖춰야 합니다.

  • PostgreSQL과 pgvector. 선택 사항이 아니며 다른 제품으로 대체할 수도 없습니다. 마이그레이션이 CREATE EXTENSION vector를 실행하고 vector(1024) 열을 만들기 때문입니다. 운영 환경은 pgvector/pgvector:pg17을 사용합니다.
  • Node 22와 pnpm. infrastructure/docker/의 모든 Dockerfile은 node:22-slim을 기반으로 빌드하며, 저장소는 루트 package.json에서 패키지 관리자를 고정합니다(pnpm@10.22.0).
  • OpenAI 호환 모델 엔드포인트와 그 키. 에이전트와 임베딩이 모두 이 엔드포인트를 거칩니다. 아래의 “추론 구성”을 참조하세요.
  • S3 호환 객체 스토리지. 로컬 개발 환경을 제외한 모든 배포에 필요합니다. 로컬 디스크의 업로드는 local 모드 밖에서 거부되며, 이 거부는 제한된 상태로 부팅되는 것이 아니라 시작 실패입니다. 자세한 내용은 “한 대의 서버 또는 관리형 클라우드”를 참조하세요.

컨테이너 경로(아래에서 설명)에는 Docker와 리버스 프록시가 필요하며, 인터넷에서 접근할 수 있는 환경에는 도메인과 TLS가 필요합니다.

사용자 수에 따른 최소 CPU, 메모리, 디스크 같은 하드웨어 사양은 저장소 어디에도 나와 있지 않으므로, 이 페이지도 사양을 제시하지 않습니다.

구성 요소와 그 연결 방식

구성 요소역할외부 수신
api/Fastify HTTP/WebSocket API입니다. 모든 클라이언트는 이 API와만 통신합니다.예
worker/에이전트 오케스트레이션과 백그라운드 실행을 담당합니다. 작업 큐를 폴링하며 포트를 열지 않습니다.아니요
admin/제품 인터페이스입니다. Vite로 빌드한 정적 번들이며 nginx가 제공합니다.예
web/공개 웹사이트입니다. 정적이며 선택 사항이고, 제품 실행과는 무관합니다.예
PostgreSQL데이터베이스, 작업 큐, 실시간 버스를 모두 담당합니다.아니요
객체 스토리지첨부 파일, 아바타, 지식 베이스 파일을 저장합니다. S3 호환이며 운영 환경에서는 MinIO를 사용합니다.아니요
gateway/APNs와 FCM을 위한 선택적 푸시 릴레이입니다. 직접 켜기 전까지는 꺼져 있습니다.예

API와 워커는 하나의 이미지입니다. 두 패키지가 긴밀하게 연결되어 있어서, Dockerfile.app이 전체 워크스페이스를 한 번 빌드하고 워커 컨테이너는 명령을 node worker/dist/index.js로 재정의합니다. local 모드에는 워커 컨테이너가 전혀 없으며, API가 자신의 프로세스 안에서 워커를 실행합니다.

Redis는 없습니다. 작업 큐와 실시간 전송은 모두 Postgres 기반입니다. 누락이 아니라 의도된 결정입니다. Google Cloud 구성 트리에서도 같은 이유로 Pub/Sub와 Redis 모듈을 삭제했습니다.

포트

로컬 개발 환경에서 API는 기본적으로 5454, 관리자 화면은 5455를 사용합니다. 체크아웃마다 환경 변수나 저장소의 .env에 NESSIE_API_PORT와 NESSIE_ADMIN_PORT를 설정해 자기만의 포트 쌍을 쓸 수 있으며, 브라우저 테스트 모음을 포함한 저장소의 모든 도구가 같은 두 값을 읽습니다. 시작하기 전에 포트 쌍을 정하고 그대로 유지하세요. 실행 중인 서비스를 다른 포트로 옮기면 관리자 화면이 엉뚱한 API와 통신하게 됩니다.

운영 환경은 이 규칙과 별개이며 그대로 유지됩니다. API 컨테이너는 내부적으로 5554에서 수신하고(NESSIE_API_PORT로 고정), 푸시 릴레이는 5556에서 수신합니다. 두 포트 모두 리버스 프록시 뒤에 있습니다.

한 대의 컴퓨터에서 개발용으로 실행하기

저장소에는 데이터베이스만 포함된 Compose 파일이 들어 있으며, 로컬 개발 루프에는 이것으로 충분합니다.

pnpm install
docker compose -f infrastructure/compose/docker-compose.yml up -d postgres

이 컨테이너는 사용자 이름, 비밀번호, 데이터베이스 이름이 모두 nessie인 Postgres를 55432 포트로 노출합니다. 연결 문자열을 저장소 루트의 .env에 넣으세요. API의 개발 스크립트가 이 파일을 읽습니다. 그다음 마이그레이션을 적용하고 워커를 한 번 빌드하세요. 로컬 모드에서 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 포트에서 핫 리로드와 함께 병렬로 실행합니다. GET /api/health로 API가 응답하는지 확인하세요.

Postgres와 pgvector 요구 사항

마이그레이션이 세 가지 확장을 직접 만듭니다. vector, pg_trgm, pgcrypto입니다. 따라서 마이그레이션을 실행하는 역할에 확장을 만들 권한이 있어야 합니다. 관리형 Postgres를 쓴다면 무엇보다 먼저 이 권한을 확인하세요.

벡터 너비는 설정값이 아니라 스키마의 일부입니다. thoughts.embedding, thought_recalls.query_embedding, knowledge_page_chunks.embedding은 vector(N) 열이며, N은 packages/schemas/src/embedding.ts의 EMBEDDING_DIMENSIONS에 한 번만 정의됩니다. 현재 값은 1024로, 운영 환경이 사용하는 임베딩 모델의 기본 너비입니다. 모든 임베딩 요청은 이 너비를 보내므로, 다른 너비로 응답하는 제공자는 데이터베이스가 거부할 벡터를 기록하는 대신 명확하게 실패합니다. 다른 너비의 임베딩 모델로 옮기려면 이 상수를 고치고, 세 열의 타입을 다시 지정하는 마이그레이션을 작성하고, 다시 임베딩해야 합니다. 서로 다른 너비의 벡터는 변환할 수 없으므로, 마이그레이션은 벡터를 잘라내지 않고 NULL로 비웁니다.

다른 애플리케이션과 같은 Docker 네트워크를 공유한다면 Nessie의 데이터베이스 컨테이너에 고유한 이름을 주세요. 프로젝트의 Compose 서비스 이름은 nessie-postgres이며 postgres는 쓰지 않습니다. 서비스 이름은 공유 네트워크에서 DNS 별칭이 되므로, 이름이 겹치면 postgres:5432가 두 데이터베이스 사이에서 라운드 로빈으로 분산되기 때문입니다.

구성

런타임 구성은 여러 계층으로 이루어집니다. API와 워커에는 읽기 전용으로 마운트된 nessie.config.json 파일이 있고, 그 위에 환경 변수가 덮어씁니다. 두 가지 사이의 매핑은 packages/config의 ConfigEnvMap에 정의되어 있으며, 이것이 기준이 되는 목록입니다.

NESSIE_MODE는 배포 형태를 선택합니다. local(기본값이자 개발자 경로), hosted, selfHosted가 있으며, 자체 호스팅 설치는 이 값으로 selfHosted를 설정합니다. 이 모드에서는 개발용 로그인이 꺼지고 CORS 허용 목록이 필요합니다.

다음은 selfHosted 배포에 없으면 시작되지 않거나 제대로 작동하지 않는 설정입니다:

변수이유
DATABASE_URL / NESSIE_DB_URLPostgres 연결 문자열입니다.
NESSIE_AUTH_SECRET16진수 32바이트(openssl rand -hex 32). 세션과 부트스트랩 토큰에 서명합니다. local 모드가 아니면 이 값이 없을 때 시작이 실패합니다. 모든 레플리카와 배포 전반에서 같은 값이어야 하며, 그렇지 않으면 릴리스마다 모든 세션이 끊어집니다.
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSIONJSON 키 링과 활성 루트를 선택하는 레이블입니다. local 모드가 아니면 필수입니다. 이것은 서명 비밀과 별개의 루트이며, 키 링은 Nessie가 저장하는 모든 영구 비밀을 암호화합니다. 둘 중 하나를 다른 쪽으로 재사용하지 마세요.
NESSIE_STORAGE_PROVIDERs3이어야 합니다. filesystem이 기본값이지만 hosted와 selfHosted 모드에서는 거부됩니다. 따라서 이 항목을 설정하지 않은 배포는, 다른 컨테이너가 읽을 수 없는 디스크에 업로드를 쓰는 대신 시작이 실패합니다.
NESSIE_CORS_ORIGINS관리자 화면의 출처입니다. 관리자와 API는 서로 다른 출처이므로, API는 자신이 제공하는 출처를 허용 목록에 등록해야 합니다.
NESSIE_API_PUBLIC_URL, NESSIE_ADMIN_PUBLIC_URLHTTP 요청 바깥에서 OAuth 리디렉션 URI를 만들 때 사용합니다. 이 값이 없으면 API는 요청 시점에 오류를 발생시킵니다.
NESSIE_MODEL_PROVIDER, _BASE_URL, _API_KEY추론 요청이 가는 곳입니다. 아래를 참조하세요.

infrastructure/compose/.env.prod.example에 모든 설정의 정확한 형태가 들어 있으며, 선택 블록(객체 스토리지, 비밀 저장소, SSO, 연결된 메일)은 해당 위치에 주석으로 표시되어 있습니다. 선택 설정을 포함한 전체 목록은 docs/deployment/configuration.md에 있습니다.

Docker Compose로 배포하기

.env.prod.example을 docker-compose.prod.yml 옆의 .env로 복사해 값을 채운 뒤, 데이터베이스를 띄우고, 빌드하고, 마이그레이션한 다음 서비스를 시작하세요. 순서가 중요합니다. 마이그레이션과 시드는 어떤 서비스도 응답하기 전에, 이미 실행 중인 데이터베이스를 대상으로 실행됩니다.

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는 일회용 부트스트랩 토큰을 발급하고 설정 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를 설정하면 부트스트랩 단계가 전혀 필요 없습니다. 외부 인증 제공자가 켜지는 순간 부트스트랩 모드가 중단됩니다. 그렇지 않으면 로그인 화면을 가로채게 되기 때문입니다. 그리고 SSO로 처음 로그인한 사람이 기본 팀을 만들고 그 팀의 소유자가 됩니다.

추론 구성

채팅은 OpenAI 호환 방식입니다. NESSIE_MODEL_BASE_URL은 엔드포인트, NESSIE_MODEL_API_KEY는 베어러 토큰, 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를 통해 사람을 인증하며, 조직과 팀 구조도 UnlikeOtherAI가 관리합니다. 관리자 로그인 페이지에는 “Login / Register” 버튼 하나만 표시됩니다.

이 연동은 표준 OIDC가 아니라 config-JWT 흐름입니다. API는 GET /api/auth/sso/config에서 서명된 RS256 설정 문서를 제공하고, GET /.well-known/jwks.json에서 대응하는 JWKS를 제공합니다. 두 엔드포인트 모두 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...”에서 멈춥니다.

운영 환경에서는 Caddy가 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로 거부합니다. 그래서 누군가 수동으로 정리하기 전까지 설치는 이전 릴리스에 머무릅니다. 프로젝트의 운영 환경도 하루 동안 이런 식으로 모든 배포를 거부당했습니다. 정리하려면 먼저 실패한 마이그레이션이 실제로 롤백되었는지 확인해야 합니다. 그 마이그레이션이 만든 객체는 하나도 남아 있으면 안 되고, 변경하려던 객체는 원래 모양 그대로여야 합니다. 이 확인을 마친 뒤에만 롤백된 것으로 해결 처리할 수 있습니다. 절반만 적용된 마이그레이션은 이 대상이 아닙니다.

자체 호스팅 Compose 배포의 백업과 복원은 저장소에 문서화되어 있지 않으므로, 이 페이지는 그 절차를 설명하지 않습니다.

한 대의 서버 또는 관리형 클라우드

위의 단일 호스트 Docker Compose 경로만 실제로 검증되어 있습니다. 프로젝트의 운영 환경이 바로 이 방식으로, 단일 서버에서 데이터베이스, 객체 스토리지, API, 워커, 관리자 화면, 사이트를 공유 리버스 프록시 뒤의 컨테이너로 실행합니다.

한 대의 기기를 넘어 확장하면 세 가지가 바뀌며, 이는 권고가 아니라 강제됩니다. local 모드 밖에서 Nessie는 API와 워커를 별도의 프로세스로, 각각 여러 복사본으로 실행합니다. 따라서 한 프로세스가 기기의 디스크를 소유한다고 가정하는 기능은 모두 거부됩니다. 파일 시스템 객체 스토리지는 시작 실패가 되고, docker 제공자를 쓰는 실행 환경 템플릿은 아무 동작도 하지 않으며 프로비저닝은 명확하게 실패합니다. 그리고 file_read, file_write, file_glob 내장 도구는 에이전트가 읽을 수 있는 실패한 도구 결과를 반환합니다. 각 거부 메시지에는 설정 이름, 모드, 대신 사용할 것이 함께 표시됩니다.

관리형 클라우드를 위해 저장소에는 Google Cloud를 대상으로 하는 Terraform 구성이 infrastructure/terraform/에 있습니다. API는 최소 인스턴스 1개를 둔 Cloud Run 서비스이며, 지속적인 Postgres LISTEN 클라이언트를 유지하고 유지보수 스윕을 실행하므로 0으로 줄이면 실시간 전달이 멈춥니다. 워커는 포트를 열지 않으므로 Service로 두면 절대 준비 상태가 되지 않아서, Cloud Run 워커 풀로 구성됩니다. 여기에 모든 롤아웃을 통제하는 마이그레이션 작업, PostgreSQL 17용 Cloud SQL, 그리고 S3 호환 백엔드를 통해 접근하는 GCS 버킷이 포함됩니다.

이 경로는 작성되었을 뿐 실제로 검증되지는 않았습니다. 저장소도 이를 분명히 밝히고 있으며, 이 페이지도 마찬가지입니다. 이 구성을 위한 Google Cloud 프로젝트는 존재하지 않고, 트리의 어느 부분도 실제 프로젝트를 대상으로 계획되거나 적용된 적이 없습니다. GCS로의 멀티파트 업로드 경로는 실제 버킷에서 테스트되지 않았고, 워커 풀 리소스 타입은 검증되지 않았으며, 그 안의 신뢰 프록시 홉 수는 측정된 값이 아니라 측정해야 할 자리표시자입니다. 관리자 화면과 공개 사이트는 이 구성에서 아예 모델링되어 있지 않습니다. 지원되는 배포가 아니라, 여러분이 직접 검증해야 할 출발점으로 다루세요.

배포의 데이터가 어디에 있는지, 소프트웨어가 무엇을 보관하고 무엇을 의도적으로 보관하지 않는지는 별도 페이지에 있습니다. EU에서 만든 제품과 데이터가 저장되는 곳.