自托管

安装与部署

在你自己的基础设施上运行整个平台。你需要一个 Supabase 项目来承载数据库与认证,另外准备 Docker 或 Node 之一。

一条命令完成部署

安装脚本会生成 .env,生成加密密钥,应用数据库迁移,并把整套服务启动起来。

bash
./scripts/setup.sh --docker
powershell
powershell -ExecutionPolicy Bypass -File scripts\setup.ps1

添加 --dev 改用本地开发服务器而不是容器, --docgen 启用服务端 Office 渲染器, --notebooks 启用开发者工作区运行时。

手动部署

  1. 1

    创建 Supabase 项目

    它提供 Postgres、认证和存储。记下项目 URL、可公开的 key,以及 service-role key。
  2. 2

    填写 .env 配置

    复制 .env.example ,并填好下面列出的必填值。
  3. 3

    应用迁移

    npx supabase link --project-ref <ref> 然后 npx supabase db push。这会创建所有表、策略和存储桶。
  4. 4

    启动

    docker compose up -d --build,或者 npm install && npm run dev。打开 http://localhost:8080.

迁移不是可选项

迁移没应用的功能会静默失败,而不是大声报错——存储桶不存在意味着上传的文件默默没保存下来,缺一列意味着某个设置无处可存。任何一次升级之后,先运行 npx supabase db push ,再断定某个功能坏了。

环境变量参考

应用会读取的每个变量,按用途分组。只有第一组是必填的;其余都是在改变你可能并不需要的行为。

必填——Supabase 与身份

变量用途
SUPABASE_URL项目 URL,服务端
SUPABASE_PUBLISHABLE_KEY匿名 key,服务端
SUPABASE_SERVICE_ROLE_KEYservice-role key。仅服务端使用——绝不能到达浏览器。
VITE_SUPABASE_URL同一个 URL,会内联进客户端产物
VITE_SUPABASE_PUBLISHABLE_KEY同一个匿名 key,客户端使用
ADMIN_EMAIL引导超级管理员账号
VITE_ADMIN_EMAIL同一个地址,用于客户端的管理入口显示
PROVIDER_CREDS_SECRET所有已存凭据的加密密钥。请务必备份——见下面的警告。
INTERNAL_RUN_SECRET为内部服务间调用签名

PROVIDER_CREDS_SECRET 不可恢复

所有已存凭据都用它加密,而它存在环境变量里而不是数据库里——所以单有一份数据库导出,拿不到任何密钥。丢了它,每个连接器、提供商密钥和 MCP 令牌都得重新录入。把它放在你存放其他应急密钥的地方,并与数据库分开备份。

模型与搜索

变量用途
OPENROUTER_API_KEY零配置的模型兜底,让全新的工作区在任何人接入自己的提供商之前就能用。
OPENROUTER_DEFAULT_MODEL兜底所使用的模型
OPENROUTER_BASE_URL指向一个兼容网关,替代 OpenRouter
OPENAI_API_KEY工作区级别的 OpenAI 密钥
FIRECRAWL_API_KEY工作区级别的网页搜索与页面抓取,供 web_search / web_browse 工具使用

数据连接

这些都不是必填——默认值就是推荐配置。它们的存在是为了针对特定数仓或网络做调优。

变量默认值用途
WAREHOUSE_MAX_ROWS1000调用方未指定时返回的行数
WAREHOUSE_ABS_MAX_ROWS5000任何调用方都不能超过的硬上限
WAREHOUSE_QUERY_TIMEOUT_MS60000单次查询的挂钟时间预算
WAREHOUSE_MAX_CONCURRENT8每个实例同时在跑的查询数
WAREHOUSE_MAX_CONCURRENT_PER_USER3按租户计
WAREHOUSE_POOLonPostgreSQL / MySQL 系数据源的连接池。设为 off 则每次查询各开一个连接。
WAREHOUSE_POOL_MAX4每套凭据的套接字数——见下面关于容量的说明
WAREHOUSE_POOL_MAX_KEYS64同时持有的不同凭据套数
HTTPS_PROXY—所有连接器出网流量的正向代理。NO_PROXY 接受一份绕行清单。
CONNECTOR_MAX_RETRIES2对 429 / 503 和传输错误重试
CONNECTOR_RETRY_500off把 500 也纳入重试——仅适用于那些用 500 表示限流的提供商
CONNECTION_HEALTH_HOURS12凭据重新校验的周期
CREDENTIAL_MAX_AGE_DAYS90凭据在多久之后被标记为陈旧

连接池容量要按你的数据库来定,而不是照抄本页

