在云服务器部署 DeepSeek Harness:从安装到 HTTPS 访问

7006 字
35 分钟
在云服务器部署 DeepSeek Harness:从安装到 HTTPS 访问
在云服务器部署 DeepSeek Harness:从安装到 HTTPS 访问

为什么把 DSH 放到云服务器上#

DeepSeek Harness,简称 DSH,可以读取项目文件、执行命令,并辅助完成开发任务。我把它部署在云服务器上,是为了通过浏览器访问固定的工作区:无论换到哪台电脑,都能继续处理同一份项目。

这篇文章记录我在 Debian + 1Panel + OpenResty 环境中的部署过程。完成后,便可以通过 HTTPS 域名访问 DSH,登录、选择工作区和模型,再开始会话。

通过浏览器访问 DSH 并收到模型回复
通过浏览器访问 DSH 并收到模型回复

部署分为几步:安装并启动 DSH,通过终端配置 API Key,设置域名和 HTTPS 反代,完成外层认证与 cookie 自动同步,最后检查 WebSocket 和模型回复。常见问题放在文末,方便遇到故障时按现象查找。

1. 环境与部署架构#

项目本文环境
系统Debian GNU/Linux 12(bookworm)
服务器资源4 vCPU、3.7 GiB 内存、4 GiB swap
面板1Panel 社区版 v2.0.2(stable)
反向代理OpenResty 1.31.1.1,Docker host 网络;镜像 1panel/openresty<1>.31.1.1-2-4-noble
DSH / NodeDSH 0.2.0-rc.2 / Node v22.23.2(/usr/bin/node)
DSH 监听地址127.0.0.1<3080>
示例域名dsh.example.com,请替换为自己的域名
HTTPS 证书通过面板申请并配置

HTTPS,443

HTTP / WebSocket

API Key

浏览器

OpenResty:证书、访问认证、反向代理

DSH:127.0.0.1:3080,systemd 管理

服务器工作区

模型 API 服务

HTTPS,443

HTTP / WebSocket

API Key

浏览器

OpenResty:证书、访问认证、反向代理

DSH:127.0.0.1:3080,systemd 管理

服务器工作区

模型 API 服务

以上是我在 2026-10-01 核对的实际环境。下文使用独立的 dsh 用户和示例目录;我的现有实例则把凭据保存在 /root/.dsh。已有服务应沿用实际配置,迁移前先备份凭据和工作区。

这里调用远程模型 API,云服务器负责运行 DSH 及工作区任务。资源需求取决于任务内容,例如安装依赖、构建项目或转换文件。

本文的回环上游地址依赖 OpenResty 使用 host 网络。 如果容器采用 bridge 网络,容器内的 127.0.0.1 不指向宿主机,需要调整上游地址。

DSH 目前属于开发者预览软件。本文记录个人部署经验,参数和接口以实际安装版本为准。由于 DSH 能执行工作区命令,建议使用专门的运行用户和项目目录。官方项目 · 官方安全说明

2. 安装 Node 与 DSH#

2.1 确认 Node 环境与 DSH 版本#

以下操作在服务器宿主机的 SSH 终端中完成。先查看现有 Node 环境和 DSH 可用版本:

Terminal window
node -v
npm -v
npm view @deepseek-ai/dsh dist-tags

选定 DSH 版本后,查询它要求的 Node 版本:

Terminal window
DSH_VERSION='替换为目标版本号'
npm view "@deepseek-ai/dsh@$DSH_VERSION" engines --json

如果尚未安装 Node,按 Node 官方安装说明 安装满足要求的版本。使用下载镜像时,核对版本、架构和安装位置。

2.2 安装并确认入口路径#

Terminal window
sudo npm install -g "@deepseek-ai/dsh@$DSH_VERSION" \
--no-audit --no-fund
command -v node
command -v dsh
npm root -g
dsh --version
dsh web --help

安装耗时取决于服务器资源和网络。官方 registry 无法访问时,可以用 --registry 指定可用镜像;出现 ETARGET 时,先检查包名、版本号,以及该 registry 是否已收录目标版本。

如果希望在后台安装并持续查看日志,可以使用:

Terminal window
# 将引号内的版本号替换为已经确认存在的版本。
sudo sh -c 'nohup nice -n 10 ionice -c3 npm install -g \
"@deepseek-ai/dsh@替换为目标版本号" --no-audit --no-fund \
> /var/log/dsh-npm-install.log 2>&1 &'
tail -f /var/log/dsh-npm-install.log

