自托管
安装与部署
在你自己的基础设施上运行整个平台。你需要一个 Supabase 项目来承载数据库与认证,另外准备 Docker 或 Node 之一。
一条命令完成部署
安装脚本会生成 .env,生成加密密钥,应用数据库迁移,并把整套服务启动起来。
./scripts/setup.sh --dockerpowershell -ExecutionPolicy Bypass -File scripts\setup.ps1添加 --dev 改用本地开发服务器而不是容器, --docgen 启用服务端 Office 渲染器, --notebooks 启用开发者工作区运行时。
手动部署
- 1
创建 Supabase 项目
它提供 Postgres、认证和存储。记下项目 URL、可公开的 key,以及 service-role key。 - 2
填写 .env 配置
复制.env.example,并填好下面列出的必填值。 - 3
应用迁移
npx supabase link --project-ref <ref>然后npx supabase db push。这会创建所有表、策略和存储桶。 - 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_KEY | service-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 不可恢复
模型与搜索
| 变量 | 用途 |
|---|---|
OPENROUTER_API_KEY | 零配置的模型兜底,让全新的工作区在任何人接入自己的提供商之前就能用。 |
OPENROUTER_DEFAULT_MODEL | 兜底所使用的模型 |
OPENROUTER_BASE_URL | 指向一个兼容网关,替代 OpenRouter |
OPENAI_API_KEY | 工作区级别的 OpenAI 密钥 |
FIRECRAWL_API_KEY | 工作区级别的网页搜索与页面抓取,供 web_search / web_browse 工具使用 |
数据连接
这些都不是必填——默认值就是推荐配置。它们的存在是为了针对特定数仓或网络做调优。
| 变量 | 默认值 | 用途 |
|---|---|---|
WAREHOUSE_MAX_ROWS | 1000 | 调用方未指定时返回的行数 |
WAREHOUSE_ABS_MAX_ROWS | 5000 | 任何调用方都不能超过的硬上限 |
WAREHOUSE_QUERY_TIMEOUT_MS | 60000 | 单次查询的挂钟时间预算 |
WAREHOUSE_MAX_CONCURRENT | 8 | 每个实例同时在跑的查询数 |
WAREHOUSE_MAX_CONCURRENT_PER_USER | 3 | 按租户计 |
WAREHOUSE_POOL | on | PostgreSQL / MySQL 系数据源的连接池。设为 off 则每次查询各开一个连接。 |
WAREHOUSE_POOL_MAX | 4 | 每套凭据的套接字数——见下面关于容量的说明 |
WAREHOUSE_POOL_MAX_KEYS | 64 | 同时持有的不同凭据套数 |
HTTPS_PROXY | — | 所有连接器出网流量的正向代理。NO_PROXY 接受一份绕行清单。 |
CONNECTOR_MAX_RETRIES | 2 | 对 429 / 503 和传输错误重试 |
CONNECTOR_RETRY_500 | off | 把 500 也纳入重试——仅适用于那些用 500 表示限流的提供商 |
CONNECTION_HEALTH_HOURS | 12 | 凭据重新校验的周期 |
CREDENTIAL_MAX_AGE_DAYS | 90 | 凭据在多久之后被标记为陈旧 |
连接池容量要按你的数据库来定,而不是照抄本页
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_SECURE | TLS 开 / 关 |
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 则表示即便数字查不出来,上限也必须守住。两种情况下失败都会被记录。 |
注意
网络出站
| 变量 | 用途 |
|---|---|
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 允许清单。设小了会读到你代理的地址而不是调用方的,允许清单永远匹配不上;设大了会读到调用方自己提供的值,而这正是该设置要堵住的绕过口。它会被限制在链长范围内,因此绝不会越界。小心
web_browse、集群的 HTTP 节点,或一条被注入的提示词指令——能够探入你的内网。如果确实必须启用,请只在没有公开嵌入的实例上启用。可观测性与审计
| 变量 | 用途 |
|---|---|
METRICS_TOKEN | 保护指标端点的 Bearer 令牌 |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP 采集器端点 |
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_BACKEND | docker | k8s | e2b |
NOTEBOOK_RUNTIME_IMAGE | 要启动的内核镜像 |
NOTEBOOK_GATEWAY_URL | WebSocket 网关地址 |
按使用场景给出的配置
上面的参考列出了每一项设置。下面这些是真正应该搭配使用的组合,以完整代码块给出,可直接粘贴进 .env。每一组都点明了它要应对的风险,因为默认值是按「单个可信运维者」选的,一旦别人也能访问这个实例,它就不再合适。
在笔记本电脑上试用
什么都没暴露,所以什么都不用加固。这就是默认状态;在别人也能访问这个应用之前,其余各套配置都可以忽略。
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"供单个团队使用的内部工具
在你的网络内或某个私有域名上可访问,没有匿名访客。这里要做的是关掉注册,并确保失控的智能体不会把账单无限拉高。
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.注意
在营销站点上做公开嵌入
最难的一种,因为匿名访客花的是 你的 额度,而你无法对他们做身份认证。这里每一项设置都在限定一个陌生人——或一枚泄漏的嵌入密钥——能让你花多少钱。
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"小心
受监管或离网环境
不允许任何出网,留存证据,并把审计痕迹送到应用无法改写的地方。
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注意
在负载均衡后自动扩容
多个应用实例对着同一个 Supabase 项目。速率限制和并发槽位在 Postgres 里计数,所以你设的数就是你得到的数——但有两项设置必须与拓扑一致。
# 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 内核,配套一个网关和一个默认拒绝的出网代理。 |
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 里,发现新问题就会更新。