发布日期:2026-11-09 | 话题:DeepSeek Harness、公网部署、Nginx、TLS | 约 2,000 字

DeepSeek Harness(dsh)是 DeepSeek AI 在 2026 年开源的 Agent harness,其 Web UI 默认只监听 loopback,不能直接让公网浏览器访问。把 0.2.2-rc.1 部署到公网,需要同时处理三件事:用 --host 绑定本机地址、用 --public-url 公告外部地址、用 --trusted-host 接受浏览器 Host;再由反向代理完成 Host 保留、路径前缀剥离、WebSocket 升级转发和 cookie 作用域改写。默认端口 3080,通配 --host 会被拒绝;TLS 可在代理或监听器终止,启动 URL 含进程凭据,3080 不应直接暴露公网。本文给出一套可复制的 Nginx 配置、TLS 两种终止方式,以及启动凭据和端口边界的安全清单。


DeepSeek Harness 公网部署,是把默认只监听 loopback 的 Web UI 放到反向代理后面,通过 --public-url 公告外部地址、--trusted-host 接受浏览器 Host,并用 TLS 保护浏览器到服务端链路的一组配置。

一、先给结论:公网部署不是加一个 --host 0.0.0.0

截至 2026-11-04 发布的 0.2.2-rc.1,dsh --profile web 默认只监听 loopback,其他机器上的浏览器无法直接访问。官方 CLI 帮助把 --host 限定为“本机某个具体 IPv4/IPv6 地址”,通配地址会在启动时被拒绝;未启用 TLS 的非 loopback 监听还会在启动时警告。

所以公网部署要同时处理三层,而不是改一个参数:

层

关键参数

作用

常见误判

监听层

--host / --port

决定 DSH 绑定哪个本机地址和端口

以为 0.0.0.0 能省事,实际被拒绝

公告层

--public-url

决定打印 URL、DSH_WEB_URL 和 Agent 上下文里的地址

以为配置后外部就能访问

信任层

--trusted-host

决定 /api 的 Host/Origin 栅栏接受哪个 authority

不配置,代理正确也返回 403

一句话结论:--public-url 负责“告诉浏览器去哪”,--trusted-host 负责“让 DSH 接受这个浏览器”,监听地址负责“端口开在哪里”。三者不能互相替代。

二、先启动一个只给本机代理访问的 DSH

同机反向代理是最常见的部署方式。先让 DSH 绑定 127.0.0.1,只允许本机 Nginx 访问:

dsh --profile web \
  --no-open \
  --host 127.0.0.1 \
  --port 3080 \
  --public-url https://app.example/ui/ \
  --trusted-host app.example

这条命令里:

  • --no-open:服务器上没有桌面浏览器,避免启动时尝试打开页面。

  • --host 127.0.0.1:只监听本机回环;公网入口交给代理。

  • --public-url https://app.example/ui/:公告带 /ui/ 前缀的外部地址,末尾斜杠会被归一化。

  • --trusted-host app.example:接受浏览器可见的 app.example authority;不带端口时匹配任意端口。

如果 DSH 跑在容器里,官方示例是绑定容器自己的 Pod 地址,而不是通配地址:

dsh --profile web --no-open \
  --host "$(hostname -i | awk '{print $1}')" \
  --public-url https://app.example/ \
  --trusted-host app.example

注意:--public-url 只改变公告,不配置监听器、路由或 cookie 作用域。真正的外部链路归反向代理所有。

三、--trusted-host 配错,代理再对也是 403

DSH 的 /api 浏览器信任栅栏会比对请求的 Host。它接受三类 authority(也就是 host 或 host:port):loopback、监听器自身的绑定地址,以及每个由 --trusted-host 点名的值。

最常见的 403 来自代理把 Host 改写成了 127.0.0.1:3080。浏览器发的是 app.example,DSH 收到的是 127.0.0.1:3080,栅栏不匹配,于是每个 API 调用都返回 403。

因此代理必须保留浏览器可见的 Host,而不是改写成上游地址。--public-url 只是展示,不等于接纳;即使公告 URL 写对了,没有 --trusted-host 仍然会被拒绝。

四、反向代理必须做对的 5 件事

官方部署指南把代理职责列得很清楚。只要用路径前缀 /ui/,下面五项都要满足:

  1. 保留 Host:原样转发浏览器可见的 Host,不要改写成监听器地址。

  2. 剥离挂载前缀:DSH 监听器只认 origin-root 路由,/ui/api/... 必须以 /api/... 到达后端。

  3. 转发升级请求:每个请求和 WebSocket 升级都要带 Upgrade 与 Connection 头。

  4. 改写 cookie 作用域:后端签发的是 Path=/ 的 host-only cookie;代理要把 Path 改成 /ui/,并在 HTTPS 链路上补 Secure。

  5. 重定向裸挂载:/ui/ 是唯一入口,/ui 必须 301 到 /ui/,否则启动 token 无法换成会话 cookie。

一个可用的 Nginx 配置如下(proxy_cookie_flags 需要 Nginx 1.19.3+):

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name app.example;

    ssl_certificate     /etc/letsencrypt/live/app.example/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example/privkey.pem;

    location = /ui {
        return 301 /ui/;
    }

    location /ui/ {
        proxy_pass http://127.0.0.1:3080/;
        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_cookie_path / /ui/;
        proxy_cookie_flags ~ secure;
        proxy_read_timeout 3600s;
    }
}