持有的套接字数大约是 WAREHOUSE_POOL_MAX × WAREHOUSE_POOL_MAX_KEYS 乘以你的副本数,且每个副本各自维护自己的连接池。在提高任何一个数值之前,先把这个乘积与数据库的 max_connections 对照一下。连接池是值得用的——实测把一次查询从 30.7 毫秒降到 2.9 毫秒——但池子开太大反而会把数仓的连接数额度耗尽。

邮件发送

邀请、告警通知和定时报告都需要它。请使用 二者之一 :Resend 或 SMTP。

变量用途
RESEND_API_KEY使用 Resend 发送
SMTP_HOST使用 SMTP 发送
SMTP_PORT—
SMTP_USER—
SMTP_PASS—
SMTP_SECURETLS 开 / 关
EMAIL_FROM外发邮件的发件人地址
SITE_URL邮件内链接所用的基础 URL
PUBLIC_APP_URL本实例的公开基础 URL

运行限制与成本

变量用途
SWARM_RUN_RATE_LIMIT_PER_MIN每个 API 密钥每分钟的请求数,超出返回 429
SWARM_RUN_MAX_CONCURRENT每个密钥的并发运行数
SWARM_RUN_TIMEOUT_MS单次运行的挂钟时间上限
ENFORCE_BUDGET_CAP让预算上限变为「拦截」而不只是告警。接受 1, true, yes。任何带公开嵌入的实例都应设置它——参见 预算.
BUDGET_FAIL_CLOSED当消费额的 查询本身 失败时该怎么办——注意这不同于查回来发现未超额。不设置(默认)会放行该调用——治理机制不该成为正常工作中断的原因。设为 true 则表示即便数字查不出来,上限也必须守住。两种情况下失败都会被记录。

注意

这些限制的计数发生在 Postgres 里,由所有实例共享,所以无论跑了多少份应用,你设的数就是你得到的数。如果数据库短时不可达,各实例会退回本地计数并记录这一点——限制是降级,而不是消失。

网络出站

变量用途
BLOCK_PRIVATE_NETWORK_FETCH拒绝指向私有地址、回环地址和链路本地地址的出站请求,包括云元数据端点。
ALLOW_PRIVATE_NETWORK_FETCH这是逃生口,用于数仓或 MCP 服务确实位于内网的情况。
TRUSTED_PROXY_HOPS有多少个 你自己的 反向代理位于应用之前。它决定 X-Forwarded-For 里哪一项被视作调用方——该请求头是追加写入的,所以只有你自己代理添加的那些项无法被伪造。默认 1 (单个反向代理);如果代理前面还有 CDN,请用 2 。只有 MCP 密钥的 IP 允许清单依赖这个值。

小心

设置 TRUSTED_PROXY_HOPS 使其与你的实际拓扑一致,再去依赖 MCP 密钥的 IP 允许清单。设小了会读到你代理的地址而不是调用方的,允许清单永远匹配不上;设大了会读到调用方自己提供的值,而这正是该设置要堵住的绕过口。它会被限制在链长范围内,因此绝不会越界。

小心

允许访问内网意味着由模型选定的 URL——来自 web_browse、集群的 HTTP 节点,或一条被注入的提示词指令——能够探入你的内网。如果确实必须启用,请只在没有公开嵌入的实例上启用。

可观测性与审计

变量用途
METRICS_TOKEN保护指标端点的 Bearer 令牌
OTEL_EXPORTER_OTLP_ENDPOINTOTLP 采集器端点
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT仅针对追踪的覆盖配置
OTEL_EXPORTER_OTLP_HEADERS发给采集器的额外请求头
OTEL_SERVICE_NAME追踪中上报的服务名
AUDIT_ARCHIVE_ON_PURGE到期时归档审计事件,而不是直接丢弃
PERSIST_PROMPT_BODIES是否把完整的提示词与响应内容存在追踪里。调试时信息丰富,但更占空间也更敏感——请慎重决定。

调度

变量用途
DISABLE_INPROCESS_SCHEDULER关掉 Web 层的进程内调度器——见下文的扩容部分。
BI_CRON_TOKEN外部 cron 调用 BI cron 端点时出示的令牌
NOTEBOOK_CRON_TOKEN同上,用于笔记本回收任务

文档渲染器

变量用途
DOCGEN_SERVICE_URL只有渲染器部署在非常规位置时才需要。留空即可——应用会探测 docgen:8099 和 localhost:8099,用哪个响应就用哪个。
DOCGEN_TOKEN应用与渲染器之间共享的 Bearer 令牌

