在内网服务器上部署 DeepSeek Harness 的 Web 端
我一直是在自己电脑上开终端用 DeepSeek Harness,关掉窗口,会话就断在那里。真正逼我想搬家的是这台服务器:它离我的笔记本隔着一整个内网,平时只能挂远程,而挂远程这件事本身就不适合长时间跑一个交互界面。
目标机是学校的一台内网 GPU 服务器:Ubuntu 22.04、16 核、64G 内存、40G 系统盘加一块 2T 数据盘,双 A40,内网地址 10.0.15.211。
这篇文章记录我怎么把它装上去、怎么让它别一断连就死、以及同事要访问时该怎么办。
一、先弄清要部署的到底是什么
这一节值得慢一点看,因为它决定了后面所有配置的含义。
dsh web 不是把一个服务「部署到云端」,它做的事情很具体:在你指定的本机地址上起一个 HTTP 服务,自己托管前端静态资源,同时在终端里打印一行带令牌的访问地址。令牌是随进程生成的,进程重开就换一个。
我用得最多的是三个参数:
| 参数 | 作用 |
|---|---|
--host | 绑定哪个本机地址。只接受一个具体的 IPv4/IPv6 字面量,通配地址会被直接拒绝 |
--public-url | 对外宣称的地址,只影响打印出来和写进上下文的那行 URL,不授予任何信任 |
--trusted-host | 额外允许哪些 authority 通过 /api 的浏览器信任栅栏 |
--host 不接受 0.0.0.0 这条,第一次见会愣一下。但想明白就通了:这个界面背后是一个能执行命令、读写文件的智能体,让它默认监听所有网卡不是个好主意。
这三个参数的分工很容易记混:
--host决定「谁连得进来」,--public-url决定「它对外说自己叫什么」,--trusted-host才决定「谁被认作可信来源」。
二、那两块 A40 在这件事里是闲着的
先把这个说清楚,免得你在机器上白折腾一轮。
DeepSeek Harness 是模型客户端,推理发生在 API 那一侧。这台机器的显卡对部署本身没有任何帮助。想「顺手把 A40 用起来」而去搭本地推理,那是完全另一个工程量级的问题,和部署 Web 端没有交集。
所以整台机器上真正被用到的资源很少:一个 Node 进程,一个监听在回环地址上的端口,加上 ~/.dsh 这个数据目录。64G 内存和 2T 数据盘在这个场景里几乎派不上用场。
内网这件事倒是影响很大。10.0.15.211 没有公网域名,也没有证书,后面所有访问方案都得围绕「怎么让同事连到这个内网地址」来设计。
三、装依赖:Node、pnpm、git
Ubuntu 22.04 自带软件源里的 Node 版本偏旧,直接 apt install nodejs 装到的版本跑不动它。我用 NodeSource 的源:
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs
node -v
pnpm 用 Node 自带的 corepack 开,不必再去装一个全局包:
corepack enable
pnpm -v
第一次执行 pnpm -v 时 corepack 会自己把对应版本下下来,看到版本号就说明通了。
git 一定要装,而且要现在就装:
apt install -y git
git --version
我在这条上栽过——另一台机器上装插件时报的都是 git ls-remote failed,底下跟一句「'git' 不是内部或外部命令」。插件从 git 仓库安装时,pnpm 真的会去调本机的 git 做 ls-remote,没有它,再换仓库地址也没用。
四、装好,然后第一次启动
npm i -g @deepseek-ai/dsh@0.2.1-rc.2
dsh --version
先只绑回环地址跑一次,别急着让外面连进来:
dsh web --host 127.0.0.1 --no-open --port 3080
终端里会打印一行这样的地址:
dsh web: http://127.0.0.1:3080/?token=<一串令牌>
这里有个细节值得验证一次:不带令牌直接访问根路径,服务返回 401。我在本机用 curl 试过,什么都不带就是 401。也就是说,那串令牌不是「欢迎页的装饰」,它是这个界面的访问凭据。
验证完按 Ctrl+C 停掉,接下来交给 systemd 管。
五、密钥:两条路,选一条
界面起来了但没有模型密钥,等于一台没有油的发动机。dsh 的默认凭据引用是环境变量 DEEPSEEK_API_KEY。
服务器上我更建议走环境变量:密钥不落在任何前端界面里,也不会进 shell 历史。
如果一时不想碰配置文件,Web 端的「模型设置」里也能填。界面在检测不到环境变量时会直接提示你输入 key,或者去配置那个环境变量。两条路都通,区别只是密钥存在哪。
六、给它一把「不会断」的椅子
--no-open 之外,真正让它在服务器上待住的是 systemd。先建用户和目录:
useradd -r -m -d /opt/dsh dsh
mkdir -p /opt/dsh/.dsh
chown -R dsh:dsh /opt/dsh
写单元文件 /etc/systemd/system/dsh-web.service:
[Unit]
Description=DeepSeek Harness Web
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=dsh
WorkingDirectory=/opt/dsh
Environment=HOME=/opt/dsh
Environment=DEEPSEEK_API_KEY=<你的密钥>
ExecStart=/usr/bin/dsh web --host 127.0.0.1 --port 3080 --no-open
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Environment= 里直接写密钥,意味着这个文件本身要当机密对待。想稳一点就换成 EnvironmentFile=/opt/dsh/dsh.env,把那个文件权限设成 600,单元文件里只写变量名。
然后:
systemctl daemon-reload
systemctl enable --now dsh-web
systemctl status dsh-web --no-pager
密钥在 ExecStart 的路径里,别忘了 dsh 的真实路径——用 which dsh 确认,npm -g 装出来的位置和你想象的可能不一样。
看日志:
journalctl -u dsh-web -f
重启进程会换一个新的令牌,所以每次重启后都要去日志里捞那一行 URL:
journalctl -u dsh-web --no-pager | grep "dsh web:"
七、同事怎么连进来
两种做法,取决于有多少人用。
只给自己用(推荐):走 SSH 隧道,服务器上什么都不用改。
ssh -L 3080:127.0.0.1:3080 root@10.0.15.211
隧道起来后,在本机浏览器打开服务器日志里那行带令牌的地址即可。隧道的意义是:服务只监听回环地址,只有能登上这台机器的人才能连,暴露面没有变大。
多人共用:在内网找一台能跑 nginx 的机器做反代,或者就放在本机上。这时要把两个参数一起给上:
dsh web --host 127.0.0.1 --port 3080 --no-open \
--public-url http://dsh.internal/ --trusted-host dsh.internal
nginx 那边正常反代到 127.0.0.1:3080。--public-url 只是让打印和写进上下文的地址变得正确,真正让浏览器端 API 调用被接受的是 --trusted-host。这两个参数经常被当成一回事,其实是各管一段。
一份够用的配置:
server {
listen 80;
server_name dsh.internal;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
}
那段 Upgrade 别省。界面的流式输出走 WebSocket,少了它页面能打开,回复却一直卡在「思考中」。proxy_read_timeout 也顺手调大,长回复不会中途被代理掐断。
八、那串令牌就是口令
这一节是我部署时最想提醒人的地方。
拿到带令牌的 URL,等于拿到这台机器上 agent 的全部权限:它能执行命令、读写文件。所以这不是一个「看板」,别用看板的风险等级去对待它。
具体到操作上:
- 令牌构成的 URL 不要贴进微信群、共享文档或工单;
- 服务器重启后令牌会变,旧链接直接失效,这一点反而是好事;
- 内网不是免死金牌——同一个网段里谁能访问
10.0.15.211,谁拿到链接就能进来; - 把
--host绑在127.0.0.1上配合隧道,比分发 URL 安全得多。
顺带一提:如果把 --host 指到非回环地址又走 HTTP,程序会打印一条暴露告警。这条告警值得认真读,它出现的时候说明你已经把界面放到了网络上。
九、装插件:拿我写的那只「白酒学伴」举例
主程序跑起来之后,装插件是同一个套路:
dsh plugin --profile web add github:LHY0125/dsh-baijiu-companion
装完要重启服务,插件才进配置:
systemctl restart dsh-web
dsh --profile web --dump-config | grep -A6 baijiu
GitHub 走不通的内网环境,换成完整 git 地址即可:
dsh plugin --profile web add https://gitee.com/lhy3364451258/dsh-baijiu-companion.git
注意 github:owner/repo 是 pnpm 的简写,它只内置 GitHub、GitLab、Bitbucket 三家,没有 gitee: 这种写法。Gitee 只能给完整地址。
十、数据在哪,升级怎么退
所有状态都在 $DSH_HOME,默认是运行用户的 ~/.dsh。我给了独立用户,所以是 /opt/dsh/.dsh。里面有 profiles/(每个 profile 的依赖与配置)、sessions/(会话日志)、storages/(插件状态)。
备份就是打包这个目录:
tar czf dsh-backup-$(date +%F).tar.gz -C /opt/dsh .dsh
升级装新版本再重启:
npm i -g @deepseek-ai/dsh@<新版本>
systemctl restart dsh-web
退回老版本就是把版本号换回去重装。升级前把 .dsh 目录备份一份——插件和配置都在里面,程序本身随时能重装。
装完之后
部署完之后,我把它从「开终端才有」变成了一个浏览器书签。真正让我省事的不是 Web 界面本身,而是 systemctl status 那一行绿色的 active (running)——不用再惦记某台机器上还有个进程是不是还活着。
真要给部署这件事排个优先级:先把令牌当口令对待,再决定用隧道还是反代,最后才去调那些参数。顺序反了,容易在「为什么同事打不开」上耗掉一晚上。
评论交流
欢迎留下你的想法