这段配置里,map 要放在 Nginx 的 http 上下文,server 块按你的站点配置放置;proxy_pass 末尾的 / 负责剥离 /ui/ 前缀,$http_host 保留浏览器 Host,map 让 Connection 在 WebSocket 升级时变成 upgrade,普通请求时变成 close。

如果代理终止 TLS,而 Nginx 到 DSH 是明文 HTTP,proxy_cookie_flags ~ secure; 这一行是必要的;如果浏览器侧也是 HTTP,就不要加,否则浏览器不会回传 cookie。

五、TLS 在哪终止:代理终止,还是监听器自己终止

DSH 支持两种 TLS 终止位置,二者不是互斥关系。

方案

浏览器到代理

代理到 DSH

证书管理

适合场景

代理终止

HTTPS

HTTP(内网)

代理或证书平台

同机 Nginx、云负载均衡

监听器终止

HTTPS

HTTPS

手动传入证书和私钥

无代理、端到端加密

无代理、直接对公网提供 HTTPS 时,要绑定具体网卡地址,而不是 loopback:

dsh --profile web \
  --host "$(hostname -i | awk '{print $1}')" \
  --port 3080 \
  --public-url https://app.example:3080/ \
  --trusted-host app.example \
  --tls-cert ./server-chain.pem \
  --tls-key ./server-key.pem

hostname -i 可能返回多个地址,命令只取第一个;请换成这台机器实际对外的网卡地址,仍然不能用通配地址。

证书文件要包含完整证书链,私钥必须是不带口令的 PEM。两个路径都相对进程工作目录解析;只传一个会报用法错误,两个都传才会启用 HTTPS。

DSH 不会获取、续期或监视证书,也不会回退到 HTTP。文件缺失、为空、PEM 无效或证书与私钥不匹配时,启动会直接失败。更换证书后要重新加载监听器或重启进程,因为证书只在绑定前读取一次。

证书的 SAN 必须覆盖浏览器实际访问的 hostname 或 IP;默认端口仍是 3080,--public-url 不会改变监听端口。

六、启动 URL 是凭据,端口是最后一道边界

打印出来的 dsh web: URL 带有进程凭据,只应发给预期用户。官方文档明确说:启动 token 会换成签名会话 cookie,之后的每个 API 方法和 WebSocket 流都要认证。

这意味着不要把启动 URL 贴到群里、工单里或公开文档里。需要隐藏 URL 行时,可以在 Web profile 的 web-runtime 配置里设置 printUrl: false;凭据不会出现在模型上下文或 DSH_WEB_URL 中,但已经拿到 URL 的人仍然可以完成登录。

另一个容易漏掉的边界是端口本身。--public-url 和 Host 栅栏都不保护监听端口,所以不要把 3080 直接暴露到公网。把 DSH 绑定在 127.0.0.1,只让本机代理访问;如果必须跨机,至少用防火墙或安全组限制来源。

例如把 DSH 跑在七牛云 QVM 上时,可以让 3080 只监听回环地址,公网只放行 443,再由 Nginx 转发到本机 3080。这样即使公告 URL 配错,攻击面也不会直接落到 DSH 的 HTTP 端口。

七、一套可复制的部署顺序

  1. 先在本机用 --host 127.0.0.1 启动,确认 https://app.example/ui/ 能打开。

  2. 配 --public-url 和 --trusted-host,用 curl -I 检查 403 是否消失。

  3. 配反向代理,重点检查 Host、/ui/ 前缀、WebSocket Upgrade 和 cookie Path。

  4. 在代理或监听器上启用 TLS,确认浏览器地址栏是 HTTPS。

  5. 用外部网络访问一次,检查登录、流式输出和长连接是否正常。

  6. 收紧端口:公网只开 443,3080 只对本机或代理网段开放。

  7. 把启动命令交给 systemd、supervisor 或容器编排托管,并记录证书续期方式。

如果只记一条:先让 DSH 留在 loopback,再把代理放到它前面;不要为了省事把监听地址改成通配地址。

常见问题

Q:--public-url 配了,为什么还是 403? A:--public-url 只公告地址,不授予信任。还要用 --trusted-host 点名浏览器可见的 authority,并确保代理没有改写 Host。

Q:能不能直接 --host 0.0.0.0 让外部访问? A:不能。DSH 明确拒绝通配地址,--host 只接受本机某个具体 IPv4/IPv6 地址;公网访问应通过反向代理。

Q:换了 TLS 证书,为什么还是旧证书? A:监听器只在绑定前读取一次证书和私钥,运行期间不会重新读取。更换文件后要重新加载监听器或重启进程。

Q:反向代理后 WebSocket 老是断? A:检查 Upgrade 和 Connection 是否转发,Host 是否保留,以及代理的读超时是否足够长。路径前缀场景还要确认 /ui/ 已被正确剥离。

Q:启动 URL 能直接发给同事吗? A:它包含进程凭据,只应发给预期用户。需要多人使用时,先确认认证和权限边界,必要时在代理层再加一层访问控制。

结语

DeepSeek Harness 的公网部署,难点不在“把端口开出去”,而在三个地址概念不要混淆:监听地址决定服务在哪,--public-url 决定浏览器看到什么,--trusted-host 决定 DSH 接受谁。把 DSH 留在 loopback、让代理处理 Host/前缀/WebSocket/cookie,再用 TLS 和端口边界收口,部署才算完整。

本文基于 DeepSeek Harness 0.2.2-rc.1(2026-11-04)官方文档与 Nginx 官方文档,写作时间为 2026-11-09。RC 版本行为可能变化,升级前请用 dsh web --help 复核参数。

参考资料