OpenHands Agent Canvas 自托管实战:VM 单端口部署、Public 模式鉴权与 nginx TLS 加固
本文基于仓库自托管指南 docs/SELF_HOSTING.md 展开,讲解如何把 OpenHands Agent Canvas 部署到一台始终在线的 VM 上,通过浏览器从任意设备访问:包括单端口 ingress 的路由架构、--public 公开模式下的 API key 鉴权机制、防火墙与 nginx + Let's Encrypt 加固,以及把远程机器注册回本地前端作为多后端之一。读完后你能够独立完成一台可公网访问、且具备基础安全防御的 Agent Canvas 部署。
威胁模型:为什么自托管必须先谈安全
在开始任何配置之前,必须先理解部署安全的第一性原理。Agent Canvas 驱动的是一个能够读写宿主机文件系统、执行 shell 命令、访问网络的编码 agent;任何能够与 agent server 通信的人,都拥有同样的能力。因此指南明确要求:把承载 Agent Canvas 的 VM 当作一台保存生产凭据的机器来对待,在暴露到公网之前必须先做加固。
这套部署模型叠加了两层核心防御:
- 云 / 网络防火墙——默认除了来自你 IP 的 SSH 外,任何入站流量都不可达;配置域名后额外放行 80 和 443(理想情况下 443 仍限制在你的 IP 白名单)。
LOCAL_BACKEND_API_KEY+ public 模式——所有/api/*调用都必须携带匹配的X-Session-API-Key请求头,且 UI 在用户与 agent 交互之前强制要求先输入该 key。
只要其中一层失效,另一层仍然能把陌生人与 agent 隔离开。
部署架构:单端口 ingress 的路由模型
整台 VM 上只暴露一个 ingress 端口。npx @openhands/agent-canvas --public 会一次性拉起三个服务,并让它们全部藏在 127.0.0.1:8000 的 ingress 代理之后,nginx 只需要知道这唯一的一个端口:
flowchart LR
user(["You"])
subgraph vm["Your VM (single host)"]
direction LR
nginx["nginx :443 (TLS)"]
ingress["Ingress proxy 127.0.0.1:8000"]
static["Static server :3001"]
agent["Agent server :18000 (LOCAL_BACKEND_API_KEY)"]
automation["Automation backend :18001"]
nginx --> ingress
ingress -- "/*" --> static
ingress -- "/api/*, /sockets" --> agent
ingress -- "/api/automation/*" --> automation
end
user -- "HTTPS / 443" --> nginx
端口分工如下:
| 服务 | 端口 | 职责 |
|---|---|---|
| Ingress proxy | 127.0.0.1:8000 |
唯一对外(经 nginx)的入口,按路径前缀路由 |
| Agent server | :18000 |
OpenHands Agent Server,处理 /api/* 与 /sockets |
| Automation backend | :18001 |
自动化调度,处理 /api/automation/* |
| Static frontend | :3001 |
前端静态资源,默认路由 /* |
这些默认值在 config/defaults.json 中有单一事实来源:ports.agentServer: 18000、ports.automation: 18001,ingress 与前端端口则在启动脚本中取默认 8000 与 3001。
ingress 代理的实现位于 scripts/ingress.mjs,几个值得注意的细节:
- 路由匹配:按路径前缀最长前缀优先匹配(见
showHelp中的 ROUTE MATCHING 说明),因此/api/automation一定先于/api命中,这正是 mermaid 图中两条路由能并存的原因;没有匹配到后端时直接返回503; - WebSocket 支持:
server.on("upgrade", ...)分支把/sockets路径上的 WebSocket 升级请求转发到 agent server,这就是前端能收到 agent 实时事件、nginx 配置中又必须写Upgrade/Connection "upgrade"头的原因; --public的强制约束:在 scripts/dev-with-automation.mjs 中,--public与--frontend-only互斥("public mode" 需要后端才能校验 key),且--public启动时若未设置LOCAL_BACKEND_API_KEY会直接报错退出,提示形如LOCAL_BACKEND_API_KEY=my-secret npm run dev -- --public。
Public 模式的工作原理:key 如何"不进入前端"
--public 标志开启公开模式:API key 不会被打进前端构建。用户首次加载 UI 时会看到一个 API key 输入屏,必须粘贴 LOCAL_BACKEND_API_KEY 才能进入。没有 --public 时,key 会被自动注入前端——方便纯本地使用,但绝不适用于公网可达的部署。
源码中这一行为有清晰的证据链(scripts/dev-with-automation.mjs):
// In local mode, bake the session key into the frontend so the user
// never has to paste it. In public mode, omit the key and set
// VITE_AUTH_REQUIRED so the frontend shows the API key entry screen
// immediately (no network round-trip needed).
if (config.launchAgentServer && config.isPublic) {
viteEnv.VITE_AUTH_REQUIRED = "true";
} else if (config.launchAgentServer) {
viteEnv.VITE_SESSION_API_KEY = config.sessionApiKey;
}
即:本地模式下把 key 烘焙进前端环境变量 VITE_SESSION_API_KEY;public 模式下刻意省略该值并设置 VITE_AUTH_REQUIRED=true,让前端立即展示 key 输入屏。使用静态构建(而非 Vite dev server)时同理,启动静态服务器会传 --session-api-key <key>(本地模式)或 --auth-required(public 模式)。
关于 key 本身的生成与存储,scripts/dev-safe.mjs 提供了几点佐证:
LOCAL_BACKEND_API_KEY是用户侧唯一面向的 key 环境变量;显式设置时优先生效,否则回退到持久化文件~/.openhands/agent-canvas/api-key.txt(文件权限0o600),保证 agent server、前端烘焙值、浏览器中已注册的 backend 条目在多次重启间指向同一个值;- 内置的
generateRandomApiKey()用randomBytes(32)生成 256-bit 随机 key——与文档推荐的openssl rand -base64 32在熵强度上等价; - 使用
export而非命令行内联传参,是为了让 key 不出现在进程列表(ps aux)中。
步骤 1:准备一台机器
任何始终在线、网络稳定的 Linux(或 macOS)主机都可以:
- 云 VM——DigitalOcean、AWS EC2、GCP、Hetzner、Linode 等,Ubuntu 24.04 LTS 是个稳妥的默认选择,单用户场景 2 vCPU / 4 GB 内存足够;
- 专用硬件——Mac Mini、Intel NUC、闲置笔记本。注意:任何从你的 LAN 可达的机器都是威胁模型的一部分。
步骤 2:先加固机器,再启动服务
这一步必须在首次启动 agent server 之前完成。默认姿态是:除 SSH(且仅限你自己的 IP)外,公网不可达任何入站流量。虽然所有服务都绑定在 127.0.0.1(见步骤 3),但网络防火墙才能保证"即使某个服务意外绑错地址,也没有人够得着"。
在云厂商 / 网络层面限制入站流量(DigitalOcean Cloud Firewall、AWS Security Group、GCP firewall rule 等):
- 入站 22(SSH)——限制到你自己的 IP / VPN CIDR;
- 其余全部——丢弃。ingress 端口(
:8000)、agent server(:18000)、automation backend(:18001)、static server(:3001)都不能从宿主机外部访问。
完成之后,机器只能通过 SSH 访问。这已经足够运行 agent(步骤 3)并通过 SSH 隧道使用 UI;若想免隧道地从浏览器访问,则在步骤 4 开放 80 与 443。
一个容易忽视的安全细节:捆绑的 OpenVSCode 编辑器与画布共享同一个浏览器源(origin)。它不是绑定自己的发布端口,而是通过路径前缀(默认 /vscode,见 config/defaults.json 中 paths.vscodeBasePath 及注释)挂在代理端口之下——这正是部署只需一个端口的原因,但路径前缀只负责路由、不负责隔离:该源上运行的任何脚本(包括经编辑器扩展或受污染资源加载的内容)都能读取画布的 localStorage,而其中存着该浏览器注册的每一个后端的 SESSION API key。该问题在上游 issue #16492 中被跟踪,配置公网部署时应了解这一暴露面。
步骤 3:运行 Agent Canvas
先在机器上安装依赖。Ubuntu 下:
apt-get update
apt-get install -y curl git
# Node.js 22.x(用 nvm、asdf 或 NodeSource 均可)
# uv(agent-server 的 uvx 运行时需要):
curl -LsSf https://astral.sh/uv/install.sh | sh
macOS(Mac Mini 等)改用 brew 安装 Node 与 uv。Node 版本的硬性下限可参见 package.json 中 engines.node: ">=22.12.0"。
以 public 模式启动:
export LOCAL_BACKEND_API_KEY=$(openssl rand -base64 32) # 只生成一次,妥善保存
npx @openhands/agent-canvas --public
openssl rand -base64 32 生成密码学随机的 256-bit key——把打印出来的值复制到安全的地方再往下走。这条命令会拉取最新发布版本,启动 agent server、automation backend 与静态前端,并在 127.0.0.1:8000 前置 ingress 代理。
SSH 会话断开后要让服务继续运行,需要进程管理器:
方案 A —— tmux(快速验证):
export LOCAL_BACKEND_API_KEY=<your-saved-key>
tmux new-session -d -s canvas 'npx @openhands/agent-canvas --public'
# 之后重新连接:tmux attach -t canvas
方案 B —— systemd(长期部署推荐):
创建 /etc/systemd/system/agent-canvas.service:
[Unit]
Description=Agent Canvas
After=network.target
[Service]
Environment=LOCAL_BACKEND_API_KEY=<your-key>
ExecStart=npx @openhands/agent-canvas --public
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
然后启用并启动:
sudo systemctl daemon-reload
sudo systemctl enable --now agent-canvas
最后重申风险:agent server 直接运行在宿主机上,拥有对文件系统、环境变量与网络的完整权限。防火墙(步骤 2)与 LOCAL_BACKEND_API_KEY 是阻止陌生者获得同等权限的最后两道闸门。
步骤 4(可选):域名 + nginx + Let's Encrypt
如果希望不依赖 SSH 隧道从浏览器访问 UI(例如手机,或不便做端口转发的机器),给主机指一个域名并用 nginx + TLS 前置。nginx 负责终止 TLS,把流量转发到 127.0.0.1:8000 的 ingress。
域名指向机器
创建一条 A 记录指向机器的公网 IPv4,例如 canvas.example.com。验证 DNS 已传播:
dig +short canvas.example.com
开放 80 与 443
回到网络防火墙,额外放行入站:
- 80(HTTP)——对
0.0.0.0/0开放(Let's Encrypt 的 HTTP-01 验证所必需)。nginx 会把所有流量重定向到 HTTPS; - 443(HTTPS)——如果可以,限制到你自己的 IP / VPN CIDR。如果必须对全网开放(例如经常移动办公),
LOCAL_BACKEND_API_KEY就是你的主要防线。
安装 nginx 与 certbot
apt-get install -y nginx certbot python3-certbot-nginx
nginx 站点配置
将以下内容写入 /etc/nginx/sites-available/canvas.example.com,替换为你的域名:
server {
listen 80;
listen [::]:80;
server_name canvas.example.com;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket / SSE 支持 —— agent 实时事件的必需项。
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
其中 Upgrade / Connection "upgrade" 与小时级超时不是可选装饰:ingress 代理本身处理 WebSocket 升级(scripts/ingress.mjs 的 upgrade 事件分支),而 agent 的实时事件流依赖长连接,默认的 60 秒 proxy_read_timeout 会让会话静默断开。
启用、测试并签发证书:
ln -sf /etc/nginx/sites-available/canvas.example.com \
/etc/nginx/sites-enabled/canvas.example.com
nginx -t && systemctl reload nginx
certbot --nginx -d canvas.example.com \
--non-interactive --agree-tos \
--email you@example.com \
--redirect
certbot 会自动加上 listen 443 ssl 配置块、HTTP 到 HTTPS 的 301 重定向,并安装自动续期的 systemd timer。
验证
curl -I https://canvas.example.com/ # → 200(显示 API key 输入屏)
curl -I http://canvas.example.com/ # → 301 跳转到 https
如果出现 502 Bad Gateway,说明 127.0.0.1:8000 上的应用挂了——检查 npx 进程是否还在运行。然后在浏览器打开 https://canvas.example.com/,输入 LOCAL_BACKEND_API_KEY,确认能进入 Agent Canvas。
步骤 5(可选):把远程机器接入本地 Agent Canvas
如果你本地已经跑着 Agent Canvas,可以把这台远程机器注册为额外后端,在 UI 中于本地与远程之间切换——这正是 Agent Canvas 多后端架构的价值所在:
- 在本地 Agent Canvas 中打开 Manage backends → Add a backend(对应前端实现 backend-form-modal.tsx,表单中"Session API key"字段在远程后端场景下是必填且非空的):
- Host Name——任意好记的名字,例如
my-vm; - Host——步骤 4 的 URL,例如
https://canvas.example.com;若走 SSH 隧道则用http://localhost:8000; - Session API key——步骤 3 中设置的
LOCAL_BACKEND_API_KEY。
- Host Name——任意好记的名字,例如
- 保存。新后端应显示 "Connected",随后在后端切换器中选择它,即可把指令发给远程机器上的 agent。
参考:关键默认值与适用前提
结合 config/defaults.json 与 package.json,当前仓库(@openhands/agent-canvas 1.16.0)的默认值速查:
| 项目 | 默认值 | 来源 |
|---|---|---|
| Ingress 端口 | 8000(绑定 127.0.0.1) |
scripts/ingress.mjs |
| Agent server 端口 | 18000 |
config/defaults.json |
| Automation backend 端口 | 18001 |
config/defaults.json |
| 前端静态端口 | 3001 |
scripts/dev-with-automation.mjs |
| Agent server / Automation 版本 | openhands-agent-server 1.44.0 / openhands-automation 1.9.0 |
config/defaults.json |
| 编辑器路径前缀 | /vscode |
config/defaults.json |
| Node 要求 | >=22.12.0 |
package.json |
适用前提与限制:本指南面向"agent server 直接跑在宿主机"的无沙箱模式(README 中明确警告该模式下 agent 拥有完整文件系统访问权,Docker 沙箱模式是另一条独立路径);key 的自动持久化文件(~/.openhands/agent-canvas/api-key.txt)是本地模式的便利设施,公网部署应始终显式 export LOCAL_BACKEND_API_KEY;端口均绑定回环地址,因此"防火墙 + API key"双层防御成立的前提是不要擅自把 18000/18001/3001 直接发布出去。
更多背景可继续参考仓库内的 docs/README.md、docs/architecture.md 与 README.md 中关于多后端切换的说明。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00