Skill
dsh-vps-https-deployer
本地 Agent 通过 OpenSSH 连接远程 Ubuntu VPS,安装 DSH Web,并使用 HTTPS 和仅密码网关开放外网访问。
- Source
- vinokok
- Updated
- Updated 2 days ago
Readme
# DSH 远程 VPS 外网部署
> ## Agent Skill(默认中文)
>
> **[立即读取并自动部署:SKILL.md](SKILL.md)**
>
> [English Skill / 英文版](SKILL.en.md)
>
> 本 Skill 供本地 Agent 通过 OpenSSH 连接远程 VPS,自动完成 DSH 安装、HTTPS、密码网关、设置兼容、验证、升级和回滚。
[中文部署正文](#架构) · [English guide / 英文部署文档](README.en.md)
本指南说明如何在**远程 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` 文件提交到公开仓库。
## 架构
```text
浏览器
|
| 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 主机指纹,再连接:
```bash
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 仓库。每个远程命令都应记录退出码;部署前先执行只读检查:
```bash
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 时不会覆盖外网访问层。
```ini
# /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
```
启用并验证:
```bash
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 记录:
```text
<dsh.example.com> -> <VPS_PUBLIC_IP>
```
申请 Let's Encrypt HTTP-01 证书时,DNS 必须能把验证请求送到 VPS 上的 Nginx。如果使用 Cloudflare,可以临时关闭代理完成验证;证书签发后再开启代理,并使用 `Full (strict)`。
临时 HTTP 配置示例:
```nginx
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 申请证书,并将证书路径配置到反向代理:
```bash
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:
```nginx
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:
```bash
/www/server/nginx/sbin/nginx -t
/www/server/nginx/sbin/nginx -s reload
```
## 4. 部署独立的仅密码登录网关
网关是独立的 Node.js 服务,应具备:
- 不暴露受保护产品名称的通用登录页
- 服务端 PBKDF2、bcrypt 或 Argon2 密码校验
- 前端 HTML/JavaScript 中不包含明文密码
- 签名的 `HttpOnly`、`Secure`、`SameSite=Strict` Cookie
- Cookie 过期和退出登录
- 登录失败限流
- HTTP 反代和 WebSocket Upgrade 转发
- 只监听 `127.0.0.1`
示例 systemd 服务:
```ini
# /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
```
秘密文件单独存放:
```bash
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 同源请求。密码网关虽然完成了认证,但浏览器地址仍然是公网域名,因此需要两层兼容:
1. 网关认证后,向 DSH 内部转发时使用 `Host: 127.0.0.1:3081`,并移除浏览器的 `Origin`、`Referer` 和 `Sec-Fetch-*` 信任标记。
2. 对外提供的 DSH 浏览器连接 bundle 需要兼容层,使已认证的远程页面被视为受控 Host。
兼容层必须放在 DSH 安装目录之外,例如:
```text
/opt/dsh-login-gateway/plugins/remote-host-compat.js
```
网关只应处理精确匹配的 `@deepseek-ai/dsh-client-connection/client.js`,先验证已知源码标记;如果 DSH 升级后结构变化,则拒绝改写并记录错误。不要直接修改 `node_modules`,这样升级 DSH 不会重置兼容层。
每次升级后验证:
```bash
curl -k -b <authenticated-cookie-file> \
https://<dsh.example.com>/plugins/@deepseek-ai/dsh-client-connection/client.js \
| grep 'isLoopback: true'
```
然后用无痕窗口打开页面,进入模型设置,确认提供方目录和设置读写正常。
## 6. 验证清单
```bash
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=Strict` Cookie
- 登录后可以打开 DSH 首页
- 模型/提供方设置正常加载
- WebSocket 保持可用
- 公网无法直接访问 3081 和 3090
- 证书续期 hook 在 reload 前执行 `nginx -t`
## 7. DSH 升级流程
```bash
# 先备份服务和网关配置
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 配置
```bash
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](README.en.md)。
Install
# Skills are files: copy them into $DSH_HOME/skills/dsh-vps-https-deployer (defaults to ~/.dsh/skills/dsh-vps-https-deployer)
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install vinokok-dsh-vps-https-deployer from the hub