按 Ctrl+C 会退出日志跟踪,后台安装仍会继续。安装完成后,用 dsh --version 确认结果,并记录 command -v dsh 返回的入口路径,后续创建服务时会用到。

3. 用 systemd 托管 DSH#

3.1 创建运行用户与工作区#

下面使用专门的 dsh 用户,家目录为 /var/lib/dsh,工作区为 /srv/dsh-workspace。如果已有运行用户,沿用它,并同步调整后续路径。

Terminal window
id dsh >/dev/null 2>&1 || sudo useradd \
--system --user-group --create-home --home-dir /var/lib/dsh dsh
sudo mkdir -p /srv/dsh-workspace /var/lib/dsh/.dsh
sudo chown dsh:dsh /srv/dsh-workspace /var/lib/dsh /var/lib/dsh/.dsh

工作区可以替换为自己的项目目录,运行用户需要具备相应的读写权限。

3.2 创建服务#

先通过 dsh web --help 确认当前版本支持下列参数,再创建服务。这里用 DSH_BIN 填入实际安装入口:

Terminal window
DSH_BIN="$(command -v dsh)"
test -n "$DSH_BIN" || { echo '未找到 dsh,请先完成安装'; exit 1; }
sudo tee /etc/systemd/system/dsh.service > /dev/null <<EOF
[Unit]
Description=DeepSeek Harness Web
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=dsh
Group=dsh
WorkingDirectory=/srv/dsh-workspace
Environment=HOME=/var/lib/dsh
Environment=DSH_HOME=/var/lib/dsh/.dsh
Environment=PATH=/usr/local/bin:/usr/bin:/bin
Environment=NO_COLOR=1
ExecStart=$DSH_BIN web --host 127.0.0.1 --port 3080 --trusted-host dsh.example.com --no-open
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now dsh

将 dsh.example.com 替换为实际域名。如果 Node 通过 nvm 等用户级工具安装,还要核对目录权限和 PATH,因为 systemd 不会自动加载交互式 shell 配置。

我的实例另设了 MemoryHigh=1536M、MemoryMax=2G。这是该机器的服务资源上限,设置时应给系统和反代留出内存,不能当作 DSH 的最低配置要求。

3.3 检查服务状态#

Terminal window
sudo systemctl status dsh --no-pager
sudo ss -lntp | grep ':3080'
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080/

DSH 服务处于 active running 状态
DSH 服务处于 active running 状态

确认状态为 active (running),并且监听地址为 127.0.0.1:3080。本文版本启用了 DSH 自带认证,未登录访问首页返回 401 属于预期行为。至此可以确认服务已启动,模型调用和浏览器连接留到后面验证。

4. 通过终端配置 API Key#

在我的远程访问环境中,打开模型设置页会看到:

加载提供商目录失败: settings are unavailable in this browser

远程访问时模型设置页提示不可用
远程访问时模型设置页提示不可用

因此,我通过 systemd 环境文件配置 API Key。这里记录的是当前实例的实际现象,其他版本和访问环境应以各自测试结果为准。

官方 DeepSeek API Key 插件默认使用 DEEPSEEK_API_KEY 作为凭据引用,启动环境变量的优先级高于 GUI 保存的同名凭据。后续换密钥时,需要更新环境文件并重启服务。官方插件说明 · 官方凭据包说明

4.1 写入环境文件#

将 YOUR_API_KEY 换成实际密钥。下面的命令会覆盖环境文件;公开代码或截图时保留占位值。

Terminal window
sudo mkdir -p /etc/dsh
sudo tee /etc/dsh/env > /dev/null <<'EOF'
DEEPSEEK_API_KEY=YOUR_API_KEY
EOF
sudo chown root:root /etc/dsh/env
sudo chmod 600 /etc/dsh/env

文件由 root 拥有,权限为 600。系统级 systemd 读取后将变量传给服务进程,DSH 的运行用户无需直接读取它。

4.2 通过 drop-in 注入#

Terminal window
sudo mkdir -p /etc/systemd/system/dsh.service.d
sudo tee /etc/systemd/system/dsh.service.d/api-key.conf > /dev/null <<'EOF'
[Service]
EnvironmentFile=-/etc/dsh/env
EOF
sudo systemctl daemon-reload
sudo systemctl restart dsh
sudo systemctl status dsh --no-pager

这里沿用实例中的 EnvironmentFile=-/etc/dsh/env。路径前的 - 表示文件缺失时仍允许服务启动,所以还要检查变量是否真正进入进程。