笔记本运行时

变量用途
NOTEBOOK_RUNTIME_ENABLED开启服务端运行时
NOTEBOOK_RUNTIME_SECRET会话令牌签名密钥。不填则由服务端自动生成。
NOTEBOOK_RUNTIME_BACKENDdocker | k8s | e2b
NOTEBOOK_RUNTIME_IMAGE要启动的内核镜像
NOTEBOOK_GATEWAY_URLWebSocket 网关地址

按使用场景给出的配置

上面的参考列出了每一项设置。下面这些是真正应该搭配使用的组合,以完整代码块给出,可直接粘贴进 .env。每一组都点明了它要应对的风险,因为默认值是按「单个可信运维者」选的,一旦别人也能访问这个实例,它就不再合适。

在笔记本电脑上试用

什么都没暴露,所以什么都不用加固。这就是默认状态;在别人也能访问这个应用之前,其余各套配置都可以忽略。

bash
SUPABASE_URL="https://<project>.supabase.co"
SUPABASE_SERVICE_ROLE_KEY="..."
SUPABASE_PUBLISHABLE_KEY="..."
VITE_SUPABASE_URL="https://<project>.supabase.co"
VITE_SUPABASE_PUBLISHABLE_KEY="..."

# One key and you can use everything; per-user keys can come later.
OPENROUTER_API_KEY="sk-or-..."

ADMIN_EMAIL="you@example.com"
VITE_ADMIN_EMAIL="you@example.com"

供单个团队使用的内部工具

在你的网络内或某个私有域名上可访问,没有匿名访客。这里要做的是关掉注册,并确保失控的智能体不会把账单无限拉高。

bash
SITE_URL="https://agents.internal.example.com"
PUBLIC_APP_URL="https://agents.internal.example.com"

# Encrypts stored warehouse/SaaS credentials. Set once — rotating it
# invalidates everything already saved.  openssl rand -hex 32
PROVIDER_CREDS_SECRET="..."

# Caps stop being advisory.
ENFORCE_BUDGET_CAP="true"

# One reverse proxy in front (Caddy/nginx).
TRUSTED_PROXY_HOPS="1"

# Self-hosted Ollama or an in-cluster MCP server lives on a private address,
# so leave private-network fetches allowed — cloud metadata stays blocked
# either way.

注意

然后在「管理 → IAM」下打开 仅邀请注册 ,让登录页不再接受新注册。参见 访问控制.

在营销站点上做公开嵌入

最难的一种,因为匿名访客花的是 你的 额度,而你无法对他们做身份认证。这里每一项设置都在限定一个陌生人——或一枚泄漏的嵌入密钥——能让你花多少钱。

bash
SITE_URL="https://www.example.com"
PUBLIC_APP_URL="https://app.example.com"
PROVIDER_CREDS_SECRET="..."

# Refuse calls past the cap instead of emailing about them afterwards.
ENFORCE_BUDGET_CAP="true"
# If spend cannot be established, refuse rather than assume zero.
BUDGET_FAIL_CLOSED="true"

# A public embed has no reason to reach anything inside your network.
BLOCK_PRIVATE_NETWORK_FETCH="true"

TRUSTED_PROXY_HOPS="1"

# Tighten the public surface below the defaults (30/min chat, 10/min ask).
MCP_RATE_LIMIT_PER_MIN="30"
SWARM_RUN_RATE_LIMIT_PER_MIN="10"
SWARM_RUN_MAX_CONCURRENT="2"

小心

在此为每一枚嵌入密钥和集群 API 密钥各设一个上限: 预算。仅有按用户的上限不够:泄漏的密钥消耗的正是那份额度,而能挡住它的是按凭据的上限。

受监管或离网环境

不允许任何出网,留存证据,并把审计痕迹送到应用无法改写的地方。

bash
BLOCK_PRIVATE_NETWORK_FETCH="true"
ENFORCE_BUDGET_CAP="true"
BUDGET_FAIL_CLOSED="true"

# Traces to your own collector; nothing leaves for a vendor.
OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector.internal:4318"
OTEL_SERVICE_NAME="zenstudio-prod"

# Expiring audit rows are printed as NDJSON before deletion, so a log
# shipper keeps them past the database's own retention.
AUDIT_ARCHIVE_ON_PURGE="1"

# No model prices are fetched at runtime; the table is vendored in the repo
# and refreshed deliberately with:  npm run prices:refresh

注意

