
DeepSeek-Harness系列(四):dsh 搬上服务器,五条路线实测

TL;DR
- 为什么值得搬上服务器:相比绑定单机的命令行 Agent 工具,dsh 的 Web 形态天然适合集中部署。跑通之后,所有设备的对话历史、上下文和长任务都能收拢到一处,随开随用。
- 核心阻碍是什么:为了防止服务器权限裸奔,dsh 出厂拒绝绑定公网端口,修改设置等敏感操作只认服务器本机请求,且普通局域网访问会触发浏览器的安全限制导致按钮失灵。
- 本文怎么读:
- 上半部分(不写一行代码):把 dsh 的安全逻辑、为什么连不上、以及五种接入方案的优劣用大白话聊透,帮你选定最适合自己的一条路。
- 下半部分(纯技术实操):直接给经过云服务器与绿联 NAS 实测的配置模板、命令和排障表,没有废话,照着抄作业即可。
刚接触 dsh 的时候,我其实挺兴奋的。
以前我最常用的是 Codex 这类终端工具,它们好用归好用,但有一个一直让我头疼的毛病:太绑定单台设备了。在家里台式机上跑了一半的任务、聊了一半的上下文,临时出门换上笔记本,或者想在手机上看一眼进度,记录全断,所有东西都碎片化地散落在不同机器上。
当我看到 dsh 原生自带一个完整的 Web 界面时,第一反应就是:这不就跟我服务器和 NAS 上跑的那些自托管服务一模一样吗?直接把它丢到 24 小时开机的服务器上,所有设备打开浏览器连过去,任务不断、记录集中,完美解决问题。
结果兴冲冲连上服务器、拉下代码、敲下启动命令,在浏览器里输进服务器地址的那一刻,直接被泼了一盆冷水——根本连不上。
但等我把这些问题挨个摸透、全部解决之后,dsh 瞬间力压 Codex 和 Claude Code,站上了我的 top1 工具持续至今。
今天就聊聊我怎么让 dsh 上服务器,开启远程访问。
上半部分:先把事情聊明白(零代码,选对路)
在动手敲命令之前,我们先花三分钟,不用任何生涩的技术词汇,把 dsh 为什么这么“难伺候”以及怎么治它理清楚。
1. 为什么装在服务器上,别人却连不上?
你平时在服务器或 NAS 上装个网盘、装个影音库,只要启动了,在家里别的电脑输个网址就能打开。但 dsh 不行,因为它的性质完全不同。
网盘最多只能读写你指定文件夹里的照片,而 dsh 是一个手里握着你服务器最高操作权限的智能体——它能自己写代码、删文件、装软件、跑脚本。如果它像普通网页一样,谁知道网址和端口都能连上来试一下,那无异于把你服务器的钥匙插在门锁上。
所以,官方在设计 dsh 时,直接在软件里定死了三道锁:
- 第一道锁:只听服务器自己的控制命令
dsh 启动后,默认只认服务器自己的操作,外面的设备喊它,它根本不理。就算你想通过修改启动选项强行让它对外开门,程序也会直接罢工退出。 - 第二道锁:认门票,不认账号密码
它没有传统的注册登录界面。每次启动,它只会在服务器的后台屏幕里悄悄吐出一串带特殊暗号(Token)的网址。你必须第一次拿着这串完整网址去访问,浏览器领到一张长期通行证存起来,以后才能正常用。 - 第三道锁:改核心设置,必须“本机亲自来”
平时跟它聊天、让它写代码,只要有通行证就行。但如果你想改 API 密钥、换工作文件夹,程序会死板地检查:这次点击是不是从服务器自己身上发出来的?如果是从外网别的电脑点过来的,哪怕你有通行证,它也会直接拒绝。
除了官方这三道锁,你电脑上的浏览器还会再补一刀:
现在的浏览器(Chrome、Edge、Safari 都一样)有个安全规定:如果一个网页既不是运行在你当前这台电脑上,网址又没有加密(不是 https 开头),浏览器就会悄悄掐断网页里负责生成随机编号的功能。表现出来的现象极其诡异:网页能正常打开,看起来一切完好,但你用鼠标点任何按钮、发任何消息,页面都像死了一样毫无反应。
2. 怎么解开这几道锁?(五种方案怎么选)
既然不能硬拆锁,我们的思路其实很简单:要么伪装成服务器自己,要么给通道加上合法的加密外壳。
根据你平时怎么用电脑和手机,一共有五种成熟的路子。不用全配,挑最适合你的一条就行(我现在用的是方案四):
- 方案一:牵一根隐形延长线(SSH 隧道)—— 适合一个人用电脑
既然 dsh 只认服务器自己,那我们就用电脑自带的连接工具,在你的电脑和服务器之间牵一根加密的虚拟网线。你在自己电脑浏览器里访问“本机”,数据顺着这根线钻进服务器,dsh 就会以为是服务器自己在操作,浏览器也觉得非常安全。
优点:不用买域名,不用装新软件,还能随便改设置。强烈建议所有人第一步都先用它跑通。 - 方案二:建一个私人虚拟局域网(Tailscale)—— 适合想用手机、平板随时连
方案一在电脑上很爽,但手机上没法牵那根线。如果你想在通勤路上用手机看任务,或者在家里、公司多台电脑之间无缝切换,可以在服务器和手机上同时装一个叫 Tailscale 的免费组网软件。它不仅能把你的所有设备拉进同一个虚拟房间,还会免费送你一个带加密锁的专属网址,完美解决浏览器按钮失灵的问题。 - 方案三:挂牌营业加门卫(反向代理 + 域名)—— 适合两三人的小团队共用
如果你有自己的域名和公网服务器,想让几个朋友或同事直接输网址就能用,那就得在 dsh 外面请个“正规门卫”(比如 Caddy 或 Nginx)。门卫负责对外出示加密证书、拦在门口收一道账号密码,然后再把请求转交给里面的 dsh。 - 方案四:装进独立集装箱(Docker 容器)—— 适合家用 NAS 用户
如果你把 dsh 装在绿联、群晖这类家用 NAS 上,会遇到一个特有的怪病:NAS 系统为了方便家庭共享,总是强行把文件夹的权限改得特别宽松;而 dsh 每次启动都会检查自己的密码本,一旦发现权限太松(谁都能看),它为了安全就直接拒绝启动。把 dsh 装进 Docker 集装箱,给它的密码本单独配一个不受 NAS 管辖的独立保险箱,就能彻底断根。 - 方案五:不开网页当钟点工(无头模式 + 定时任务)—— 适合定时自动跑活
如果你只是想让它每天早上 8 点自动汇总一下代码改动、写份日报,根本不需要开网页界面。直接用系统的定时闹钟每天唤醒它一次,跑完就歇着,一点内存都不占。
3. 一个重要的安全提醒:别装“免折腾外挂”
你在网上找教程或逛插件市场时,可能会看到有人分享所谓的“一键开启局域网访问”插件,号称装上就能直接用 IP 访问。千万别在服务器上装这种东西。
这些外挂的原理,基本都是在程序运行时强行篡改访问来源、暴力拆掉官方的安全校验。装上之后不仅网页经常莫名其妙掉线,更要命的是你相当于把一台拥有系统执行权限的机器人直接扔在了大街上。用上面说的方案一或方案二也就花十分钟,犯不着拿服务器的安全去赌。
下半部分:技术流实操与配置(直接抄作业)
理清了思路,下面进入纯技术环节。包含环境部署、进程托管、反代配置、Docker 编排与故障速查,所有配置均在 Debian 服务器与绿联 DX4600 Pro(UGOS Pro)上实测通过。
路线一:SSH 本地端口转发 + systemd 托管
原理:通过 ssh -L 将本地 127.0.0.1:3080 映射至远端 127.0.0.1:3080。浏览器侧判定为 localhost 安全上下文(放行 crypto.randomUUID),服务端判定为 Loopback 来源(放行特权 API)。
1. 服务端基础安装与首访初始化
环境依赖 Node 24 LTS 与 pnpm。首次启动建议在前台或 tmux 中完成以获取初始 Token:
npm install -g @deepseek-ai/dsh # 安装dsh
mkdir -p ~/projects/dsh-workspace && cd ~/projects/dsh-workspace && git init
# 启动并记录终端输出的 http://127.0.0.1:3080/?token=...
dsh web --no-open
2. 客户端打通隧道并持久化配置
在本地机器执行转发:
ssh -L 3080:127.0.0.1:3080 user@server_ip -N
本地浏览器访问刚刚记录的 http://127.0.0.1:3080?token=... 链接,就能直接访问 dsh 界面了。
为免去每次手敲命令及防止 TCP 空闲断连,直接在本地 ~/.ssh/config 固化配置:
Host dsh-tunnel
HostName 服务器IP
User 用户名
LocalForward 3080 127.0.0.1:3080
ServerAliveInterval 60
ServerAliveCountMax 3
日常启动仅需执行:ssh -N dsh-tunnel。
3. 服务端 systemd 生产级托管
注意规避两个高频坑:
- 环境变量缺失导致静默退出:systemd 不加载
.bashrc。缺失HOME时 dsh 会找不到~/.dsh并主动调用exit 0;由于退出码为 0,不会触发Restart=on-failure。使用nvm时还必须显式注入 Node 的PATH。 - 原子写导致的 777 权限复发:dsh 更新凭据采用“写临时文件 + rename 覆盖”机制,新文件权限由进程
umask决定。在默认 umask 宽松的系统或 NAS 上,修改设置后文件权限会变为 777,导致下次启动因安全自检失败(credentials is readable beyond its owner)而拒启。必须通过UMask=0077锁死。
# 确认二进制真实路径
which dsh
sudo tee /etc/systemd/system/dsh.service > /dev/null <<'EOF'
[Unit]
Description=DeepSeek Harness Web Service
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=你的用户名
WorkingDirectory=/home/你的用户名/projects/dsh-workspace
# 显式注入 HOME 与 Node PATH(请按实际 nvm/系统路径替换)
Environment=HOME=/home/你的用户名
Environment=PATH=/home/你的用户名/.nvm/versions/node/v24.21.0/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/dsh web --no-open
Restart=on-failure
RestartSec=5
SyslogIdentifier=dsh
StandardOutput=append:/home/你的用户名/dsh-web.log
StandardError=append:/home/你的用户名/dsh-web.log
# 强制进程创建的文件权限不超过 600/700
UMask=0077
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now dsh
查看状态与提取新 Token:tail -f ~/dsh-web.log。
路线二:Tailscale Serve(多端与移动端 HTTPS 接入)
通过 Tailscale 注入合法 Let's Encrypt 证书并反代本地端口,解决移动端无 SSH 隧道及非安全上下文问题。
- 登录 Tailscale 管理后台(DNS 页面):开启 MagicDNS 与 HTTPS Certificates。
- 服务端一键暴露:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
sudo tailscale serve --bg http://localhost:3080
- 访问:组网设备直接通过
https://<机器名>.<tailnet>.ts.net访问。
(注:Tailscale Serve 转发流量在应用层仍带有外部 Host/Origin 特征,若触发特权 API 403 拦截,低频配置修改切回 SSH 隧道操作即可。)
路线三:Caddy / Nginx 反向代理(公网域名 + Basic Auth)
1. 反代层配置(二选一)
选项 A:Caddy(自动 TLS)
# 生成密码哈希
caddy hash-password --plaintext '你的强密码'
# /etc/caddy/Caddyfile
dsh.yourdomain.com {
basic_auth {
admin 粘贴密码哈希
}
reverse_proxy 127.0.0.1:3080
}
选项 B:Nginx(需手动配齐 Header 与关闭缓冲)
注意:Nginx 的 proxy_set_header 不会跨块继承,四个核心头必须写在最终生效的 location 块内;同时关闭 buffering 以保障 SSE 流式响应。
server {
listen 80;
server_name dsh.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3080;
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_set_header Host $http_host;
proxy_set_header Origin $http_origin;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 3600;
}
}
随后执行 sudo certbot --nginx -d dsh.yourdomain.com 完成 TLS 部署。
2. dsh 侧配置 CSRF 域名白名单(必选)
外部域名访问必须在启动命令中声明 --trusted-host,否则所有 API 请求均被防 CSRF 中间件拦截返回 403:
dsh web --no-open --trusted-host dsh.yourdomain.com
(若挂载在子路径下,0.2.x+ 版本追加 --public-url [https://example.com/dsh/](https://example.com/dsh/)。)
路线四:Docker 容器化(NAS ACL 权限隔离方案)
针对绿联 UGOS、群晖 DSM 等底层存储池强制覆写挂载目录为 777 的问题,核心解法是:工作区代码走 Bind Mount(方便宿主机查看),而 ~/.dsh 配置目录走 Docker Named Volume(由 Docker 托管权限,彻底绕开 NAS 文件系统干预)。
1. Dockerfile(锁定版本并补齐工具链)
缺失 git 会导致前端 Diff 视图静默失效;缺失 ripgrep/curl/unzip 会导致内置检索与插件安装失败:
FROM node:24-slim
ARG DSH_VERSION=0.2.0
ENV TZ=Asia/Shanghai
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 make g++ ca-certificates tzdata \
git openssh-client curl unzip ripgrep procps less nano \
&& rm -rf /var/lib/apt/lists/*
RUN usermod -d /home/appuser node
RUN npm i -g "@deepseek-ai/dsh@${DSH_VERSION}" pnpm
WORKDIR /home/appuser
2. docker-compose.yml
容器内拥有独立 Network Namespace,因此容器内需指定 --host 0.0.0.0 以接收网桥流量,同时在宿主机 ports 层严格绑定 127.0.0.1:33080:3080 维持回环安全边界:
services:
dsh:
build:
context: .
args:
DSH_VERSION: "0.2.0"
image: dsh-custom:0.2.0
user: "1000:1000" # 对齐宿主机实际 uid:gid
environment:
HOME: /home/appuser
command:
- dsh
- web
- --host
- "0.0.0.0"
- --port
- "3080"
- --trusted-host
- "dsh.yourdomain.com" # 仅走隧道时可移除此项
- --no-open
ports:
- "127.0.0.1:33080:3080"
volumes:
- /home/你的用户名/projects:/home/appuser/projects
- dsh-config:/home/appuser/.dsh # Named Volume 隔离权限
volumes:
dsh-config:
name: dsh-config
构建与日志查看:
docker compose up -d --build
docker compose logs -f dsh
路线五:Headless + Cron(无人值守批处理)
复用 ~/.dsh/.credentials.yaml 中的凭据,直接通过 CLI 驱动定时任务。
# 1. 手动验证连通性
cd ~/projects/dsh-workspace
dsh --profile headless "审阅最近三天的 Git 提交记录,输出改动摘要"
# 2. 写入 crontab -e(Cron 环境变量极简,必须全量使用绝对路径)
0 8 * * * cd /home/你的用户名/projects/dsh-workspace && /usr/bin/dsh --profile headless "总结昨日代码提交并生成简报" >> ~/dsh-cron.log 2>&1
生产安全基线:
- 严禁在无人值守脚本中开启
danger-full-access。 - 在
~/.dsh配置中启用预算熔断(overLimit: block),防止死循环刷爆账单。
故障排查速查表
| 故障表现 | 触发根因 | 解决方案 |
|---|---|---|
SSH 隧道报错 administratively prohibited: open failed | 服务端(常见于群晖/威联通)默认禁用 TCP 转发 | 修改 /etc/ssh/sshd_config 中 AllowTcpForwarding yes 并重启 sshd(注意系统升级可能重置) |
反代后页面可打开,但接口全报 403 | 未配置 --trusted-host 触发 CSRF 拦截,或 Nginx 漏传 Header | 启动参数补齐 --trusted-host;确认 Nginx 的 Host/Origin/Upgrade/Connection 写在当前 location 块内 |
| 页面渲染正常,所有按钮点击无反应 | 裸 http://内网IP 访问触发非安全上下文,crypto.randomUUID 被禁用 | 切回 127.0.0.1 SSH 隧道,或启用 Tailscale / 反代的 HTTPS |
对话正常,仅修改设置并保存时报 403 | 特权 API 硬编码仅允许 127.0.0.1 / localhost 来源 | 预期安全设计。低频改配需求临时走 SSH 隧道完成 |
systemd 启动两秒即变成 dead (status=0) | 缺失 HOME 环境变量导致主动 exit 0,不触发 on-failure | 在 .service 中补齐 Environment=HOME=... 与 Node PATH |
重启后复发 credentials is readable beyond its owner (777) | 原子写生成的新文件继承了宽松 umask 或被 NAS 强改权限 | 裸机 systemd 增加 UMask=0077;NAS 改用 Docker + Named Volume 托管 ~/.dsh |
写在最后
折腾完这一整套流程,我现在打开电脑或手机,随时都能连上服务器里那个一直在线的 dsh。以前散落在各个设备上的脚本任务和临时对话,终于有了一个稳定的大本营。
回看这个系列的前四篇,我们从最初看懂它的插件化思路、拆解底层的 Cordis 架构、在本地跑通第一套配置,一直走到今天把它安稳地放进服务器和 NAS。把这些基础的工程细节一个个摸透,它才真正从一个“尝鲜的新玩具”,变成了我日常开发流里底座级别的工具。
这篇文章算是给基础搭建与部署做一个阶段性的收尾,但肯定不是结束。随着后面我在实际项目里拿它跑更多复杂的自动化流程、写更多自定义插件,攒够了新的实战经验或者踩到了新坑,这个系列还会继续往下更。
先把服务器上的服务跑起来,我们下篇见。