环境文件修改后需要重启 DSH;daemon-reload 只重新加载服务配置,不会更新已运行进程的环境。

4.3 验证注入结果,不显示密钥#

Terminal window
PID=$(systemctl show -p MainPID --value dsh)
if [ "$PID" -gt 0 ]; then
sudo cat "/proc/$PID/environ" \
| tr '\0' '\n' \
| grep -c '^DEEPSEEK_API_KEY=.'
else
echo 'DSH 未运行,请先检查服务日志'
fi

输出 1,表示进程环境中存在非空的 DEEPSEEK_API_KEY。密钥是否有效、所选模型是否可用,还要通过实际回复验证。

这里用 sudo cat 读取进程环境,再统计匹配数量。若直接使用 sudo tr ... < /proc/$PID/environ,重定向会先由当前 shell 执行,普通用户可能在 sudo 生效前就遇到权限不足。

使用自定义供应商时,还需配置对应的凭据引用、模型和接口地址;DEEPSEEK_API_KEY 只覆盖使用这一凭据名的配置。

5. 配置域名与 HTTPS#

5.1 添加 DNS 记录#

以 dsh.example.com 为例,在 example.com 的 DNS 管理页面添加:

字段示例
主机记录dsh
记录类型A
记录值服务器公网 IPv4 地址

DNSPod 中添加指向服务器公网 IP 的 A 记录
DNSPod 中添加指向服务器公网 IP 的 A 记录

截图中的 xxx.<ip>.com 是原始标注,实际应理解为自己的域名,例如 dsh.example.com。服务器 IP 填入“记录值”,无需拼进域名。首次调试可以先直接解析到服务器,减少 CDN 回源配置带来的影响。

5.2 在 1Panel 创建反向代理网站#

进入“网站 → 网站 → 创建网站”,绑定实际域名,将反代目标设为 http://127.0.0.1:3080。随后在 HTTPS 配置中申请或选择证书,并启用 HTTPS。

1Panel 网站管理和创建网站入口
1Panel 网站管理和创建网站入口

上图展示创建入口,上游、证书和自定义配置在对应站点中设置。云安全组及主机防火墙应放行网站端口,DSH 的 3080 端口继续只监听回环地址。

6. 配置反代:Host 与 WebSocket#

DSH 启动参数中的 --trusted-host 应与访问域名一致。反代原样透传浏览器的 Host,能够保留非标准 HTTPS 端口,避免 Host 与 Origin 不一致。

下面是反代的核心配置。请合并到现有站点的 location / 中,并保留面板生成的证书等设置:

location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $http_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 X-Forwarded-Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
proxy_cache off;
client_max_body_size 300m;
}

Upgrade 和 Connection 用于 WebSocket 升级,读超时设为 3600 秒。Nginx 默认的 WebSocket 空闲超时为 60 秒,指长时间没有上游数据时断开连接;应用心跳也可以维持连接。Nginx WebSocket 文档

300m 是示例附件大小限制,可按需要调整。保存配置时核对面板的 include 规则,避免重复创建 location /,或让同一片段被加载多次。

检查并重载 OpenResty#

先用 docker ps 找出本站使用的 OpenResty 容器,将容器名填入 ORESTY:

Terminal window
sudo docker ps --format '{{.Names}}'
ORESTY='替换为实际 OpenResty 容器名'
sudo docker exec "$ORESTY" /usr/local/openresty/nginx/sbin/nginx -t

配置检查通过后,再执行重载:

Terminal window
sudo docker exec "$ORESTY" /usr/local/openresty/nginx/sbin/nginx -s reload

7. 配置外层访问认证#

本文采用“外层认证 + 服务器注入 DSH cookie”的登录方式。先保护 HTTP 与 WebSocket 入口,再按第 8 节配置自动兑换和同步,浏览器便只需完成外层登录。

7.1 配置 Basic Auth#

为了给公网入口增加用户名和密码验证,我在反代中配置了 Basic Auth。浏览器访问时会显示:

反向代理的 Basic Auth 登录框
反向代理的 Basic Auth 登录框

Basic Auth 位于反代外层,DSH 的 token/cookie 属于内层认证。第 8 节通过 sync.sh 自动提供内层凭据,两层配合完成登录。

下面使用 openssl passwd -5 生成 SHA-256 crypt 密码记录:

