跳到主要内容

自部署指南

用一条 Docker 命令即可自部署 UnDercontrol。all-in-one 镜像把前端和后端打包在同一个容器里, 无需自己拼装——运行即可打开浏览器使用。

镜像同时发布 linux/amd64linux/arm64(Apple 芯片、ARM 服务器),同一条命令在任何机器上都能用。

快速开始(免费 / Personal)

无需许可证。单用户,SQLite,本地文件存储。

docker run -d --name undercontrol \
-p 3000:8080 \
-e HOST_DOMAIN=http://localhost:3000 \
-e JWT_SECRET=change-me-to-a-random-string \
-v undercontrol-data:/app/data \
lintao0o0/undercontrol:latest

然后打开 http://localhost:3000,点击 Start 即可使用。前端和后端在同一容器中, 自动通过 /api/v1 连接——无需配置服务器地址。

首次启动 banner

容器日志会让成功和失败一目了然。启动成功时,docker logs undercontrol 的末尾会打印一个 ready banner,直接告诉你去哪打开、用什么账号登录:

==============================================================================

UnDercontrol v1.x.x is ready

--> Open http://localhost:3000 to get started

Login as: personal@undercontrol.local
default password: personal123 (set PERSONAL_TIER_PASSWORD to change it)
Tier: Personal (max users: 1)
Database: SQLITE
Storage: LocalFS

==============================================================================

如果配置有误,容器会立即退出,同一份日志里会出现 STARTUP FAILED 块,明确指出要修什么—— 缺 HOST_DOMAIN、Pro/Max 下缺 ADMIN_EMAIL,或端口被占用。密码提示只在账号仍使用出厂默认密码时才会出现。

裸机部署(npm,无需 Docker)

服务端也以 npm 包发布,Web UI 直接编译进二进制——除了 Node.js 18+ 什么都不用装。 支持 macOS(Intel 和 Apple Silicon)、Linux(x64 和 ARM64)、Windows(x64)。

npm install -g @oatnil/ud-server @oatnil/ud   # 服务端 + CLI

ud-server -host-domain http://localhost:8080 -data-path ./data

然后打开 http://localhost:8080——终端里会打印与 Docker 相同的 ready banner, 包含登录凭据。所有数据都在 ./data 下(SQLite 数据库和上传文件),备份或迁移 实例就是复制这个目录。

  • 配置与 Docker 完全一致:配置参考 里的每个环境变量都同时是 CLI 参数(ud-server -help 可查看全部)。HOST_DOMAIN 是唯一必填项。
  • 许可证同理:启动前 export LICENSE_TOKEN / LICENSE_HOST_SECRET 即可解锁 Pro 功能。
  • 升级:npm update -g @oatnil/ud-server;卸载:npm uninstall -g @oatnil/ud-server./data 目录不受影响)。
  • 想以服务方式常驻,用 systemd / launchd 包一层即可,和任何单二进制程序一样。

Pro / Max(多用户)

加上许可证和管理员账号即可启用多用户、PostgreSQL、S3 存储和管理后台。许可证请联系 UnDercontrol 团队获取。

docker run -d --name undercontrol \
-p 3000:8080 \
-e HOST_DOMAIN=http://localhost:3000 \
-e JWT_SECRET=change-me-to-a-random-string \
-e ADMIN_EMAIL=admin@example.com \
-e ADMIN_PASSWORD=your-secure-password \
-e LICENSE_TOKEN=your-license-token \
-e LICENSE_HOST_SECRET=your-license-host-secret \
-v undercontrol-data:/app/data \
lintao0o0/undercontrol:latest

用你设置的 ADMIN_EMAIL / ADMIN_PASSWORD 登录。

Pro/Max 必须设置 ADMIN_EMAIL

Pro/Max tier 启动时会用 ADMIN_EMAIL 创建初始管理员账号。如果缺失,服务会直接拒绝启动并给出明确报错—— 启动前请先设置它(以及 ADMIN_PASSWORD)。

docker-compose

