自建 OpenWork 这个开源桌面应用:容器与 Helm 两条路,先算清四笔账
本文基于 openwork 仓库 commit 3b41381(2026-08-03)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/different-ai/openwork 最新代码与文档为准。
决定要不要自建 OpenWork,最该先看的不是 Helm 命令,而是 ee/LICENSE——因为你打算部署的那套控制面,源码正好落在 /ee 目录下,它不按根目录的 MIT 走。 这一条比集群怎么开、数据库放哪儿都更早决定事情能不能做。
这里说的 OpenWork 指 GitHub 上的 different-ai/openwork 这个开源项目,一个把技能、MCP 连接与外部服务打包成可共享「能力」的桌面应用。它不是泛指的「开放工作」这类概念,也和同名的职场点评网站没有关系。仓库 README 这样定位自己:一个免费、开源、用于分享 AI 工作流的桌面应用,覆盖 macOS、Windows 和 Linux。
仓库自建文档 packages/docs/start-here/self-host.mdx 开篇就把话说在前面:多数人直接用桌面应用就够了,自建是给那些希望 OpenWork 跑在自己服务器、VPC 或私有云、再让桌面端远程连过去的团队准备的。这句限定值得认真对待——如果你是一个人用,下面这些账根本不需要算。
站内另外三篇和这篇分工不同:AI 基础设施怎么选型 讲的是要不要自建、按什么维度比较,MCP 服务的生产部署 讲 MCP 服务本身上生产的通用做法,容器隔离怎么做 讲运行时隔离层面的取舍;这篇只盯一个具体项目,把 OpenWork 仓库里真实存在的部署面、许可证条款和坑位摊开。
一、你到底要部什么:一个远程工作区,还是一整套控制面
self-host.mdx 把 OpenWork 拆成四块普通服务:桌面或 Web 客户端本身、用于登录与 worker 启动的 Den web、承担认证与 worker 供给的控制面 Den controller、以及可选的推理代理与计量服务。这里有个容易找错地方的细节:文档里说的 Den controller,源码目录并不是同名那个——ee/apps/den-controller 现在只剩一份说明,写着它已被 ee/apps/den-api 取代,认证、组织、管理端与 worker 端点都归后者。照着旧名字去翻目录,看到的会是一个空壳。
分界线画得很干脆:如果你只需要一个私有的远程工作区,部 worker 运行时就行;如果你要的是账号、团队、组织管理这一整套,那就得部 Den web、Den controller,推理服务按需。这两件事的工作量差着数量级,很多人一上来就照着 Helm 文档走,其实需求只是前者。
从仓库结构也能看出这条分界。全仓 3490 个受版本控制的文件里,apps/ 下 4 个应用、packages/ 下 12 个包属于开放部分;ee/apps/ 10 个应用、ee/packages/ 3 个包是另一层。Helm chart 叫 openwork-ee,它拉的镜像是 openwork-den-api、openwork-den-web 和可选的 openwork-inference——这三者的源码目录都在 ee/ 下面。文档量也不小:packages/docs/ 有 57 份 mdx(其中 model-context-protocol/ 占 10 份),架构与操作类的 docs/ 有 20 份 md,另有 evals/ 26 份流程说明。
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| Den API | 控制面 API,监听 8788,承载认证、组织、install-link 认领 | ee/apps/den-api | 一旦要多人账号体系,必部 |
| Den Web | Next.js 前端,监听 3005,/api/den 路由代理到 Den API | ee/apps/den-web | 桌面端指向的那个 baseUrl 就是它 |
| 推理服务 | 可选的模型代理与计量,chart 里默认关闭 | ee/apps/inference | 要自己做模型计量时才开 |
| worker 运行时 | 承载工作区文件系统状态与 opencode 的 SQLite 数据 | ee/apps/den-worker-runtime | 只要一个私有远程工作区时,只需要它 |
| Helm chart | EE 发布物,模板化 Deployment、Service、ConfigMap、Secret、健康探针、迁移 Job | packaging/helm/openwork-ee | 走 Kubernetes 路线的唯一入口 |
| 各云的 values 起步文件 | AWS/GCP/Azure 三套示例值 | packaging/helm/openwork-ee/examples/ | 第一次填 values 时直接抄 |
| 容器构建与本地栈 | 生产镜像 Dockerfile 与本地 compose 编排 | packaging/docker/ | 本地跑通、或自己重打镜像时 |
| 数据库迁移 | 迁移 Job 与 bootstrap 脚本 | ee/packages/den-db | 每次升级前都要跑通 |
| 桌面客户端 | Electron/React 客户端 | apps/desktop | 配置它指向你的 Den web 源 |
packaging/ 目录下一共三种分发方式:docker、helm、aur。前两种是自建相关的,第三种是 Linux 桌面分发。
二、容器与 Helm 解决的不是同一个问题
看 packaging/docker/ 会发现里面混着两类东西,别搞混。
一类是本地开发栈。packaging/docker/den-dev-up.sh(或 pnpm dev:den-docker)一条命令拉起 MySQL、Den 控制面(8788,PROVISIONER_MODE=stub)和 Web 应用(3005),迁移在 API 启动前自动跑。它打印随机化的主机 URL,好让多套栈并存。这条路是给你在笔记本上摸清楚服务关系用的,不是生产姿态——文档也明说本地那串 mysql://root:password@127.0.0.1:3306/openwork_den 只能用于开发。
另一类是生产镜像:Dockerfile.den 出 ghcr.io/different-ai/openwork-den-api,Dockerfile.den-web 出 openwork-den-web,Dockerfile.inference 出 openwork-inference。README 里写明这些镜像是给 Terraform、Helm、ECS、EKS 和客户云部署用的,并建议生产环境用不可变 tag 或 digest。另外还有一个更小的形态:单容器里跑 openwork-server(8787),内部托管 opencode,工作区挂在 /workspace,主机数据目录挂在 /data,访问靠 OPENWORK_TOKEN 与 OPENWORK_HOST_TOKEN 两个令牌。这正好对应「我只要一个私有工作区」那条路,opencode 不直接暴露,只能经 OpenWork 的 /opencode/* 代理走。
Helm 是另一条路,也是仓库明显推荐的那条。docs/aws-eks-helm.md、docs/gcp-gke-helm.md、docs/azure-aks-helm.md 三份指南给出的判断口径一致:除非客户明确跑不了 Kubernetes,否则就用 Helm,因为 EE 的发布物本身就是 Helm chart,服务拆分也天然映射到 Kubernetes。AWS 那篇还补了一句挺诚实的复盘——AWS 侧实测发现的短板不是 Helm 本身,而是缺云侧基础设施指导和缺 chart 上的云服务注解开关。
三家云的推荐形态各不相同,别互相套用:
- AWS:EKS Auto Mode 集群 + RDS for MySQL,直接用
LoadBalancer类型的 Service 让 AWS 起 Network Load Balancer,默认起两个(web 一个、API 一个),ACM 证书在 NLB 上终止 TLS。 - GCP:GKE Autopilot + Cloud SQL for MySQL,走 GKE Ingress 配 Google 托管证书、预留全局 IP 和显式后端健康检查。文档同时诚实交代:Google 对新的 L7 流量管理推荐 Gateway API、GKE Ingress 已进入维护模式,当前 chart 只发 Ingress 资源,所以 Gateway API 支持算未来的加固项。
- Azure:AKS 的应用路由附加组件所带的托管 NGINX Ingress + Azure Database for MySQL Flexible Server。
GCP 和 Azure 两篇都特意写了同一句反向建议:不要把裸 LoadBalancer Service 当作常规路径,那是四层负载,浏览器面向的 Web 与 SSO 流程需要七层的主机路由、托管证书和后端健康检查。
第一笔账在这里:所有权边界。 AWS 那篇把它列得最清楚——EKS 集群、节点生命周期、VPC 网络、负载均衡器、RDS、DNS、TLS 证书、IAM、安全组归云厂商;OpenWork 的 chart 只管 Deployment、Service、ConfigMap、Secret、健康探针和数据库迁移 Job。中间那一层没人管,就是你。
三、许可证这笔账必须在开集群之前算
这是最容易写错、也最要命的一条。
仓库根目录的 LICENSE 是一份分层声明,三句话:/ee 目录下的全部内容按 ee/LICENSE 定义的许可证(根 LICENSE 把它称作 Fair Source License);并入 OpenWork 的第三方组件各随其原始许可证;除此之外的部分才是 MIT,版权标注为 Copyright 2026 Different AI。
而 ee/LICENSE 文件自身的抬头写的是 Functional Source License, Version 1.1, MIT Future License,缩写 FSL-1.1-MIT,版权归 Different AI Inc。它的核心是「Permitted Purpose」——除 Competing Use 之外的任何用途都算许可范围。所谓 Competing Use,文件里给了三条判定:把该软件放进商业产品或服务提供给他人、并且构成对该软件的替代;构成对许可方在软件发布之日已提供的其他产品或服务的替代;或提供相同、实质相似的功能。文件也正面列了明确属于许可范围的几种:自用与内部访问、非商业教育、非商业研究,以及为按本条款使用该软件的被许可方提供专业服务。另外它带一条 Grant of Future License——在该版本对外发布之日起满两周年后,可按 MIT 使用。
把这条和上一节的目录结构对上就明白了:chart 名叫 openwork-ee,Den API 和 Den Web 的源码目录是 ee/apps/den-api、ee/apps/den-web。你自建的那套团队控制面,恰好就是走 ee/LICENSE 的那部分。所以凡是讲到团队控制面、企业能力的地方,不能笼统写成「MIT 开源」——这个项目的许可是分层的。
至于你的具体场景能不能商用、能不能改、改完能不能对外提供,一律以许可证原文为准,必要时找法务判断。本文不提供法律意见,只提醒这个判断必须发生在开集群之前,而不是上线之后。
四、密钥、证书与数据库:自建真正的成本集中在这里
自建的账单里,硬件不是大头,你要亲自扛的东西才是。
数据库。 Den 控制面用 MySQL 兼容数据库,本地 Docker 用 MySQL 8.4,生产可以用标准 MySQL 或 PlanetScale 兼容凭据;文档明确写了当前 Den 部署路径不需要 Postgres。worker 运行时那侧则是文件系统状态加挂载路径里的 opencode SQLite 数据。生产要求写得很具体:数据库、备份、对象存储、worker 卷、以及可能含运维元数据的日志都要开静态加密;应用到数据库的流量要求 TLS;并且要用托管的、带自动故障转移和备份的 MySQL 兼容库,或者自己提供等价冗余,同时把恢复流程、备份保留期、RTO、RPO 和负责故障转移与恢复的人写下来。最后一句才是自建的真实代价——那个「负责人」得是你团队里一个具体的人。
TLS 模式别糊弄。 文档专门点破一个常见误解:sslmode=require 只是把连接加密了,它不做证书身份校验。要真校验,得用 sslmode=verify-ca、sslmode=verify-full,或严格等价的 sslaccept=strict,并通过数据库平台或 Helm 的 customCa 提供 CA 包。AWS 指南给的冒烟路径是在私有 RDS 上先用 ?sslaccept=accept——保持 TLS 但不要求把 RDS 的 CA 包挂进镜像,之后再切严格校验。如果迁移 Job 日志里出现 self-signed certificate in certificate chain,通常就是严格校验开在了 CA 包就位之前。
两个密钥。 BETTER_AUTH_SECRET 和 DEN_DB_ENCRYPTION_KEY,AWS 指南建议各用一次 openssl rand -base64 48 生成,且明确要求不要跨环境复用。DEN_DB_ENCRYPTION_KEY 至少 32 字符,OpenWork 用它加密数据库里选定的敏感列,但文档同时提醒:它不替代基础设施层的静态加密。这两把钥匙丢了或泄了,后果都由你承担——API 密钥的安全管理 里的那套轮换与最小暴露面思路在这儿同样适用。
四个互不相通的信任面。 certificate-trust-and-proxies.mdx 把它拆成四块:桌面端的 Electron/Chromium、被拉起的本地运行时与 sidecar、Den 容器与 Helm、以及严格模式下的 MySQL TLS。这四处不共用同一个证书存储。文档还点了一个很实际的坑:NODE_EXTRA_CA_CERTS 和 Helm 的 customCa 都是进程启动期设置,在浏览器里或应用内的环境变量编辑器里改,对 Den 来说已经太晚了。
认证。 Den 用 Better Auth,跑在你的部署里、用你的数据库,配 BETTER_AUTH_SECRET、BETTER_AUTH_URL、可信来源,以及可选的 GitHub/Google OAuth 凭据。SSO(SAML/OIDC)按文档说法处于当前推进中,设计上对接你自己的身份提供方。如果你根本不部 Den,worker 访问就退回令牌模式,靠 OPENWORK_TOKEN 和 OPENWORK_HOST_TOKEN。
五、边界与代价:这套设计明确不管什么
自建文档里有一段很少见的坦白,说的是「Next.js 前端 + Hono 后端配 MySQL + Electron/React 桌面端」这三件套覆盖了大多数功能,但不包含四样东西:经沙箱的远程代码执行、分析统计、走 OpenTelemetry 的遥测、以及用于邀请和生命周期消息的事务邮件。想要这些,要么自己接,要么接受没有。
同一份文档列的可选厂商也说明了边界在哪:托管部署路径用 Render,云 worker 的可选沙箱是 Daytona,可选分析是 PostHog(可以用 NEXT_PUBLIC_POSTHOG_HOST 指向自托管,或在自建构建里清掉 key),此外还有可选的计费、生命周期消息、社交登录与 GitHub 仓库连接器。核心运行时不强依赖这些,但你想要的那个功能可能刚好依赖其中之一。
私网部署里有三件事会让运维当场懵:
第一,内网 MCP 服务器会被拒。症状是加或测一个私有地址的 MCP 服务器时报 MCP_URL_BLOCKED。原因是 Den 在服务端去取 MCP URL,默认把私有和保留地址当 SSRF 风险挡掉。解法是开 DEN_ALLOW_PRIVATE_MCP_URLS=1(或 Helm 里对应的公开配置值),但文档说得很直白:这等于关掉那层 SSRF 防护,只在 Den 的网络位置可信、且有权添加 MCP 连接的人也可信时才用。这是一个信任决定,不是气隙部署的必要条件。
第二,Cloud MCP 诊断会被跳过。Settings → Debug 会报没观察到实时云目录,说该端点不在诊断信任策略内——因为诊断只对受信来源发带凭据的探测,默认信任的是官方托管来源而非你的自建 Den。文档特意提醒:不要把探测被跳过当成部署坏掉的证据,Cloud MCP 在真实对话里仍可工作。
第三,Den 的出网自检默认指向公网主机。DEN_DIAGNOSTICS_ORIGIN 默认是公开的诊断服务,VPN 隔离的 Den 当然够不着。要修就得把诊断应用部到内网可达的位置,再用 DEN_DIAGNOSTICS_ORIGIN 和 DEN_DIAGNOSTICS_BEARER_TOKEN 指过去。这个检查是可选的、管理员手动触发的,不是 Den 的后台依赖。
出网这笔账也得单算。Den 侧的硬需求其实很小:MySQL 端点,加上部署或拉镜像时能取到 chart 和镜像(ghcr.io,除非你镜像到内部 registry)。其余全是按功能开的。但桌面端不一样——安装包与更新走 github.com 与 release-assets.githubusercontent.com,npx 拉 openwork-ui-mcp 走 registry.npmjs.org,模型目录默认在 models.openworklabs.com(可用 OPENCODE_MODELS_URL 指向内部镜像)。这些被封掉不会让应用打不开,但会让对应功能静默失效。
隔离部署文档里还有一句值得抄到评审纪要上:把安装包挂载到内网只解决了安装包分发,它不等于整个产品气隙——npm、模型目录、模型厂商 API、GitHub 插件内容、OAuth 元数据、邮件投递、诊断、桌面更新和证书信任,每一项都得当独立依赖单独评审。相关的一个默认值也符合这个思路:对于没有获批访问外部泄露密码查询接口的隔离 Kubernetes 部署,Helm 默认关掉该查询,邮箱密码登录的本地锁定仍然保留。
什么时候不适用?两头都有。一个人用、只想要个工作区,桌面应用直接装就完了,上面这些一条都不用管。另一头,文档写在 150 人以上规模的公司铺开时,单台自建 VM 会更难管,那种规模通常要 SSO、审计日志、集中执行的模型与工具白名单、以及 VPC/本地部署——这些能力的落点,又回到了上一节的许可证分层问题。
六、上手与避坑清单
每条都带上「为什么会踩」,因为大部分坑的共同点是不报错。
1. 用 values 文件,不要堆 --set。 会踩是因为看起来更快。OpenWork 有几个值是逗号分隔字符串,比如 config.public.corsOrigins,普通 --set 的解析经常把它拆坏,而拆坏之后不会立刻报错,等到浏览器 CORS 报错时你已经在查别的地方了。避法就是照抄 packaging/helm/openwork-ee/examples/ 里对应云的起步文件,把 REPLACE_* 占位全换掉。
2. chart 的键写错是静默忽略的。 这条最阴。文档点名 config.urls、config.databaseUrl、secrets.* 这几个写法会被 chart 直接忽略——正确的是公开 URL 归 config.public.*、数据库与应用密钥归 secret.values.*。写错不报错,部署照样起来,只是行为不对。避法是装之前先渲染再核对,helm template 输出到文件,然后 grep DATABASE_URL、BETTER_AUTH_URL、DEN_API_PUBLIC_URL、DEN_WEB_PUBLIC_ORIGIN 这几个键确认值是你想要的。分享渲染结果或终端输出前记得脱敏。
3. 排查迁移 Job 别用 kubectl describe job。 会踩是因为这是所有人排查 Job 的肌肉记忆。问题在于该 hook Job 当前会把 DATABASE_URL 和 DEN_DB_ENCRYPTION_KEY 渲染进环境里,describe 出来的内容一旦贴进工单或群里,你的库密码和列加密密钥就泄了。避法是看日志加看脱敏后的渲染清单;确实需要保留日志时,临时把 migrations.hook 设成 false、backoffLimit 设成 0,用 --wait=false 装完再按 job-name 取日志,调完改回 hook: true 与 backoffLimit: 2。
4. 迁移跑在部署之前,顺序别倒过来查。 迁移 Job 在 Deployment 和 Service 安装前执行,它失败的时候运行时可能看起来是「就绪」的。会踩是因为你盯着 pod 状态就以为没事。避法是先 kubectl get jobs 确认迁移过了,再去调 web 和 API 的就绪。
5. ownerEmails 留空等于把 owner 让给第一个访问的人。 chart 默认是 single_org 模式,首次登录前就该把 config.tenancy 下的组织名、slug、ownerEmails 和 config.public.bootstrapAdminEmails 配好。文档写得很客气——留空的话第一个够到部署的用户就能认领所有权,不推荐用于生产。会踩是因为冒烟测试时没人管这个,然后冒烟环境悄悄变成了生产。
6. 先把 HTTPS 和域名稳定下来,再测 SSO。 会踩是因为想省事,直接拿云给的裸负载均衡器主机名往下走。浏览器的认证 cookie 和身份提供方的回调策略在稳定 HTTPS 源上才好验证,裸主机名下的失败原因很难归因。避法是先做 DNS 和证书,再进 SSO;webOrigin、apiOrigin、corsOrigins、betterAuthTrustedOrigins、authCallbackUrl 必须一起指向最终的 HTTPS 域名,其中任何一个和实际不匹配,表现就是登录循环或 CORS 报错。IdP 侧的回调地址、以及 SAML 的 ACS 与 metadata 地址,用 OpenWork 生成显示的那份,别自己拼——文档也说了 OpenWork 会拒绝未签名或弱签名的 SAML 响应。
7. 单源拓扑下 DEN_API_PUBLIC_URL 仍然要配对。 会踩是因为把桌面端指向 Den web 之后,很自然地觉得 API 源不重要了。实际上它不需要是第二个面向桌面的源,但值必须正确——在单源形态下应设为代理后的 API 基址,例如 https://openwork.example.internal/api/den。Den 用这个值处理安装链接认领、OAuth 回调、webhook、MCP 的 resource/audience 值和各种链接。顺带一提,外部 MCP 客户端和桌面端是两回事,它们要够到你给它们配的那个 MCP 端点。
8. EKS Auto Mode 新集群 kubectl get nodes 是空的,这不是故障。 会踩是因为空列表看着就像集群没起来,于是开始重建。Auto Mode 在有待调度负载时才供给算力,第一批 pod 排上之前返回 No resources found 属正常。同理,pod 显示未容忍 Auto Mode 污点时,通常是还在选容量,等 NodeClaim 出来再看事件。
9. 自建默认不开套餐门禁。 文档提到自建安装除非运维显式设置 DEN_PLAN_GATING_ENABLED=true,否则门禁是关的,所以 SSO 管理在 EE 自建默认可用。会踩的反向情况是:SSO 设置界面显示企业门禁,那八成是这个开关被打开了。
七、动手之前的自检
按顺序过一遍,任何一条答不上来就先别开集群:
一,你要的是私有工作区还是完整控制面?前者一个容器加两个令牌就够,后者才需要下面全部。二,ee/LICENSE 的 Permitted Purpose 条款下,你的用法是否成立?拿不准就先找法务,别先部署。三,谁负责数据库故障转移与恢复,RTO 和 RPO 写在哪个文档里?四,DEN_DB_ENCRYPTION_KEY 和 BETTER_AUTH_SECRET 存在哪、怎么轮换、谁能看到?五,你打算关掉的那些能力(沙箱执行、遥测、事务邮件、分析)里,有没有哪一项其实是业务必需?六,出网策略批下来了吗——ghcr.io 和数据库是硬需求,其余按功能逐项审。
接下来该读哪个文件,取决于你走哪条路:走 Kubernetes 就从 packaging/helm/openwork-ee/README.md 和 packaging/helm/openwork-ee/examples/ 里对应云的 values 起步文件开始,再配你那朵云的 docs/aws-eks-helm.md、docs/gcp-gke-helm.md 或 docs/azure-aks-helm.md;只想先在本机摸清服务关系,就看 packaging/docker/README.md;网络和信任面是主要顾虑,就按 packages/docs/start-here/ 下的 private-network-deployment.mdx、outbound-network-access.mdx、certificate-trust-and-proxies.mdx 三份顺序读。
自建之后的日子和部署那天不是一回事,权限模型、监控告警、升级节奏都得跟上,这部分可以参考 Agent 的日常运维 里的思路。这个项目仍在高频迭代,上面每一处配置键、默认值和端口都以你当时拉到的那份仓库代码与文档为准。
本篇属于一个把开源AI 工作流桌面应用 OpenWork逐层拆开讲的系列,整体地图见 OpenWork 是什么:把技能与 MCP 打包成能力的开源桌面应用;沿着这条线往下,还可以看 OpenWork 开源桌面应用策略:能锁住什么、锁不住什么、链路在哪断 和 OpenWork 开源桌面应用怎么在没有外网的环境活下来:三份文档合起来才是一套离网方案。