Terminal window
DIR=/opt/1panel/www/sites/dsh.example.com
read -rp '用户名: ' AUTH_USER
read -rsp '密码: ' AUTH_PASSWORD; echo
AUTH_HASH=$(printf '%s' "$AUTH_PASSWORD" | openssl passwd -5 -stdin)
printf '%s:%s\n' "$AUTH_USER" "$AUTH_HASH" \
| sudo tee "$DIR/htpasswd" > /dev/null
unset AUTH_PASSWORD AUTH_HASH

密码文件应放在无法通过网站直接下载的位置,并设置为 OpenResty 运行用户可读。接着在 location / 中加入:

auth_basic "DSH";
auth_basic_user_file /www/sites/dsh.example.com/htpasswd;

本文宿主机路径为 /opt/1panel/...,容器内对应路径为 /www/...。请根据自己的挂载关系填写 auth_basic_user_file。

密码工具的参数应按对应工具理解。例如,OpenSSL 的 -5 指 SHA-256 crypt;传统 DES crypt 的 8 位密码限制不适用于 apr1。Apache 密码工具文档

完成第 8 节的自动登录配置后,再检查 WebSocket。如果页面正常、WS 却返回 401,应核对 WS 入口的外层认证。sync.sh 只提供 DSH 内层凭据,独立的 WS location 也需要有效的外层访问控制。

7.2 WebSocket 通行证:一次认证绕过与修复#

我的实例为 /api/remote.mux 配置了精确匹配的 location:普通页面使用 Basic Auth,WS 入口则检查静态 dshgate cookie,通过后再注入 DSH 会话 cookie。因此,dshgate 本身也是访问凭据,持有它就能建立新连接。

检查时发现,发放通行证的 Set-Cookie 指令带有 always。匿名请求虽然收到 401,响应仍发出了 dshgate;仅带这个值的 WS 握手随后得到 101,绕过了 Basic Auth。

修复时,我去掉了这条 Set-Cookie 的 always,保留 HSTS 的 always,并同时轮换通行证的发放值和比较值。下面只列相关配置行,部署时需合并到实际 location:

# location / 内;继续保留 auth_basic 和 auth_basic_user_file
add_header Strict-Transport-Security "max-age=31536000" always;
add_header Set-Cookie "dshgate=NEW_RANDOM_VALUE; Path=/; HttpOnly; Secure; SameSite=Lax";
# location = /api/remote.mux 内;与上面使用同一个新值
if ($cookie_dshgate != "NEW_RANDOM_VALUE") { return 401; }

不带 always 的 add_header 只对文档规定的状态码生效,其中不包含 401。错误页、内部跳转和响应头继承也应结合站点配置检查。Nginx 响应头文档

另外,$remote_user 非空不代表密码验证成功。它读取的是 Basic 请求头中解析出的用户名,错误密码或不存在的用户也可能让它有值,不能据此发放通行证。Nginx 变量源码 · Basic 头解析源码

修复后的验证结果如下:

  • 在实际站点中,匿名、用户不存在和密码错误的请求均返回 401,且不再下发通行证;WS 使用空值、错误值或旧值也都被拒绝。
  • 在同镜像隔离环境中,正确 Basic 凭据得到 200 和新通行证;新通行证通过模拟 mux 的校验,进入内容阶段后返回 404。这个测试验证了门禁逻辑,没有建立真实 DSH WebSocket。
  • 最后,我刷新已登录页面,确认新建立的 remote.mux 返回 101,并能收到模型回复,完成实际站点的合法访问验证。

静态通行证与 Basic Auth 密码相互独立,改密码时也应轮换通行证。会话 cookie 的浏览器属性不代表服务器端有效期;轮换只影响新握手,已有 WS 长连接仍可能由旧 worker 继续持有。本次重载保留了这些连接,没有强制中断会话。

重载是否生效,应结合日志和请求行为判断。本次通过新 worker 启动、旧通行证被拒绝确认了变化。nginx -T 适合检查磁盘配置和 include 链,不能读取运行中 worker 的配置;reload 命令返回 0,也只说明信号已送达。Nginx 命令行文档

这是本文部署流程中的必做步骤。 浏览器通过外层认证后,OpenResty 需要为请求提供有效的 DSH 会话 cookie。sync.sh 负责在服务器上兑换凭据、写入反代片段,并配合定时器提前刷新,让日常访问无需手动输入启动 token。

sync.sh 是本实例自行配置的脚本。第 7 节的外层认证控制谁能访问,本节的 cookie 注入完成 DSH 内层登录,两部分都需要配置。