如需持久化数据和更方便的配置,推荐使用 docker-compose:

services:
undercontrol:
image: lintao0o0/undercontrol:latest
ports:
- "3000:8080"
volumes:
- ./data:/app/data
environment:
- HOST_DOMAIN=http://localhost:3000
- JWT_SECRET=change-me-to-a-random-string
# 仅 Pro/Max:
# - ADMIN_EMAIL=admin@example.com
# - ADMIN_PASSWORD=your-secure-password
# - LICENSE_TOKEN=your-license-token
# - LICENSE_HOST_SECRET=your-license-host-secret
docker compose up -d

环境变量

下表覆盖大多数部署会用到的变量。服务端读取的全部配置项(含交互式配置生成器和启动预览) 见配置参考

变量是否必填默认值说明
HOST_DOMAIN客户端访问本实例的公开 URL,用于生成文件下载/上传链接,必须可达(如 http://localhost:3000https://ud.example.com)。
JWT_SECRET用于签发认证 token 的随机密钥。
ADMIN_EMAILPro/Max初始管理员的登录用户名。Pro/Max tier 必填。
ADMIN_PASSWORDPro/Maxadmin123初始管理员密码,请务必修改。
LICENSE_TOKENPro/Max解锁 Pro/Max 功能的许可证 token。
LICENSE_HOST_SECRETPro/Max与许可证 token 配套的 host secret。
PERSONAL_TIER_PASSWORDpersonal123Personal tier 唯一用户(personal@undercontrol.local)的密码。请在首次启动前设置:Start 自动登录始终读取该变量,用户创建后只改环境变量、或只在应用内改密码,都会导致自动登录失效(两者必须一致;登录名本身不可修改)。
PORT8080服务在容器内监听的端口。
UD_ENCRYPTION_KEYIM 必需用于加密用户密钥(目前是各自的 Telegram bot token)。任何人连接 IM 之前必须设置:未设置时「即时通讯」区会拒绝保存 token 并给出说明。视为每个实例永久不变——更换会使已保存的 token 全部失效,用户需要重新粘贴。
IM_MAX_BYO_BOTS20本实例最多同时运行多少个用户自带 bot(每个 bot 占用一条长轮询连接)。

可选:PostgreSQL、S3 与 AI

all-in-one 镜像默认使用 SQLite + 本地文件存储,对大多数自部署实例已经足够。Pro/Max 下可通过额外环境变量接入外部服务:

  • PostgreSQL — 设置 DATABASE_URL(或单独的 DB_* 变量)以替代内置 SQLite。
  • S3 / R2 存储 — 设置 S3_* 变量,把上传文件存到 S3 兼容对象存储(AWS S3、Cloudflare R2、MinIO),替代本地卷。
  • AI 服务 — 设置 OpenAI 兼容的 AI_* 变量以启用 AI 功能。

可选:Alfred 的 Telegram 通道

Alfred 是内置的管家 Agent。用户在网页端任意评论里 @alfred 即可与他对话,这在任何实例上都开箱可用、 无需任何配置。IM 通道则把 Alfred 带到手机上:每位用户连接自己的 Telegram bot,随时随地找他。

不再有全实例共用的 bot token。每位用户自己建 bot、自己持有凭据,聊天只会到达这个 bot——在多人实例上, 这是唯一站得住脚的做法。

运维方只需做一次:

  • UD_ENCRYPTION_KEY 设为一串随机密钥。bot token 以 AES-256-GCM 加密存储,没有密钥就无法保存; 缺失时「即时通讯」区会提示用户联系管理员。之后更换会使已存 token 全部失效,所以请在用户开始使用前定下来。
  • 可选:调整 IM_MAX_BYO_BOTS(默认 20),即本实例同时运行的用户 bot 数量上限。 该值也可在管理后台 → 系统配置 → Integration 中随时修改。
  • 多人实例上,决定何时把 IM 通道对你以外的用户开放——见下文《多用户开放》。

每位用户自己完成:

  1. 在 Telegram 里找 @BotFather,发送 /newbot,拿到 token。bot 的名字和头像归自己。
  2. 设置 → 即时通讯 粘贴 token。服务器会先向 Telegram 验证再保存,因此 token 输错会当场报错, 而不是留下一个永远不回话的 bot。保存后不可回读——界面只显示 bot 用户名和掩码。
  3. 向自己的 bot 发送 /start。Telegram 要求先 /start,bot 才能给你发消息。
  4. 在同一区域生成一次性绑定码,10 分钟内向自己的 bot 发送 /link 绑定码

每个 bot 都是私人入口:不是它主人的人向它发消息会被礼貌拒答,绑定码也只能在其主人自己的 bot 上核销。 更换 token 会重连;移除会停止轮询并删除凭据,而账号绑定与对话归档都保留。

若 token 之后被 Telegram 拒绝(例如在 @BotFather 里重置过),用户会在自己的「即时通讯」区看到 token 被拒绝以及 Telegram 给出的原因,粘贴新 token 即可恢复。管理员不再有修复入口, 也不会退回到别人的 bot 代发。

DISCORD_BOT_TOKEN / --discord-bot-token 配置层面仍可读取,但 Discord provider 尚未实现—— 只设置它不会启动任何东西。

多用户开放

IM 通道默认只对实例所有者开放:im.multi_user_enabled 默认为 false,关闭期间其他用户在「即时通讯」区 只会看到「多用户暂未开放」。网页端 @alfred 不受影响。

确认边界之后,在管理后台 → 系统配置中打开它——每条消息都会在某台机器上运行 Agent 会话, 这个开关决定的正是:第二个用户的消息可以到达谁的机器。

从旧版共享 bot 升级

早期版本用一个全实例的 TELEGRAM_BOT_TOKEN。该配置项已移除,升级需要两步手动操作。 升级前请先读完本节。

自动迁移只认存为运行时设置项 integration.telegram.bot_token 的 token。该设置项只在开发过程中 短暂存在过,没有随任何一个版本发布,所以在所有真实实例上,token 都在 TELEGRAM_BOT_TOKEN 环境变量(或对应的启动参数)里——而迁移不读这个。也就是说:不会有任何东西被自动迁移, 而且失败是静默的。 容器照样健康启动,日志照样显示通道已启动。

请按下面的顺序升级:

  1. 先把现有的 TELEGRAM_BOT_TOKEN 的值抄到安全的地方,再动其他任何配置。 升级后它就失效了,而这是你手上唯一的一份。
  2. 设置 UD_ENCRYPTION_KEY,取一个随机值(openssl rand -hex 32)。 没有它,服务端会拒绝存储任何 bot token。请视为永久不变——参见上文该变量的说明。
  3. 照常升级镜像并启动实例。
  4. 手动重新登记 token。 所有者打开 设置 → 即时通讯,粘贴第 1 步抄下来的 token。 服务端会先向 Telegram 校验再存储,所以粘错会当场报错,而不是变成一个永远不回话的 bot。
  5. 再重启一次实例。 这一步很容易漏掉,但很关键:旧的共享 bot 创建的会话和投递记录还没有 归属到任何一个用户的 bot 上,而新的运行时只能看到已归属的记录。这次重启做的正是这件事, 并且它只有在已经存在一个 bot 时才有效——所以必须排在第 4 步之后。

在第 5 步完成之前,通道看上去是正常的,但发往已有会话的回复会被静默丢弃。界面上不会有任何 报错,唯一的迹象是日志里的一行 IM relay has no live bot for a conversation, dropping message, 其中 botState="unmigrated"。升级后新建的会话不受影响,这会让问题看起来时好时坏。

验证方式是真发一条消息、确认收到回复——容器健康、日志干净、通道已启动,这三条都不能证明消息发得出去。

确认即时通讯可用之后,就可以把 TELEGRAM_BOT_TOKEN 从配置里删掉了。在那之前留着它没有任何代价, 而且可以让你原地回滚到上一个版本。

其他用户不迁移,因为他们本就没有属于自己的东西可迁——他们用的是运维方的 bot。每个人连接自己的即可。

首次启动引导

用户首次打开一个全新实例时,会看到一个五步引导。每一步都可跳过;已经配置好的内容会直接显示为完成状态, 而不会再次询问。这里没有任何一项是服务运行的前提——一个什么都没配的实例同样能顺利走完引导。

  1. 语言 —— 界面使用英文还是中文。
  2. 工作区状态 hooks —— 询问是否允许向工作区项目的 .claude/settings.local.json 添加 Claude Code hooks,桌面端正是靠它展示实时的 Agent 状态(运行中 / 等待中 / 空闲)。未获许可不会写入任何内容。
  3. 把本机注册为 daemon —— daemon 是实际运行 Agent 会话的机器。在 UnDercontrol 桌面应用里这是一键完成的, 同时会扫描本机已安装的 Agent CLI(Claude Code、Codex 等)。在浏览器里,该步骤会推荐安装桌面应用, 或给出适用于服务器/远程机器的无界面方案:npm install -g @oatnil/udud loginud daemon start。 两种方式都一样:只要服务端看到有 daemon 在线,该步骤即完成。
  4. 连接 Telegram —— 用户用 @BotFather 建好自己的 bot 并粘贴 token,与「设置 → 即时通讯」里是同一套流程。 与其他步骤一样可跳过;第一条指引会指回第 3 步,因为没有机器在背后的 bot 什么也答不了。 未设置 UD_ENCRYPTION_KEY 的实例上,该步骤会说明暂时无法保存 token,并提示联系管理员。
  5. 认识 Alfred —— 介绍这位内置管家(把活派给合适的 Agent、记住你的偏好与决策、随手记下临时想法), 并告诉用户在网页评论里 @alfred 就能找他。

运维方需要做什么: 如果希望第 4 步可用,请在用户走引导之前设置 UD_ENCRYPTION_KEY。 引导中其余步骤都不依赖服务端配置;跳过第 4 步的用户之后随时可以在「设置 → 即时通讯」连接自己的 bot。

前后端分离镜像(进阶)

如果需要前端和后端独立扩缩容,或分别放在不同代理后面,它们也各自发布为多架构镜像:

  • 后端 — lintao0o0/undercontrol-backend:latest
  • 前端 — lintao0o0/undercontrol-vite-app:latest

后端使用与上面相同的环境变量;前端通过 nginx 提供静态资源并把 /api 反代到后端。

运行多个后端副本。 正确性不再依赖单副本:每位用户同时只有一个 Alfred 会话,这一约束已由数据库索引保证—— 抢输的副本插入失败,并把消息交给赢的那个会话。不过 IM 通道仍然更适合单副本:每个副本都会为每个用户 bot 各开一条长轮询,而 Telegram 只允许一个消费者,多副本同时轮询会互相抢消息。有用户连接 IM 时请只跑一个后端副本; 前端无论如何都可以自由扩缩。

数据与备份

所有状态都在 /app/data(上面挂载的卷)下:SQLite 数据库,以及默认情况下的上传文件。备份该卷即可备份整个实例。 如果使用外部 PostgreSQL 和 S3,则改为备份它们。

故障排查

先看 docker logs undercontrol:正常启动的日志以上面的 ready banner 结尾;配置有误则以 STARTUP FAILED 块结尾,直接点名要修的变量。

  • 容器启动后立即退出 — 读日志里的 STARTUP FAILED 块。常见原因:缺 HOST_DOMAIN、 Pro/Max 下缺 ADMIN_EMAIL、端口被占用。
  • no matching manifest for linux/arm64/v8 — 更新到最新镜像,现已同时发布 amd64 和 arm64。
  • 不知道去哪登录 — ready banner 里打印了访问 URL 和当前 tier 的登录账号。
  • 文件链接无法访问HOST_DOMAIN 必须是客户端实际访问实例的 URL,包含协议和端口。
  • 仍无法解决时,携带日志和配置联系支持(去除敏感信息)。