DeepSeek Harness 上云指南(Docker)

[前言]

本文写的是我近期把 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. 浏览器需要「安全上下文」

本地与云端两种请求通过三道内建约束的成对评估轨迹 两道轨评估同一套三道门:监听绑定、/api 信任栅栏、安全上下文。本地因为恰好处于回环与 localhost 而全部通过;云端第一条规则处首次分歧,需逐一补齐配置才能访问。 本地 · LOCAL 云端 · CLOUD http://127.0.0.1:3080 https://dsh.example.com 1 · 监听绑定(默认仅 127.0.0.1) PASS 本机回环 = 天然满足 NEEDS FIX 首处分歧 patch webserver -> 0.0.0.0 2 · /api 信任栅栏 PASS 回环地址自动放行 NEEDS FIX 域名加进 trustedHosts(裸 host) 3 · 安全上下文 · crypto.randomUUID PASS localhost 被浏览器认可 NEEDS FIX 上 HTTPS,或 SSH 隧道走 127.0.0.1 打开即用 npx @deepseek-ai/dsh web 三处补齐才可用 缺一 -> 第八节 坑 1 / 坑 2 / 坑 3 LEGEND PASS 规则自动满足 NEEDS FIX 需要补齐该项配置 首次分歧点

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,只是为了端口映射和网关能到达它,不是对外暴露。
DeepSeek Harness 云服务器单容器部署结构 公网浏览器经 Nginx/OpenResty 终结 HTTPS,再转发给同位容器的 dsh-passwords 登录网关,网关再转发给同一容器内的 dsh web;dsh 端口只发布到宿主机回环,两个数据卷分别存放配置会话与工作区。 公网 · INTERNET 宿主机 · SELF-HOST 容器 · CONTAINER HTTPS · 443 HTTP · 8080 HTTP · 3080 VOLUME MOUNT 浏览器 USER · HTTPS PROXY Nginx / OpenResty :443 · TLS 终结 AUTH dsh-passwords 网关 :8080 · 登录 / 授权 WEB dsh web :3080 · 仅发布 127.0.0.1 /data 配置 · 会话 · 数据库 /root/workspace Agent 工作区 LEGEND 跨信任边界 容器内 / 回环 挂载 / 数据卷

如果你不想自建镜像,还有两种更省事的替代拓扑(可跳过本文其余部分):

  • 纯内网 / 临时体验: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:3080dsh 网页端口只发布到宿主机回环,公网不可达(安全底线)
-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)。首次访问会进入配置页:

  1. 输入上面的 SETUP_KEY;
  2. 创建主用户(密码至少 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]。


七、验收清单

按顺序过一遍,全绿即部署成功:

  1. docker logs dsh-cloud 能看到 dsh 启动日志与密码门启动消息(无报错);
  2. curl http://127.0.0.1:8080/gateway/healthz 返回 200(就绪检查 /gateway/readyz 会连同数据库一起查);
  3. 公网 https://dsh.example.com 能打开登录页,输入账号密码能登录;
  4. 登录后能正常创建会话、让 Agent 干活(说明模型 Key 与工作区都通了);
  5. 在云服务器本机执行 curl http://127.0.0.1:3080 能通,但从公网访问 http://服务器IP:3080 应超时/拒绝(确认 3080 没有暴露到公网);
  6. 挂着页面 5 分钟以上不再断线重连(说明长连接超时配置生效);
  7. (可选)设置页不再报 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 命令即可

升级时的三个注意点:

  1. 补丁脚本可能失败。patch-remote-settings.sh 等脚本是幂等的,但上游代码改串时它会刻意失败退出(构建直接红)。这是保护机制:说明补丁对象变了,需要同步更新脚本内容再构建,而不是用 || true 之类绕过。
  2. SETUP_KEY 要保持稳定。重建容器时继续传同一个 DSH_PASSWORDS_SETUP_KEY,或干脆用 entrypoint 里持久化的 .env(首次启动后 SETUP_KEY 已固化在 /data/dsh-passwords/.env,不再随机)。只有当你主动换密钥时,所有人才需要重新登录。
  3. 备份要成对。网关的 .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_VERSION build-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(回环即安全上下文,且绕过网关的认证层)——适合管理员自己排查「是不是网关/补丁/反代又出问题」的场景。


如果你按本文部署成功,或者卡在某个没列到的坑上,欢迎留言交流。整理这篇文章比部署本身还费时间——希望它能帮你把这几天的弯路缩成半小时。

评论

  1. 卢钰龙
    Windows Edge
    1 月前
    2026-9-11 9:34:07

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

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