启动 token 随进程启动变化,用于兑换 cookie;cookie 的签名密钥会持久保存。本地凭据提供方将其存于 $DSH_HOME/.credentials.yaml。

只要签名密钥、authority(规范化域名和端口)及有效期符合要求,既有 cookie 就能跨重启继续使用。 脚本用于首次兑换和周期刷新,重启后同步一次也是我的日常操作习惯。

默认 cookieMaxAgeDays 为 30 天,脚本在 20 天时刷新。调整有效期后,应同步缩短刷新周期。凭据删除或更换后,DSH 在下一次激活时加载新密钥,原有签名随之失效,需要重新兑换。DSH 浏览器认证设计

DSH 启动日志中的 token

sync.sh:携带实际 Host 兑换 cookie

写入 dsh-auth.inc,校验并重载 OpenResty

浏览器通过外层访问认证

OpenResty 向上游请求注入 DSH cookie

DSH 校验签名、authority 和有效期

DSH 启动日志中的 token

sync.sh:携带实际 Host 兑换 cookie

写入 dsh-auth.inc,校验并重载 OpenResty

浏览器通过外层访问认证

OpenResty 向上游请求注入 DSH cookie

DSH 校验签名、authority 和有效期

8.2 准备目录与 include#

以下继续使用 dsh.example.com。将它换成自己的域名,并核对宿主机与容器的挂载路径:

Terminal window
sudo install -d -m 755 /usr/local/lib/dsh-auth
sudo install -d -m 700 /var/lib/dsh-auth
# 站点已在 1Panel 中创建后执行;已有文件时 touch 不会清空其内容。
sudo mkdir -p /opt/1panel/www/sites/dsh.example.com/proxy
sudo touch /opt/1panel/www/sites/dsh.example.com/proxy/dsh-auth.inc

在实际转发请求的 location 中,加载容器内对应的文件:

include /www/sites/dsh.example.com/proxy/dsh-auth.inc;

如果 /api/remote.mux 使用独立 location,也要加载这条 include,并保留 Host 和 WebSocket 转发参数。占位文件必须在配置检查前存在,否则 Nginx 无法读取它。

自动注入会让反代请求携带有效的 DSH 凭据。因此,普通页面应保留 Basic Auth;采用通行证的 WS 入口,应只接受成功认证后发出的有效通行证。

8.3 服务器现用脚本#

下面是服务器现用脚本,将域名和容器名统一改为示例值。使用前,把 dsh.example.com、your-openresty-container 及相关路径换成自己的配置。

该版本从 MainPID 对应日志中取 token,以成功状态文件的修改时间判断是否刷新。配置检查或重载命令失败时,会尝试恢复上一版 include,并返回非零退出码。

服务器上已验证未到刷新周期时的正常跳过;兑换、写入、回滚和失败重试在隔离环境完成测试,本次没有强制改写生产 cookie。

将代码保存为 /usr/local/lib/dsh-auth/sync.sh,以 root 运行:

