dshkit

报错排查:跑 dsh web 时 3080 端口被占用

DeepSeek Harness 网页端绑在 127.0.0.1:3080。怎么找出占用端口的进程、怎么正确传 --port(参数顺序有讲究)、以及怎么让换端口这件事固化下来。

更新于 2026-08-131 min
一句话结论

把新端口写在 launcher 的 flag 后面:dsh --profile web --port 3100。顺序有讲究,因为 --port 属于 app 而不是 launcher,launcher 遇到第一个不认识的 token 就停止解析。要找占用 3080 的进程用 lsof -nP -iTCP:3080 -sTCP:LISTEN。

dsh web 把网页端开在 http://127.0.0.1:3080。3080 这个端口足够常见——开发服务器、容器端口映射、 其它本地工具——撞车是常事。

找出占用端口的东西

macOS 与 Linux:

lsof -nP -iTCP:3080 -sTCP:LISTEN

Windows(PowerShell):

Get-NetTCPConnection -LocalPort 3080 -State Listen | Select-Object OwningProcess
Get-Process -Id <pid>

如果查出来是上一次 dsh 没干净退出留下的 node 进程,杀掉重来。如果是你确实需要的服务,那就让 harness 让路。

换个端口

dsh --profile web --port 3100

这也是为什么在边界情况下 dsh web --port 3100dsh --profile web --port 3100 不总是等价。 行为奇怪的时候,用显式的 --profile 写法。

让它固化下来

每次启动都手敲 --port 很烦。端口属于 app 配置,所以可以写进 patch 层。先找到拥有它的那一行:

dsh --profile web --dump-config

然后在 $DSH_HOME/profiles/web/cordis.patch.yml 里命中那一行。记住解析顺序——bundle、profile patch 文件、home 级的 $DSH_HOME/cordis.patch.yml、最后 --patch。home 级的设置会盖掉 profile 级的,改动 看起来没生效时先查这个。

一次性实验则完全不用碰文件:

dsh --profile web --patch ./tmp/alt-port.yml

提示被占用,但没人监听

三个常见嫌疑。

容器发布了这个端口。 docker ps 看 PORTS 那一列。已发布的端口即使里面的进程闲着,也会占住宿主机 的绑定。

半死的进程。 上次运行丢了终端但没释放 socket。lsof 仍然能看到,按 PID 杀掉。

监听地址不匹配。 harness 绑的是 127.0.0.1,不是 0.0.0.0。绑在 0.0.0.0:3080 的进程会冲突, 绑在某个具体外部网卡上的可能不会。如果 lsof -iTCP:3080 看起来是空的,去掉 -sTCP:LISTEN 再跑一次, 能抓到 TIME_WAIT 之类其它状态的 socket。

换完之后验一下

别默认改动生效了,确认新端口真的在服务:

curl -sSI http://127.0.0.1:3100 | head -1

有响应行说明起来了。connection refused 说明 flag 没落地——先查参数顺序,这比配置问题常见得多。

常见问题

为什么 dsh --port 3100 web 会失败,而 dsh --profile web --port 3100 可以?

launcher 先解析自己的 flag,遇到第一个不认识的 token 就把后面全部交给 app。把 --profile 这类 launcher flag 放在 --port 这类 app flag 前面。

能让局域网访问网页端吗?

默认绑 loopback,对一个能在你仓库里执行 shell 命令的工具来说这是刻意的。真需要远程访问,请走带认证的隧道,而不是把监听地址放宽。

提示端口被占,但看起来没人在监听。

通常是上次没干净退出的 dsh 进程,或者某个容器发布了这个端口。两边都查一下;macOS 上还要注意区分绑在 127.0.0.1 和绑在 0.0.0.0 的进程。

接着看