[前言]
本文写的是我近期把 DeepSeek Harness(以下简称 DSH,命令行是 dsh)部署到云服务器 Docker 容器的一段经历。
本地体验其实很「无脑」:装好 Node.js 后执行一条命令,浏览器打开 http://127.0.0.1:3080 就能用。可一旦把 DSH 装进容器、放到云服务器上,事情就全变了——你会发现网页只能自己访问、设置页打不开、请求被 /api 拦掉、连个登录都没有。花了很多时间把各种 BUG 一个个解决之后,我想把这段经历整理出来:一方面方便自己后期回顾(升级时那些坑大概率还会再踩),另一方面帮助有同样需求的人少走弯路。文章末尾的附录是我在云服务器上的实盘记录,供对照。
本文按部署顺序展开:先讲清楚「为什么一行命令在云端失灵」(看懂这一节,后面遇到问题就不会懵),再给出一套单容器 + 登录网关 + Nginx 反代的完整方案(所有文件均可直接复制),最后是常见坑速查与实盘记录。如果你是第一次部署,从上往下跟着做即可;文中示例全部使用占位符(dsh.example.com、sk-xxx),替换成你自己的值。
阅读前你需要具备(或先补一下):
- 一台云服务器(本文以 Ubuntu 24.04 为例,2C4G 起步即可,DSH 本身不吃配置,跑模型靠 API)
- 已安装 Docker(下文给出命令)
- 一个域名(强烈建议;没有域名只能走 SSH 隧道方案,见第八节坑 8)
- 一个 DeepSeek API Key(或其他模型提供方的 Key)
注意:DSH 目前处于 developer preview,rc 版本迭代很快,文中的补丁基于 0.1.1-rc.x 版本实测。你安装的版本若已更新,症状与解法思路通用,但具体的文件路径、代码串可能会变——文中补丁脚本在「上游改串」时会刻意失败退出,这是设计行为,请同步更新脚本而不是绕过它。
一、先看明白:本地「一行命令」,为什么到云端就不灵了
官方 README 的快速开始是这样的:
# 方法一:不安装直接用
npx @deepseek-ai/dsh web
# 方法二:全局安装后用
npm install -g @deepseek-ai/dsh
dsh web
(需要 Node.js 22 或 24 主线版本。)
然后浏览器打开 http://127.0.0.1:3080。到云端/容器里,下面三条内建约束会把这条路堵住。理解它们,等于理解后面所有配置在干什么。
1. DSH 默认只听 127.0.0.1(回环地址)
本地使用时一切正常,因为浏览器和 dsh 在同一台机器上。但出于安全考虑(防止把 Agent 这类能执行命令的服务直接暴露),CLI 甚至刻意禁用了 --host 0.0.0.0——你不让命令行改,只能走配置文件层面去覆盖。
在容器里这就麻烦了:即使做了 docker run -p 3080:3080 端口映射,流量到达了容器网卡,dsh 却只监听容器内的回环地址,连接照样被拒。必须通过 config patch 把 webserver 的监听地址改为 0.0.0.0,见第四节 web-patch.yml。
2. /api 有一道「信任栅栏」
浏览器页面里不少特权调用(读设置、读写凭据、浏览目录等)只放行来自两类来源的请求:
- 回环地址(localhost / 127.0.0.1)
- 配置在
connection.trustedHosts里的主机(authority)
本地访问天然是回环,没问题;走公网域名访问时来源是域名,不满足条件,请求就会被栅栏挡下。解法是把你的访问域名/地址写进 trustedHosts。注意:条目必须是裸 authority(dsh.example.com 或 dsh.example.com:443),不能带 http:// 前缀。
3. 浏览器需要「安全上下文」
DSH 的前端要调用 crypto.randomUUID,而浏览器只允许在安全上下文里使用它——HTTPS,或 localhost。也就是说,用裸 IP + HTTP 访问时,控制台会直接报 crypto.randomUUID is not a function,页面功能大面积失灵。这解释了为什么 HTTPS 不是「加分项」而是「必需品」(或者用 SSH 隧道把流量引到本地 127.0.0.1,见坑 8)。
另外还有两件「不是技术约束、但必须想清楚」的事:
4. DSH 的网页本身没有登录。 谁拿到地址谁就能直接使用你的 Agent——而 Agent 能读工作区文件、执行命令、访问你配置好的凭据。公网裸奔等于把家门钥匙挂在门口。所以必须加一层认证网关(本方案使用 dsh-passwords)。
5. 容器是「一次性」资源。 容器会被删除、重建、升级,但配置(/data:数据库、会话、密钥)和工作区文件(/root/workspace)必须放在数据卷/挂载里,与容器的生命周期解耦。这一点最容易在升级时踩坑,见第九节。
二、总体架构:把上面每一环补上
本教程主线的架构如下(dsh 与网关在同一容器里,反代在宿主机上):
浏览器(https://dsh.example.com)
│
▼
① Nginx / OpenResty / Caddy(宿主机,443 终结 TLS)
│ proxy_pass http://127.0.0.1:8080(放宽长连接超时,见第六节)
▼
② dsh-passwords 网关(容器内 8080)
—— 登录 / 会话 / 子用户权限
—— 转发时把 Host/Origin 改写成 127.0.0.1:3080
▼
③ dsh web(容器内 3080)
—— 监听 0.0.0.0(patch 放开),但只对宿主机回环发布端口
每一层的职责,正好对应第一节的每一环:
- ① 解决「约束 3」:终结 TLS,让浏览器处于安全上下文;它只负责把流量导给网关,不参与业务。同时它要放行长连接(SSE/WebSocket),否则 60 秒就会被掐断(坑 6)。
- ② 解决「第 4 件事」:没有它,公网任何访客都能直达 dsh。dsh-passwords 是 dsh 的一个插件,注册后你启动 dsh 时它会把网关自动拉起来,不需要额外守护进程。
- ③ 解决「约束 1、2」:dsh 的端口只发布到宿主机的回环地址(
127.0.0.1:3080),公网根本摸不到 3080,只能从网关进来;容器内监听用 patch 放开到0.0.0.0,只是为了端口映射和网关能到达它,不是对外暴露。
如果你不想自建镜像,还有两种更省事的替代拓扑(可跳过本文其余部分):
- 纯内网 / 临时体验:SSH 隧道直连 dsh 的 3080,浏览器开
http://127.0.0.1:3080。没有认证层,只适合你自己用(见坑 8)。 - 官方 all-in-one 镜像:dsh-passwords 上游提供了打包好的镜像,配 Caddy/Nginx 就能跑,几分钟起步。缺点是对版本、补丁、额外插件(如插件市场、侧栏)的控制弱一些。详见其 README 的 Docker 安装段。
下面回到本文主线。完整文件在正文各小节里逐个给出,先把它们放进同一个目录:
mkdir -p ~/dsh-deploy && cd ~/dsh-deploy
三、服务器准备
1. 安装 Docker(Ubuntu/Debian)
# 国内网络慢时可用阿里云镜像安装脚本:https://developer.aliyun.com/mirror/docker-ce
curl -fsSL https://get.docker.com | sh
sudo systemctl enable --now docker
docker --version
想免 sudo 执行 docker:
sudo usermod -aG docker $USER后重新登录。
2. 域名解析与放行端口
- 在 DNS 控制台把
dsh.example.com用 A 记录解析到服务器的公网 IP; - 在云厂商安全组/系统防火墙放行 80 与 443(反代与证书验证用)。
3. 准备 API Key 与目录骨架
# DEEPSEEK_API_KEY 等会作为环境变量注入容器
mkdir -p ~/dsh-deploy && cd ~/dsh-deploy
mkdir -p ~/dsh-workspace # 挂载进容器作为 Agent 的工作区
四、构建镜像:五个文件 + docker build
「不能一行命令」的根源就在这一步:镜像要在构建期把 dsh、网关、补丁全部固定下来。一共五个文件(.npmrc、patch-remote-settings.sh、web-patch.yml、Dockerfile、entrypoint.sh),逐个来看。
4.1 .npmrc:npm 镜像源
registry=https://registry.npmmirror.com
@deepseek-ai:registry=https://registry.npmjs.org
默认源是 npmmirror(国内下载快);第二行把
@deepseek-ai这个 scope 指回官方源,是因为它的 rc 版本迭代快、镜像同步经常滞后。服务器在国外的话,把两行都改成registry=https://registry.npmjs.org即可。国内装 dsh 时遇到「rc 版本 404 / 装到旧版」,基本都是镜像源同步滞后导致,见坑 8。
4.2 patch-remote-settings.sh:公网下设置页补丁
先看它的用途(为什么要打这个补丁,见坑 4 的完整解释):
- DSH 客户端会根据
window.location.hostname判断自己是否在回环环境(connection.isLoopback)。 - 公网域名下,设置相关的镜像会走「memory 模式」被短路,页面直接报 “settings are unavailable in this browser”,连请求都不发——即使服务端的信任栅栏已经通过。
- 该脚本把 dsh 客户端包里两处三目表达式改成恒为 host 模式,使公网域名下也能正常使用设置页配置模型。
脚本幂等(已补过会跳过),且上游代码改串时会失败退出(防止静默打偏)。内容较长,全文放在文末 [附 A],这里先按如下方式使用:
# 把 [附 A] 的 patch-remote-settings.sh 内容保存为 ~/dsh-deploy/patch-remote-settings.sh
4.3 web-patch.yml:webserver 绑定 + trustedHosts
# dsh web 启动时用 --patch 应用本文件(见 4.5 / 5.1)
# 规则一:webserver 从默认 127.0.0.1 改绑 0.0.0.0。
# CLI 的 --host 0.0.0.0 被刻意禁用,所以只能通过 patch 覆盖该行 config,
# 让 Docker 端口映射能把流量送进容器(dsh 默认只听容器内回环)。
# 规则二:把 /api 信任栅栏放行你的公网访问域名/地址。
- id: webserver
config:
host: '0.0.0.0'
port: 3080
- id: connection
config:
# 裸 authority(host 或 host:port),不要带 http:// 前缀。
# 把你的公网域名填进去;若还想让某台内网机器/某个 IP 直连 3080 访问,
# 再加一条形如 '192.168.1.5:3080' 的条目(无此需求则只留域名即可)
trustedHosts: ['dsh.example.com']
把 dsh.example.com 换成你自己的域名。
4.4 Dockerfile
# DSH + dsh-passwords 登录网关:单容器镜像
# 构建目录里应放:Dockerfile / .npmrc / web-patch.yml / entrypoint.sh / patch-remote-settings.sh
FROM node:22-slim
# 版本钉住便于复现:--build-arg DSH_VERSION=0.1.1-rc.2;留空则取 npm 最新
ARG DSH_VERSION=
# 基础运行时:DSH 生态里的构建与子进程会用到 bash/git/python
# (国内服务器可先替换 apt 源为清华等镜像再执行 install,速度更快)
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash git ca-certificates python3 python3-pip build-essential \
&& rm -rf /var/lib/apt/lists/*
# npm 镜像源(与本 Dockerfile 同目录的 .npmrc)
COPY .npmrc /root/.npmrc
# pnpm:DSH 的插件 / profile 管理需要
RUN npm install -g pnpm
# DSH 本体(@deepseek-ai scope 已在 .npmrc 指回官方源)
RUN npm install -g @deepseek-ai/dsh${DSH_VERSION:+@${DSH_VERSION}}
# 客户端补丁:公网域名下强制设置镜像为 host 模式(即本目录的 patch-remote-settings.sh)。
# 必须在构建期执行——它要改的是「已安装的 dsh 客户端包」里的文件。
COPY patch-remote-settings.sh /opt/patch-remote-settings.sh
RUN bash /opt/patch-remote-settings.sh
# dsh-passwords 登录网关:npm 全局安装(显式走官方源,避开镜像同步滞后)
RUN npm install -g --registry=https://registry.npmjs.org --no-audit --no-fund dsh-passwords
# 部署补丁:webserver 绑 0.0.0.0 + /api trustedHosts(本目录的 web-patch.yml)
COPY web-patch.yml /opt/dsh/web-patch.yml
# 入口脚本:首次启动生成 .env / SETUP_KEY、注册网关插件、启动 dsh(本目录的 entrypoint.sh)
COPY entrypoint.sh /usr/local/bin/dsh-entrypoint
RUN chmod +x /usr/local/bin/dsh-entrypoint
ENV DSH_HOME=/data \
DSH_TELEMETRY_DISABLED=1 \
LANG=C.UTF-8
WORKDIR /root/workspace
# 3080 = dsh 网页服务;8080 = dsh-passwords 网关
EXPOSE 3080 8080
ENTRYPOINT ["/usr/local/bin/dsh-entrypoint"]
4.5 entrypoint.sh:首次启动的自举逻辑
容器每次启动都会跑它:生成网关的 .env(含一次性 SETUP_KEY)、把 .env 软链到 dsh-passwords 插件目录(网关进程只认插件目录下的 .env)、把网关插件注册进 DSH 的 web profile,最后启动 dsh(网关会被插件自动拉起)。
#!/usr/bin/env bash
set -euo pipefail
# ── 1) 生成 / 更新 /data/dsh-passwords/.env(数据卷,重建容器不丢)──
ENV_DIR="/data/dsh-passwords"
ENV_FILE="${ENV_DIR}/.env"
mkdir -p "${ENV_DIR}"
if [ ! -f "${ENV_FILE}" ]; then
# 首次启动:SETUP_KEY 优先取环境变量(可固定),否则随机生成并打印一次
if [ -n "${DSH_PASSWORDS_SETUP_KEY:-}" ]; then
SETUP_KEY="${DSH_PASSWORDS_SETUP_KEY}"
else
SETUP_KEY="$(node -e 'console.log(require("crypto").randomBytes(24).toString("hex"))')"
fi
DB_ENC_KEY="$(node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))')"
cat > "${ENV_FILE}" <
line.startsWith("SETUP_KEY=") ? `SETUP_KEY=${key}` : line
);
fs.writeFileSync(file, lines.join("\n"));
' "${ENV_FILE}"
fi
fi
# ── 2) 网关进程硬编码读插件目录下的 .env,软链到持久卷 ──
DSH_PASSWORDS_DIR="$(npm root -g)/dsh-passwords"
ln -sfn "${ENV_FILE}" "${DSH_PASSWORDS_DIR}/.env"
# ── 3) 注册 dsh-passwords 插件(幂等;注册后其 cordis.patch.yml 会作为
# bundle 层自动生效,例如把目录选择器切成「应用内浏览对话框」)──
node "${DSH_PASSWORDS_DIR}/scripts/register-plugin.mjs"
# ── 4) 启动 dsh(密码门网关会被插件自动拉起)──
exec dsh web --patch /opt/dsh/web-patch.yml
说明:
MCP_GATEWAY_AUTO_TLS=0表示 TLS 交给前面的 Nginx/OpenResty 终结,网关只做 HTTP;MCP_GATEWAY_UPSTREAM指向同容器内的 dsh(3080)。若你想让网关自己签发 HTTPS 证书(适合没有反代的场景),把MCP_GATEWAY_AUTO_TLS改回默认开、端口改 443,并给 ACME 验证留出 80 端口,参见 dsh-passwords 的 README。
4.6 执行构建
cd ~/dsh-deploy
# 想复现某个版本就 --build-arg DSH_VERSION=0.1.1-rc.2,否则留空取最新
docker build -t dsh-cloud --build-arg DSH_VERSION=0.1.1-rc.2 .
构建过程会下载 dsh / dsh-passwords / pnpm 及系统包,耗时几分钟属正常。构建成功后再回看 4.4 里各 RUN 步骤确认没有报错(补丁脚本失败退出时构建会直接红掉,这是好事——说明上游代码变了,见第九节)。
五、首次运行:网关密钥与登录
5.1 启动容器
cd ~/dsh-deploy
# 生成一个固定的首次配置密钥(推荐,之后重建容器不用重新登录)
SETUP_KEY="$(openssl rand -hex 24)"
docker run -d \
--name dsh-cloud \
--restart unless-stopped \
-p 127.0.0.1:3080:3080 \
-p 127.0.0.1:8080:8080 \
-v dsh-data:/data \
-v "$HOME/dsh-workspace:/root/workspace" \
-e "DEEPSEEK_API_KEY=sk-替换成你的Key" \
-e "DSH_PASSWORDS_SETUP_KEY=$SETUP_KEY" \
dsh-cloud
不想固定密钥的话,把
-e "DSH_PASSWORDS_SETUP_KEY=..."那一行删掉即可——容器会在首次启动日志里生成并打印一次随机 SETUP_KEY(见 5.2)。
逐个拆解:
| 参数 | 含义 |
|---|---|
-p 127.0.0.1:3080:3080 | dsh 网页端口只发布到宿主机回环,公网不可达(安全底线) |
-p 127.0.0.1:8080:8080 | 网关也只发布到回环,由同机的 Nginx 转发进来 |
-v dsh-data:/data | 数据卷:网关 .env、SQLite 数据库、DSH profile、会话都在这里,重建容器不丢 |
-v ~/dsh-workspace:/root/workspace | 工作区挂载:Agent 的文件操作都发生在这个目录,宿主直接可见 |
DEEPSEEK_API_KEY | 模型 API Key(也可稍后在设置页配置,见 5.4) |
DSH_PASSWORDS_SETUP_KEY | 首次配置密钥;不传则容器随机生成并在首次启动日志里只打印一次 |
若你的 Nginx 跑在别的机器/别的容器里(不在宿主机上),把 8080 的映射改成
-p 8080:8080并只对该机器放行端口,其余不变。
5.2 拿到 SETUP_KEY
- 注入了固定值:自己保管即可。
- 没注入:看首次启动日志(只打印这一次,之后存在
/data/dsh-passwords/.env):
docker logs dsh-cloud | head -30
5.3 首次访问:创建主用户
浏览器打开 https://dsh.example.com(先把第六节的反代配好;嫌麻烦也可先用坑 8 的 SSH 隧道临时访问 3080)。首次访问会进入配置页:
- 输入上面的 SETUP_KEY;
- 创建主用户(密码至少 12 位,需含大写、小写、数字、符号)。
创建成功后,这个地址的所有访问都会先过登录页,配置成功也意味着 SETUP_KEY 使命结束。
5.4 配置模型与工作区
- 模型 / API Key:登录后在「设置」里添加你的 Provider(DeepSeek 等)并填入 Key。如果在公网域名下设置页报 “settings are unavailable in this browser”,说明 4.2 的补丁没生效或版本已变,先看坑 4。
- 工作区目录:新建会话选择工作目录时,用「浏览」对话框(可直接输入服务器绝对路径)选
/root/workspace或其子目录——它就是你在宿主机挂载进去的目录。
5.5 日常命令
docker logs -f dsh-cloud # 看日志(升级/排错第一步)
docker restart dsh-cloud # 重启
docker ps # 确认两个端口都在监听
六、接入 HTTPS 反代(Nginx 示例)
6.1 先拿到证书
- 服务器能访问外网时,certbot 一键签发:
sudo apt install certbot python3-certbot-nginx && sudo certbot --nginx -d dsh.example.com; - 用宝塔/1Panel 等面板的,直接在面板里申请站点证书(我的实盘用的就是 1Panel,见附录 B);
- 域名在国内云厂商的,也可以直接在厂商控制台申请免费证书,下载后填进 Nginx。
6.2 Nginx 站点配置
# /etc/nginx/sites-available/dsh.example.com
server {
listen 80;
server_name dsh.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name dsh.example.com;
ssl_certificate /etc/letsencrypt/live/dsh.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dsh.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 关键:默认 60s 会掐断长连接,导致事件流/WebSocket 反复断线(见坑 6)
proxy_read_timeout 24h;
proxy_send_timeout 24h;
proxy_buffering off;
}
}
要点说明:
proxy_pass指向网关 8080,而不是 dsh 的 3080;- 长连接超时必须放宽。DSH 的界面大量依赖两条长连接:插件清单的 SSE(
/plugins/events)和事件通道的 WebSocket(/api/events.*)。默认proxy_read_timeout 60s会在 60 秒时把它们掐断,浏览器端表现为net::ERR_HTTP2_PROTOCOL_ERROR、页面反复重连; proxy_buffering off保证 SSE 事件不被缓冲延迟。
启用并验证:
sudo ln -s /etc/nginx/sites-available/dsh.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
curl -sI https://dsh.example.com/gateway/healthz # 应返回 200
用 Caddy 的话两行搞定(证书自动申请):
dsh.example.com { reverse_proxy 127.0.0.1:8080 },见 [附 A]。
七、验收清单
按顺序过一遍,全绿即部署成功:
docker logs dsh-cloud能看到 dsh 启动日志与密码门启动消息(无报错);curl http://127.0.0.1:8080/gateway/healthz返回 200(就绪检查/gateway/readyz会连同数据库一起查);- 公网
https://dsh.example.com能打开登录页,输入账号密码能登录; - 登录后能正常创建会话、让 Agent 干活(说明模型 Key 与工作区都通了);
- 在云服务器本机执行
curl http://127.0.0.1:3080能通,但从公网访问http://服务器IP:3080应超时/拒绝(确认 3080 没有暴露到公网); - 挂着页面 5 分钟以上不再断线重连(说明长连接超时配置生效);
- (可选)设置页不再报 settings unavailable,能增删改模型 Provider。
八、常见坑速查
现象 → 原因 → 解法,都是我实际踩过或排查过的。升级/排错时先翻这一节。
坑 1:容器起来了,但网页打不开 / 连接被拒
原因:dsh 默认只听容器内 127.0.0.1,没做 4.3 的 webserver patch 时,端口映射流量进不了 dsh。
解法:确认 web-patch.yml 存在且被 --patch 应用(看容器启动日志),重建容器。验证:镜像里没装 curl,用 Node 自带的 fetch 探测容器内端口:
docker exec dsh-cloud node -e 'fetch("http://127.0.0.1:3080").then(r=>console.log("HTTP",r.status)).catch(e=>console.error(e.message))'
应输出 HTTP 200。
坑 2:页面能开,但 /api 请求被拦 / 功能一直转圈
原因:/api 信任栅栏没放行你的域名。
解法:把访问域名加进 web-patch.yml 的 trustedHosts(裸 authority,不要带 http://),重建容器。
坑 3:浏览器控制台报 crypto.randomUUID is not a function
原因:非安全上下文——你在用 http://IP 之类的方式访问。
解法:改用 https://域名;或走 SSH 隧道用 http://127.0.0.1 访问(坑 8)。
坑 4:设置页报 “settings are unavailable in this browser”
原因:客户端有一道比服务端更早的「门」——connection.isLoopback 由浏览器的 location.hostname 决定,公网域名下设置镜像走 memory 模式被短路,load()/ensure() 直接返回,连请求都不发。网关改写 Host 只能过服务端那道栅栏,过不了这一道。
解法:构建期打 4.2 的客户端补丁(强制 host 模式);或临时用 SSH 隧道从 127.0.0.1 打开(回环会被判定为 loopback)。安全代价见附录 B.6。
坑 5:登录页消失 / /api/dsh-passwords/* 全 404 / 日志里根本没有密码门启动消息
原因(如果你装了 dsh-market 插件市场):插件市场的状态文件 /data/profiles/web/.dsh-market/state.json 里 disabled 数组会在每次启动时被「replay」成 live-disable——一旦数组里出现 dsh-passwords,启动时它被直接禁用,网关不拉起,日志还毫无报错。在插件市场 UI 里点过「关闭插件」或切换主题都可能写进这个文件。
解法:先备份,再把 disabled 清空({"disabled":[],"groups":{},...}),重启容器。
提醒:别在插件市场 UI 里关闭 dsh-passwords。
坑 6:页面每 60 秒断线 / 反复重连 / ERR_HTTP2_PROTOCOL_ERROR
原因:反代默认 proxy_read_timeout 60s 掐断了 SSE(插件清单)与 WebSocket(事件通道),浏览器端表现为反复 101/重连。
解法:反代加 proxy_read_timeout 24h; proxy_buffering off;(第六节)。1Panel/OpenResty 用户注意:改的是网站反代规则生成的 root.conf,之后别在面板里重新生成反代规则,否则会被覆盖,见附录 B.4。
坑 7:侧栏终端(dsh-better-sidebar)连不上,浏览器报 1006
原因:网关的 WebSocket 升级有白名单(只放行 /api/events.mux、/api/events.host、/plugins/events、/aionui-panel/events* 等内置路径),白名单外的升级一律回 404 并断开;/sidebar/ws/terminal 等不在其中。
解法(二选一):新版网关用环境变量 MCP_GATEWAY_WS_USER_ALLOWLIST / MCP_GATEWAY_WS_ADMIN_ALLOWLIST 放行;我部署的版本还没有这些变量时的实测做法是对 dist/gateway.js 打补丁追加白名单路径(幂等脚本,见附录 B.3)。
排查法:ws://127.0.0.1:3080/sidebar/ws/... 直连能 OPEN、经网关 ws://127.0.0.1:8080/... 失败 → 就是白名单问题。
坑 8:npm 装 dsh 很慢 / rc 版本 404 或装到旧版
原因:npm 镜像源同步滞后(rc 版本迭代太快)。
解法:@deepseek-ai scope 指回官方源(4.1 的 .npmrc 已处理),改完重新构建。装 dsh-passwords 时也显式带 --registry=https://registry.npmjs.org(Dockerfile 里已带)。
坑 9:重建容器后要全部重新登录
原因:SETUP_KEY 变了(会话 JWT 的签名随之变化),属预期行为。
解法:重建时固定传 DSH_PASSWORDS_SETUP_KEY(与首次相同),旧会话不受影响。
坑 10:「我的数据怎么没了?」
原因:把 /data 或工作区放进了容器的可写层,而不是数据卷/挂载——容器一删全没。
解法:确认 docker run 带上了 -v dsh-data:/data 与工作区挂载;删容器前用 docker inspect dsh-cloud 核对 Mounts。
(选)无域名时的 SSH 隧道方案
# 在你自己的电脑上执行(服务器上不需要任何反代):
ssh -N -L 3080:127.0.0.1:3080 user@服务器IP
# 浏览器打开 http://127.0.0.1:3080 即可使用
注意:直连 dsh、无认证层,只适合你自己用;127.0.0.1 是浏览器认可的安全上下文,crypto.randomUUID 正常。
九、日常更新与维护
DSH 的 rc 迭代很快,隔一段时间就值得升一次。核心原则:数据在卷里,版本在镜像里。
cd ~/dsh-deploy
# 1) 重新构建镜像(可加 --build-arg DSH_VERSION=新版本 钉住某个版本)
docker build -t dsh-cloud .
# 2) 删旧容器、起新容器(数据卷 dsh-data 与工作区挂载不动,配置、数据库、会话都在卷里)
docker rm -f dsh-cloud
# 3) 重新执行 5.1 的 docker run 命令即可
升级时的三个注意点:
- 补丁脚本可能失败。
patch-remote-settings.sh等脚本是幂等的,但上游代码改串时它会刻意失败退出(构建直接红)。这是保护机制:说明补丁对象变了,需要同步更新脚本内容再构建,而不是用|| true之类绕过。 - SETUP_KEY 要保持稳定。重建容器时继续传同一个
DSH_PASSWORDS_SETUP_KEY,或干脆用 entrypoint 里持久化的.env(首次启动后 SETUP_KEY 已固化在/data/dsh-passwords/.env,不再随机)。只有当你主动换密钥时,所有人才需要重新登录。 - 备份要成对。网关的
.env与数据库platform.db在/data/dsh-passwords/下,MCP_DB_ENC_KEY(在.env里)是数据库字段的加密密钥——两者必须一起备份,只拷数据库没拷.env等于拷了个解不开的加密文件。
[附 A] 补丁脚本与 Caddy 备用配置
A.1 patch-remote-settings.sh(正文 4.2 的全文)
把下面内容保存为 ~/dsh-deploy/patch-remote-settings.sh:
#!/usr/bin/env bash
# 客户端补丁:设置镜像强制 host 模式(公网域名可配设置面板)。
# 原理:DSH 客户端按 connection.isLoopback(= 浏览器 location.hostname)选择
# host/memory 两种设置镜像;公网域名下走 memory 模式被短路,设置页报
# “settings are unavailable in this browser”。这里把两处三目改成恒为 host:
# - dsh-client-ui-settings/lib/client.js 设置镜像恒 "host"
# - dsh-client-ui-settings-general/lib/client.js 设置文档控制器恒启用
# 幂等:已补过则跳过;旧串/新串都不在则失败退出(上游改串时需同步本脚本)。
set -euo pipefail
base="$(npm root -g)/@deepseek-ai/dsh/node_modules/@deepseek-ai"
patch_file() {
local file="$1" from="$2" to="$3"
local p="$base/$file"
if [ ! -f "$p" ]; then
echo "NOT FOUND: $p"
return 1
fi
local count
count="$(node -e "process.stdout.write(String(require('fs').readFileSync(process.argv[1],'utf8').split(process.argv[2]).length-1))" "$p" "$from")"
if [ "$count" -eq 0 ]; then
if grep -qF "$to" "$p"; then
echo "SKIP (already patched): $file"
return 0
fi
echo "DRIFT: neither old nor new string found in $file"
return 1
fi
if [ "$count" -ne 1 ]; then
echo "AMBIGUOUS (${count}x): $file :: $from"
return 1
fi
node -e '
const fs=require("fs");const [p,from,to]=process.argv.slice(1);
const s=fs.readFileSync(p,"utf8");
if(!s.includes(from)){console.error("MISS: "+p);process.exit(1)}
fs.writeFileSync(p,s.split(from).join(to));console.log("PATCHED: "+p)
' "$p" "$from" "$to"
}
patch_file dsh-client-ui-settings/lib/client.js \
'new SettingsDescribeMirror(connection.api, connection.isLoopback ? "host" : "memory")' \
'new SettingsDescribeMirror(connection.api, "host")'
patch_file dsh-client-ui-settings-general/lib/client.js \
'connection.isLoopback ? new SettingsDocumentStore(connection.api, ctx.settingsScope.describe()) : void 0' \
'new SettingsDocumentStore(connection.api, ctx.settingsScope.describe())'
echo "remote-settings patch: ok"
A.2 Caddy(可选,两行替代整个第六节)
# Caddyfile —— 证书自动申请与续期,无需手动管理
dsh.example.com {
reverse_proxy 127.0.0.1:8080
}
# 安装 caddy 后,在 Caddyfile 所在目录执行:
caddy run
Caddy 默认对反向代理的读超时已经是宽松的(不设 60s 上限),事件流/WebSocket 不会像 Nginx 默认配置那样被掐断;如遇问题再显式加
flush_interval -1。
[附 B] 我的实盘记录:1Panel + OpenResty
我的服务器上用 1Panel 的 OpenResty 做反代,正文方案的原型就是这套,记录几个实盘细节供对照。
B.1 拓扑与版本
浏览器 → OpenResty(1Panel,443,已有证书)→ dsh-passwords 网关(8080)→ dsh(127.0.0.1:3080)
- 镜像
deepseek-harness:0.1,容器名DeepSeek-Harness; - dsh 版本钉在
0.1.1-rc.2(DSH_VERSIONbuild-arg);dsh-passwords 跟随 npm latest(构建时自动拉官方源最新版); - 数据卷
dsh-data:/data;Agent 工作区挂载到容器内/root/workspace(root 用户 home 的子目录,目录选择器默认停在/root,可直接进入选目录); - 端口:3080 只发布到
127.0.0.1;网关 8080 我发布到了 0.0.0.0(前面有登录层),建议按正文绑回环更稳妥。
B.2 重建脚本(rebuild.sh)
实盘里构建上下文是仓库根目录(Dockerfile 会 COPY 插件源码目录),每次构建前先把插件源码同步到官方最新。相比正文 4.4 的最小 Dockerfile,实盘镜像多了三样东西:插件市场(COPY dsh-plugin/dsh-market 到 /opt/dsh-market 并 npm 构建)、dsh-better-sidebar 终端的 WS 白名单补丁(构建期执行)、以及 entrypoint 里追加的 dsh plugin --profile web add link:/opt/dsh-market。重建脚本如下(密钥走环境变量,勿硬编码):
#!/usr/bin/env bash
# 一键重建镜像并重启容器(密钥一律走环境变量,勿硬编码)
set -euo pipefail
cd "$(dirname "$0")"
IMAGE="deepseek-harness:0.1"
CONTAINER="DeepSeek-Harness"
PORT="3080"
GATEWAY_PORT="8080"
API_KEY="${DEEPSEEK_API_KEY:?请先 export DEEPSEEK_API_KEY}"
SETUP_KEY="${DSH_PASSWORDS_SETUP_KEY:-}" # 固定则重建后不用重新登录
DSH_VERSION="${DSH_VERSION:-0.1.1-rc.2}" # 实盘钉住的版本
WORKSPACE="${WORKSPACE:-$HOME/dsh-workspace}"
# 插件源码以官方为准:每次构建前 fetch + reset,本地有改动/残留一律丢弃
# (实盘对 dsh-plugin/dsh-passwords 与 dsh-plugin/dsh-market 都执行)
update_plugin() {
local dir="$1"
echo ">> 更新插件源码(官方为准):${dir}"
git -C "${dir}" fetch origin main
git -C "${dir}" reset --hard origin/main
git -C "${dir}" clean -fd
}
update_plugin ../dsh-plugin/dsh-passwords
docker build --no-cache -f Dockerfile -t "${IMAGE}" \
--build-arg DSH_VERSION="${DSH_VERSION}" \
--build-arg DSH_PASSWORDS_VERSION="${DSH_PASSWORDS_VERSION:-}" \
..
docker rm -f "${CONTAINER}" >/dev/null 2>&1 || true
docker run -d \
--name "${CONTAINER}" \
--restart on-failure:5 \
-p "127.0.0.1:${PORT}:3080" \
-p "127.0.0.1:${GATEWAY_PORT}:8080" \
-v dsh-data:/data \
-v "${WORKSPACE}:/root/workspace" \
-e "DEEPSEEK_API_KEY=${API_KEY}" \
${SETUP_KEY:+-e "DSH_PASSWORDS_SETUP_KEY=${SETUP_KEY}"} \
"${IMAGE}"
update_plugin会把插件源码目录git reset --hard到官方 main——这是「以官方为准」的约定:有本地改动就别放在这两个目录里,会直接被冲掉。
B.3 两个补丁的出处与维护约定
patch-remote-settings.sh(正文 [附 A]):解决公网域名下设置页不可用。DSH 某个版本起SettingsDescribeMirror按isLoopback选择 host/memory,公网域名被 memory 短路;脚本把两处三目改成恒 host。patch-remote-sidebar-ws.sh(正文坑 7):dsh-passwords 网关的 WS 升级白名单默认只含 dsh 内置事件路径,我装的 dsh-better-sidebar 终端路径(/sidebar/ws/terminal、/sidebar/ws/agent-terminals)不在其中。这个补丁把两个路径追加进dsh-passwords/dist/gateway.js的白名单;如果你的网关版本支持MCP_GATEWAY_WS_USER_ALLOWLIST环境变量,优先用环境变量,不用打补丁。
两个脚本的共同约定:幂等(已补过跳过)+ 上游改串即失败退出(防止静默打偏)。镜像构建时执行;升级后构建若失败,先怀疑「补丁对象变了」,去对应版本源码里核对那行代码再更新脚本。
B.4 1Panel / OpenResty 反代注意点
1Panel 网站的反代规则会生成到 /opt/1panel/www/sites/<站点域名>/proxy/root.conf(容器内 /www/sites/...)。我在 location ^~ / 里加了两行:
location ^~ / {
proxy_pass http://127.0.0.1:8080;
proxy_read_timeout 24h;
proxy_buffering off;
}
坑: 1Panel 网站设置里重新生成/保存反代规则会覆盖 root.conf,把这两行冲掉(症状立刻回到坑 6 的每 60 秒断线)。改完别再动面板里该网站的反代设置,或改完检查一遍 root.conf。
B.5 插件市场(dsh-market)事故备忘
我把 dsh-market 以 link 方式加进了镜像(dsh plugin --profile web add link:/opt/dsh-market),它给 UI 提供了插件开关。它带来的坑就是正文坑 5:市场状态文件 /data/profiles/web/.dsh-market/state.json 的 disabled 数组会被 live-disable。曾因数组里出现 "dsh-passwords" 导致登录页整个消失、日志无任何报错。排查顺序:docker logs 看不到密码门启动消息 → 先看这个文件 → 清空 disabled 数组(备份后编辑)→ 重启。
B.6 安全注记(重要)
打完 4.2 的设置补丁后有一个安全代价:设置与凭据相关方法(读设置、写凭据等)对任何登录用户开放,网关的子用户权限模型不拦截这一类调用。我当前只建了主用户一个账号,所以没问题;以后要建子用户之前,先想清楚这个模型是否满足你的要求。
更稳妥的兜底方案始终存在:SSH 隧道直连 127.0.0.1:3080(回环即安全上下文,且绕过网关的认证层)——适合管理员自己排查「是不是网关/补丁/反代又出问题」的场景。
如果你按本文部署成功,或者卡在某个没列到的坑上,欢迎留言交流。整理这篇文章比部署本身还费时间——希望它能帮你把这几天的弯路缩成半小时。

站主,加一个联系方式板块吧