#!/usr/bin/env bash
# 用当前 dsh 进程的启动 token 换一个浏览器会话 cookie,注入给 OpenResty。
# dsh 重启或 cookie 快到期时自动刷新;平时空转退出。
#
# 2026-10-01 加固:
# 1) token 按 dsh.service 当前 MainPID 过滤(journalctl _PID=),不再依赖日志行数窗口;
# 2) 写文件/找容器/兑换 cookie/nginx -t/reload 任一步失败都非零退出,且不打印成功;
# 3) TOKEN_FILE 只在 nginx -t 与 reload 都成功后才写,失败后下次执行必然重试;
# 4) 刷新判定改用 TOKEN_FILE 的 mtime(= 上次成功时间),不看 include 的 mtime,
# 避免「失败后 include 变新 → 下次被跳过」;
# 5) 替换 include 前保留上一版到 $BACKUP,nginx -t / reload 失败时恢复并报告结果。
set -uo pipefail
# 以下路径均可用环境变量覆盖,仅用于隔离测试/换站点;默认值即本站生产值。
DOMAIN="${DSH_AUTH_DOMAIN:-dsh.example.com}"
DSH_URL="${DSH_AUTH_DSH_URL:-http://127.0.0.1:3080}"
INCLUDE="${DSH_AUTH_INCLUDE:-/opt/1panel/www/sites/dsh.example.com/proxy/dsh-auth.inc}"
BACKUP="${DSH_AUTH_BACKUP:-/var/lib/dsh-auth/dsh-auth.inc.prev}"
TOKEN_FILE="${DSH_AUTH_TOKEN_FILE:-/var/lib/dsh-auth/token}"
SERVICE="${DSH_AUTH_SERVICE:-dsh.service}"
ORESTY="${DSH_AUTH_ORESTY:-your-openresty-container}"
NGINX_BIN=/usr/local/openresty/nginx/sbin/nginx
REFRESH_DAYS=20
fail() { echo "sync.sh: $*" >&2; exit 1; }
# 1) 取「当前 dsh 进程」的启动 token
MAIN_PID="$(systemctl show -p MainPID --value "$SERVICE" 2>/dev/null)"
case "$MAIN_PID" in
'' | 0) fail "$SERVICE 未运行或没有主进程(MainPID=${MAIN_PID:-空})" ;;
*[!0-9]*) fail "无法解析 $SERVICE 的 MainPID:$MAIN_PID" ;;
esac
TOKEN="$(journalctl -u "$SERVICE" _PID="$MAIN_PID" --no-pager 2>/dev/null \
| grep -o 'token=[A-Za-z0-9_-]\+' | tail -1 | cut -d= -f2)"
[ -n "$TOKEN" ] || fail "未在 $SERVICE 当前进程(PID $MAIN_PID)的日志里找到启动 token"
# 2) 是否需要刷新:以「上次成功同步时间」(TOKEN_FILE 的 mtime)为准
NEED=1
if [ -f "$TOKEN_FILE" ] && [ -f "$INCLUDE" ] && [ "$(cat "$TOKEN_FILE")" = "$TOKEN" ]; then
AGE=$(( ( $(date +%s) - $(stat -c %Y "$TOKEN_FILE") ) / 86400 ))
[ "$AGE" -lt "$REFRESH_DAYS" ] && NEED=0
fi
[ "$NEED" = 1 ] || exit 0
# 3) 用启动 token 换浏览器会话 cookie
COOKIE="$(curl -si -H "Host: $DOMAIN" "$DSH_URL/?token=$TOKEN" \
| grep -i '^set-cookie:' | head -1 \
| sed -E 's/^[Ss]et-[Cc]ookie:[[:space:]]*([^;]+).*/\1/' | tr -d '\r')"
[ -n "$COOKIE" ] || fail "换取 cookie 失败(token 可能已过期,或 $DSH_URL 不可达)"
# 4) 写新 include(先保留上一版)
printf 'proxy_set_header Cookie "%s";\n' "$COOKIE" > "$INCLUDE.tmp" \
|| fail "写入 $INCLUDE.tmp 失败,未改动 $INCLUDE"
HAVE_BACKUP=0
if [ -f "$INCLUDE" ]; then
cp -p "$INCLUDE" "$BACKUP" || fail "备份 $INCLUDE 到 $BACKUP 失败,未改动 $INCLUDE"
HAVE_BACKUP=1
else
echo "sync.sh: 注意:$INCLUDE 原先不存在,本次首次写入,无上一版可回滚" >&2
fi
mv "$INCLUDE.tmp" "$INCLUDE" || fail "替换 $INCLUDE 失败"
rollback() {
if [ "$HAVE_BACKUP" = 1 ]; then
if cp -p "$BACKUP" "$INCLUDE"; then
echo "sync.sh: $1;已恢复上一版 include($BACKUP)" >&2
else
echo "sync.sh: $1;恢复失败!请手工把 $BACKUP 复制回 $INCLUDE" >&2
fi
else
echo "sync.sh: $1;无上一版可恢复,请人工检查 $INCLUDE" >&2
fi
}
# 5) 固定容器 + 配置检查 + 重载
docker inspect -f '{{.State.Running}}' "$ORESTY" 2>/dev/null | grep -qx true \
|| { rollback "OpenResty 容器 $ORESTY 不存在或未运行,未 reload"; exit 1; }
docker exec "$ORESTY" "$NGINX_BIN" -t >/dev/null 2>&1 \
|| { rollback "nginx -t 失败,未 reload"; exit 1; }
docker exec "$ORESTY" "$NGINX_BIN" -s reload >/dev/null 2>&1 \
|| { rollback "nginx -s reload 失败"; exit 1; }
# 6) 全部成功后才提交成功状态
printf '%s' "$TOKEN" > "$TOKEN_FILE" \
|| fail "写入 $TOKEN_FILE 失败(配置已生效,下次执行会重新兑换)"
echo "已同步 $DOMAIN 的 cookie"
exit 0
Terminal window
sudo chown root:root /usr/local/lib/dsh-auth/sync.sh
sudo chmod 700 /usr/local/lib/dsh-auth/sync.sh

