dsh-external-access-guide
远程 VPS 上部署 DSH 并通过 HTTPS 安全开放外网访问 | Deploy DSH on a remote VPS and expose it securely over HTTPS
- Stars
- 0
- Language
- —
- Created
- Aug 24, 2026
- Updated
- Aug 24, 2026
Introduction
DSH 远程 VPS 外网部署
Agent Skill(默认中文)
本 Skill 供本地 Agent 通过 OpenSSH 连接远程 VPS,自动完成 DSH 安装、HTTPS、密码网关、设置兼容、验证、升级和回滚。
中文部署正文 · English guide / 英文部署文档
本指南说明如何在远程 Ubuntu VPS 上部署 DeepSeek Harness(DSH)Web,并通过公网 HTTPS 域名、Nginx/反向代理 和仅密码登录网关安全访问。适用于通过 SSH 管理 VPS 的管理员,不会修改本地电脑上的 DSH Desktop 客户端。
参考部署使用 <VPS_IP>、/opt/dsh-headless-test、Nginx,以及 <dsh.example.com>。请替换所有尖括号占位符。
安全提醒: 本文已脱敏。不要把密码、API Key、SSH 私钥、TLS 私钥、Session secret、密码哈希、Cookie 或
.env文件提交到公开仓库。
架构
浏览器
|
| HTTPS + 仅密码登录 Cookie
v
Nginx :443
|
v
独立登录网关 127.0.0.1:3090
|
| 认证后的反向代理
v
DSH Web 127.0.0.1:3081
DSH 和登录网关都只监听 VPS 本机回环地址,公网唯一入口是 Nginx 的 HTTPS 端口。
前提条件
- Ubuntu VPS,已安装 Node.js 和 systemd
- DNS 名称
<dsh.example.com>指向 VPS - Nginx 或标准 Nginx
- DSH 独立安装在例如
/opt/dsh-headless-test - 建议使用专用非 root 服务账号;只有现有部署确实需要时才使用 root
1. 从本地 Agent 连接远程 VPS
部署从本地 Agent 开始。使用系统 OpenSSH 和一次性远程命令执行,连接和部署逻辑由本 Skill 独立完成。
先在本地确认 SSH 主机指纹,再连接:
ssh-keyscan -t ed25519 <VPS_HOST> > /tmp/<VPS_HOST>.known_hosts
ssh-keygen -lf /tmp/<VPS_HOST>.known_hosts
ssh -o UserKnownHostsFile=/tmp/<VPS_HOST>.known_hosts -o StrictHostKeyChecking=yes <VPS_USER>@<VPS_HOST>
自动化 Agent 应使用本地安全存储中的 SSH 私钥,不应把私钥、密码或 token 写进 skill、脚本、日志或 Git 仓库。每个远程命令都应记录退出码;部署前先执行只读检查:
ssh -o UserKnownHostsFile=/tmp/<VPS_HOST>.known_hosts -o StrictHostKeyChecking=yes <VPS_USER>@<VPS_HOST> \
'uname -a; command -v systemctl; command -v node || true; command -v nginx || true'
确认目标主机、操作系统、磁盘空间、Node.js、systemd 和反向代理状态后,Agent 才能继续执行远程安装。
2. 在远程 VPS 上运行 DSH 服务
将 DSH 安装在独立目录中,并让它只绑定 127.0.0.1。网关文件不要放进 DSH 的 node_modules,这样升级 DSH 时不会覆盖外网访问层。
# /etc/systemd/system/dsh-web.service
[Unit]
Description=DeepSeek Harness Web UI
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=<dsh-user>
WorkingDirectory=/opt/dsh-headless-test
Environment=DSH_HOME=/opt/dsh-headless-test/home
ExecStart=/opt/dsh-headless-test/node_modules/.bin/dsh web --host 127.0.0.1 --port 3081 --no-open --trusted-host <dsh.example.com>
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
启用并验证:
systemctl daemon-reload
systemctl enable --now dsh-web.service
systemctl status dsh-web.service
ss -ltnp | grep 3081
监听地址应为 127.0.0.1:3081,不能是 0.0.0.0:3081。
3. 配置 DNS 和 HTTPS 证书
添加 A 记录:
<dsh.example.com> -> <VPS_PUBLIC_IP>
申请 Let's Encrypt HTTP-01 证书时,DNS 必须能把验证请求送到 VPS 上的 Nginx。如果使用 Cloudflare,可以临时关闭代理完成验证;证书签发后再开启代理,并使用 Full (strict)。
临时 HTTP 配置示例:
server {
listen 80;
server_name <dsh.example.com>;
root /www/wwwroot/<dsh.example.com>;
location ^~ /.well-known/acme-challenge/ { try_files $uri =404; }
location / { return 301 https://$host$request_uri; }
}
通过 certbot webroot 申请证书,并将证书路径配置到反向代理:
certbot certonly --webroot -w /www/wwwroot/<dsh.example.com> -d <dsh.example.com>
install -d -m 750 /www/server/panel/vhost/cert/<dsh.example.com>
install -m 640 /etc/letsencrypt/live/<dsh.example.com>/fullchain.pem /www/server/panel/vhost/cert/<dsh.example.com>/fullchain.pem
install -m 640 /etc/letsencrypt/live/<dsh.example.com>/privkey.pem /www/server/panel/vhost/cert/<dsh.example.com>/privkey.pem
配置证书续期 deploy hook:续期后复制证书,并且仅在 nginx -t 成功后 reload Nginx。
4. 配置 Nginx 反代 HTTPS 和 WebSocket
HTTPS 虚拟主机应反代到登录网关,而不是直接反代到 DSH:
map $http_upgrade $dsh_connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name <dsh.example.com>;
ssl_certificate /www/server/panel/vhost/cert/<dsh.example.com>/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/<dsh.example.com>/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:3090;
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 https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $dsh_connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
}
检查并 reload:
/www/server/nginx/sbin/nginx -t
/www/server/nginx/sbin/nginx -s reload
4. 部署独立的仅密码登录网关
网关是独立的 Node.js 服务,应具备:
- 不暴露受保护产品名称的通用登录页
- 服务端 PBKDF2、bcrypt 或 Argon2 密码校验
- 前端 HTML/JavaScript 中不包含明文密码
- 签名的
HttpOnly、Secure、SameSite=StrictCookie - Cookie 过期和退出登录
- 登录失败限流
- HTTP 反代和 WebSocket Upgrade 转发
- 只监听
127.0.0.1
示例 systemd 服务:
# /etc/systemd/system/dsh-login-gateway.service
[Unit]
Description=DSH password gateway
After=network-online.target dsh-web.service
Requires=dsh-web.service
[Service]
Type=simple
User=<gateway-user>
WorkingDirectory=/opt/dsh-login-gateway
Environment=NODE_ENV=production
Environment=GATEWAY_HOST=127.0.0.1
Environment=GATEWAY_PORT=3090
ExecStart=/usr/bin/node /opt/dsh-login-gateway/server.js
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/dsh-gateway
[Install]
WantedBy=multi-user.target
秘密文件单独存放:
install -d -m 750 /var/lib/dsh-gateway
chmod 600 /var/lib/dsh-gateway/password.hash /var/lib/dsh-gateway/session.secret
这些文件绝不能上传到公开仓库。
5. 解决远程 HTTPS 下的 DSH 设置限制
DSH 会将设置、凭据和模型发现操作限制为 loopback 同源请求。密码网关虽然完成了认证,但浏览器地址仍然是公网域名,因此需要两层兼容:
- 网关认证后,向 DSH 内部转发时使用
Host: 127.0.0.1:3081,并移除浏览器的Origin、Referer和Sec-Fetch-*信任标记。 - 对外提供的 DSH 浏览器连接 bundle 需要兼容层,使已认证的远程页面被视为受控 Host。
兼容层必须放在 DSH 安装目录之外,例如:
/opt/dsh-login-gateway/plugins/remote-host-compat.js
网关只应处理精确匹配的 @deepseek-ai/dsh-client-connection/client.js,先验证已知源码标记;如果 DSH 升级后结构变化,则拒绝改写并记录错误。不要直接修改 node_modules,这样升级 DSH 不会重置兼容层。
每次升级后验证:
curl -k -b <authenticated-cookie-file> \
https://<dsh.example.com>/plugins/@deepseek-ai/dsh-client-connection/client.js \
| grep 'isLoopback: true'
然后用无痕窗口打开页面,进入模型设置,确认提供方目录和设置读写正常。
6. 验证清单
systemctl is-active dsh-web.service
systemctl is-active dsh-login-gateway.service
ss -ltnp | grep -E '127.0.0.1:(3081|3090)'
/www/server/nginx/sbin/nginx -t
curl -k -I https://<dsh.example.com>/
功能验证:
- 未登录请求只返回通用登录页
- 错误密码返回
401 - 连续失败后触发
429 - 正确密码返回
HttpOnly; Secure; SameSite=StrictCookie - 登录后可以打开 DSH 首页
- 模型/提供方设置正常加载
- WebSocket 保持可用
- 公网无法直接访问 3081 和 3090
- 证书续期 hook 在 reload 前执行
nginx -t
7. DSH 升级流程
# 先备份服务和网关配置
systemctl cat dsh-web.service > /var/backups/dsh-web.service.txt
systemctl cat dsh-login-gateway.service > /var/backups/dsh-login-gateway.service.txt
cp /www/server/panel/vhost/nginx/<dsh.example.com>.conf /var/backups/
# 只在 DSH 自己的安装目录升级
cd /opt/dsh-headless-test
pnpm update @deepseek-ai/dsh
# 重启并验证
systemctl restart dsh-web.service
systemctl restart dsh-login-gateway.service
systemctl is-active dsh-web.service dsh-login-gateway.service
网关源码、秘密文件、systemd 服务、Nginx 配置和兼容插件都在 DSH npm 包之外,正常情况下不会被 DSH 升级覆盖。
8. 回滚
建议保存以下带日期的备份:
- Nginx 虚拟主机
- DSH systemd 服务
- 网关 systemd 服务
- 网关源码
- DSH profile 配置
systemctl stop dsh-login-gateway.service
# 检查后恢复指定日期的备份
/www/server/nginx/sbin/nginx -t
/www/server/nginx/sbin/nginx -s reload
systemctl start dsh-web.service
如果兼容层拒绝新的 DSH bundle,应先停用兼容层并审查新版本结构,不要盲目对任意 JavaScript 做替换。
常见问题
settings are unavailable in this browser
浏览器加载的 bundle 仍把公网域名视为非 loopback。检查兼容层是否生效,并用无痕窗口或清除站点数据重新加载。
transport failure for /api/llm.providers: HTTP 403
检查网关是否移除了 Origin、Referer、Sec-Fetch-*,是否设置了内部 loopback Host,以及 WebSocket 是否正确转发。
出现浏览器 Basic Auth 弹窗
Nginx 仍启用了旧的 auth_basic。删除 HTTPS location 中的 Basic Auth,改为反代到登录网关。
修改 bundle 后出现 502
改写响应时,删除 transfer-encoding: chunked 再设置新的 content-length;保留 JavaScript Content-Type,并禁止该改写资源使用旧缓存。
安全建议
- 使用高强度唯一密码,怀疑泄露时立即轮换。
- 使用权限最小化且有效期较短的 GitHub 部署凭据。
- 开启 Cloudflare 代理时使用
Full (strict)。 - 优先使用专用非 root Linux 服务账号。
- 不要通过防火墙规则或端口转发暴露 DSH 的回环端口。
- 每次 DSH 版本变化后重新审查兼容层。
语言切换: 中文为默认入口;英文请查看 README.en.md。