审计保留期在产品里设置,不在环境变量里——「管理 → IAM」,默认 365 天。删除用户不再删掉他的痕迹:记录会连同操作者邮箱一起保留,这样调查时仍有东西可查。

在负载均衡后自动扩容

多个应用实例对着同一个 Supabase 项目。速率限制和并发槽位在 Postgres 里计数,所以你设的数就是你得到的数——但有两项设置必须与拓扑一致。

bash
# MUST be set: instances resolve their own origin from this, never from the
# request's Host header.
PUBLIC_APP_URL="https://app.example.com"

# A dedicated secret for server-to-server calls, so the database master key
# stays out of outbound headers.  openssl rand -hex 32
INTERNAL_RUN_SECRET="..."

# CDN in front of the load balancer? Then two hops, not one.
TRUSTED_PROXY_HOPS="2"

# Alerts, refreshes and purges run in-process. A cross-instance lease stops
# them double-firing, but the tidier arrangement on a fleet is to disable
# them on the web tier and drive /api/bi/cron from one external scheduler.
DISABLE_INPROCESS_SCHEDULER="true"
BI_CRON_TOKEN="..."

可选服务

服务数据概况它带来什么
文档生成渲染器--profile docgen通过 python-pptx、python-docx、openpyxl 与 LibreOffice 在服务端生成 PowerPoint / Word / Excel——即「深度」生成模式。
笔记本运行时--profile notebooks为开发者工作区提供真实的 Python 内核,配套一个网关和一个默认拒绝的出网代理。
bash
docker compose --profile docgen --profile notebooks up -d --build

两者都是可选的。没有渲染器时,文档在浏览器里生成,「深度」模式会置灰并说明原因。没有笔记本运行时,笔记本会退回到浏览器内的 Python 运行时。

部署形态

Docker Compose
默认形态。一个应用容器,加上你启用的任意可选 profile。单机足以支撑一个规模不小的团队。
反向代理后的 Node
直接构建并运行服务端。TLS 在你的代理层终结。
负载均衡器后的自动扩缩虚拟机
应用层是无状态的,所以需要多少个相同容器就跑多少个。设置 DISABLE_INPROCESS_SCHEDULER=1,并由一个外部 cron 驱动后台任务。
Kubernetes
我们提供了应用和笔记本运行时的清单文件,其中包含把内核挡在公网之外的出网策略。

扩容

应用层是无状态的——不需要会话粘滞,所以想在负载均衡后放多少实例都可以。这么做时有两点要注意:

  • 调度器。 告警、刷新和清理都在进程内运行。跨实例租约能防止重复触发,但更清爽的做法是在 Web 层设 DISABLE_INPROCESS_SCHEDULER ,再用一个外部 cron 去调 cron 端点。
  • 限制在整个集群范围内生效。 速率限制和并发槽位在 Postgres 里计数,所以无论跑了多少实例,你配的数就是你得到的数。如果数据库短时不可达,实例会退回本地计数并记录这一点——限制是变弱,而不是消失。预算上限是另一道天花板,计数方式相同;参见 预算.
  • 设置代理层数。 TRUSTED_PROXY_HOPS 必须与你自己有多少层代理在前面一致—— 1 表示只有一个负载均衡, 2 表示前面还有一个 CDN。MCP 密钥的 IP 允许清单校验的就是它选出来的那个地址。

笔记本的 Docker 运行时按设计就是单机的——它在自己所在的主机上启动容器。要分散部署请改用 Kubernetes 编排。

运营

健康检查
有一个健康检查端点上报进程存活状态——把负载均衡指向它。
备份
所有持久状态都在 Supabase 里。请使用它的备份功能,并把 PROVIDER_CREDS_SECRET 单独保存——没有它的数据库备份,凭据部分是读不出来的。
升级
拉取代码、重新构建,然后推送迁移。迁移是增量式的;跨多个版本升级前请先看发布说明。
日志
平台层面看容器日志;智能体做了什么看应用内的追踪。两者回答的是不同的问题——智能体行为异常时先看追踪。

对外暴露之前

  • 关掉公开注册,或强制走 SSO—— 访问控制.
  • 设置 ENFORCE_BUDGET_CAP ,并给每一枚嵌入密钥和 API 密钥都设上限。
  • 用 TLS 提供服务;service-role key 绝不能到达浏览器。
  • 把嵌入密钥限定到你自己的域名。
  • 复核对话、会话记录和审计的保留期限。
  • 备份 PROVIDER_CREDS_SECRET 到一个你真的取得回来的地方。

注意

安装过程中的问题及其解法都收在仓库的 docs/INSTALL.md 里,发现新问题就会更新。