各参数的用途如下:

参数用途
DOMAIN实际访问的 authority;非标准端口也要包含端口,例如 dsh.example.com<8443>
DSH_URLDSH 本机地址;本文为 http://127.0.0.1<3080>
INCLUDE宿主机片段路径;容器内 include 路径按挂载关系填写
BACKUP上一版片段的备份位置;目录必须先存在
TOKEN_FILE保存上次同步成功的 token,并以文件修改时间判断刷新周期
SERVICEDSH 的 systemd 服务名,本文为 dsh.service
ORESTY本站实际 OpenResty 容器名,必须替换示例值
REFRESH_DAYS刷新周期天数;本文 20 天,小于默认 30 天有效期

脚本按以下顺序工作:

  1. 读取服务 MainPID,再查询对应 PID 的日志。服务未运行或找不到 token 时,返回失败。
  2. 如果 token 未变、include 存在,且距上次成功同步不足 20 天,直接退出。
  3. 携带实际 Host 兑换 cookie,取出 Set-Cookie 的 名字=值,用于上游 Cookie 请求头。
  4. 写入临时片段、保留上一版,再以 mv 替换 include,避免读取半截文件。
  5. 检查容器状态与 Nginx 配置,再发送重载信号;失败时尝试恢复备份,并报告结果。
  6. 命令成功后才写 TOKEN_FILE。刷新失败不更新成功标记,下次执行仍会重试。

刷新时间取自成功状态文件,脚本没有解析 cookie 的到期 payload。“已同步”表示上述命令成功执行,浏览器验收仍需结合日志和实际请求;恢复磁盘文件也不等于确认运行配置已恢复。

8.4 配套 systemd 定时器#

Terminal window
sudo tee /etc/systemd/system/dsh-auth-sync.service > /dev/null <<'EOF'
[Unit]
Description=Sync DSH browser cookie into nginx
After=dsh.service
[Service]
Type=oneshot
ExecStart=/usr/local/lib/dsh-auth/sync.sh
EOF
sudo tee /etc/systemd/system/dsh-auth-sync.timer > /dev/null <<'EOF'
[Unit]
Description=Periodically sync DSH browser cookie
[Timer]
OnBootSec=60
OnUnitActiveSec=10min
AccuracySec=30s
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now dsh-auth-sync.timer
sudo /usr/local/lib/dsh-auth/sync.sh

定时器在开机后检查一次,随后每 10 分钟运行;未到刷新时间时,脚本正常退出且没有输出。

After=dsh.service 只规定启动顺序,不会主动启动 DSH,也不保证它已经打印 token。首次执行过早时,先确认 DSH 就绪,再手动重试。

8.5 日常使用与验证#

普通重启后,等待 DSH 就绪,再运行同步脚本:

Terminal window
sudo systemctl restart dsh
# 检查本次启动日志,等待服务就绪后再执行。
sudo /usr/local/lib/dsh-auth/sync.sh

等待时间以服务状态和启动日志为准。旧 cookie 仍有效时,通常可以继续访问。

凭据重建后需要重新激活 DSH,新进程会生成新的 token,正常同步即可触发兑换。现用脚本没有强制刷新参数;token 未变且未到周期时,重复执行会正常跳过。

查看定时器、同步日志及片段中的指令数量:

Terminal window
sudo systemctl status dsh-auth-sync.timer --no-pager
sudo journalctl -u dsh-auth-sync.service -n 30 --no-pager
# 只确认有 Cookie 注入指令,不显示有效 cookie 内容。
sudo grep -c '^proxy_set_header Cookie ' \
/opt/1panel/www/sites/dsh.example.com/proxy/dsh-auth.inc

匹配数为 1,说明文件中有一条 Cookie 注入指令。随后刷新浏览器,确认外层登录成功、新的 remote.mux 为 101,并能收到回复。

脚本的常见输出及处理方式如下:

现用脚本输出含义与处理
已同步域名的 cookie命令流程成功,接着检查浏览器新连接与模型回复
无输出、退出码 0token 未变且未到刷新周期,正常跳过
未在日志里找到 token检查本次启动是否就绪,MainPID 与日志是否匹配
兑换请求失败或没有会话 cookie检查 Host、当前启动 token、DSH 状态和上游地址
配置校验或重载失败、退出码非 0查看恢复上一版是否成功,修复配置、容器名或挂载路径,再重试

同步脚本负责 DSH 内层 cookie,外层页面与 WebSocket 的访问控制仍由反代配置完成,两部分应一起验证。

8.6 调试时手动使用 token 登录#

需要排查 DSH 自带认证时,可以手动使用启动 token。本文的日常访问由上述同步脚本完成,下面的方法用于调试。先查看启动日志:

Terminal window
sudo journalctl -u dsh -n 50 --no-pager

日志中的地址类似:

dsh web: http://127.0.0.1:3080/?token=REDACTED

将地址部分换成实际 HTTPS 域名,保留当前启动 token,在浏览器中访问一次。兑换会话 cookie 后,就可以通过不带 token 的首页地址访问。

token 属于登录凭据,分享日志或截图时应遮盖。它随进程启动变化,而既有 cookie 在签名密钥、authority 和有效期满足要求时可以跨重启使用。

完成自动登录配置后,浏览器的日常访问无需执行这一步。

9. 选择工作区并验证首次会话#

打开 DSH,添加服务器上的工作区,例如 /srv/dsh-workspace。这里填写的是服务器目录。

选择已配置的模型,创建会话,发一条简单消息,确认能够收到回复。再打开开发者工具的 Network 面板,找到 remote.mux:

remote.mux 的 WebSocket 握手返回 101 Switching Protocols
remote.mux 的 WebSocket 握手返回 101 Switching Protocols

101 Switching Protocols 表示 WebSocket 已完成升级。结合模型回复,可以确认浏览器连接和模型调用均正常。服务显示 running、首页返回 200,只完成了其中一部分检查。官方首次使用指南

10. 常见问题与排查方向#

现象优先检查
systemd 启动失败dsh/Node 入口、PATH、目录权限、用户和启动参数
502 Bad GatewayDSH 是否运行、监听端口、容器网络与上游地址
API 返回 403trusted-host、透传 Host、浏览器 Origin 与端口
DSH 提示 authentication required手动登录或反代注入是否生效,authority、签名密钥与有效期是否匹配
401 且有 WWW-Authenticate: Basic反代用户名密码、密码文件路径与可读权限
页面正常但 WebSocket 401WS 实际匹配的 location、外层认证、通行证是否有效
一直连接中或重连Upgrade/Connection、超时、后端日志和认证状态
settings are unavailable in this browser核对版本与访问环境;本文通过 systemd 环境文件配置 API Key
设置了 Key 但模型仍调用失败变量是否进入进程、所选供应商与凭据引用、密钥有效性及 API 返回错误
npm ETARGET版本号、dist-tags 与当前 registry 的包版本
上传 413Nginx client_max_body_size 与应用自身附件限制
服务退出,日志有 OOM主机可用内存、swap、服务资源上限及正在运行的工作区任务

11. 日常维护#

Terminal window
# 查看状态
sudo systemctl status dsh --no-pager
# 查看日志;分享前清理 token 等凭据
sudo journalctl -u dsh -n 50 --no-pager
# 修改启动参数、环境文件或 drop-in 后重启
sudo systemctl daemon-reload
sudo systemctl restart dsh

升级前,备份实际的 DSH_HOME、工作区和 systemd 配置,并记录当前 DSH、Node 版本。备份可能包含会话和凭据,应作为私有数据保存。

执行 Office 文件转换、视频转码等任务时,注意内存和 swap 占用。我的实例曾遇到 Office 网页预览带来的资源问题,具体转换方式和插件配置应按当前版本核对。

写在最后#

把 DSH 放到云服务器上后,我可以通过浏览器回到同一个工作区,继续处理项目。部署过程中最有用的经验,是按服务、反代、认证、模型调用逐层检查:每一步有明确结果,遇到故障时就更容易定位。

对我的环境来说,模型设置页不可用的问题通过 systemd 环境文件解决;HTTP 与 WebSocket 的认证则分别做了成功和拒绝测试。最后刷新页面,看到新的连接建立并收到模型回复,这套部署才算真正跑通。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

在云服务器部署 DeepSeek Harness:从安装到 HTTPS 访问
https://blog.nyqking.top/posts/dsh-cloud-server-deployment/
作者
Kieran Nie
发布于
2026-10-01
许可协议
CC BY-NC-SA 4.0
文章目录