问题:会话越开越多,端口开始打架
把 Claude Code 放到一台常开的 Mac mini 上以后,很多人会很快走到下一步:用 git worktree 开出三四个工作目录,在 tmux 里每个窗口跑一个 Claude Code 会话,各做一个功能。有人甚至让单个会话连续跑十几个小时,自己只是偶尔从笔记本或平板上看一眼进度。
这时最容易踩的坑不是模型,而是开发服务器的端口。前端项目默认都想用 3000,后端默认 8000 或 8080。第一个会话起了服务,第二个会话再起就报 EADDRINUSE;更糟的是有的工具会自动换到 3001,Claude 却还在按 3000 去测,结果测的是另一个会话的代码,日志看起来一切正常,结论却是错的。
思路:一个 worktree 一段固定端口
解决办法很朴素:不要让端口「自动找空位」,而是给每个 worktree 预先分好一段端口,并且把这段端口写进三个地方:项目的环境变量文件、会话能读到的说明文件、你自己的转发配置。这样无论哪个会话重启、哪个服务崩了再起,端口都不会漂移。
下面的步骤假设你在新加坡 Mac mini 上已经装好了 Claude Code、git 和 tmux,并能用 SSH 或 UU 远程连接登录。
步骤一:建 worktree 时顺手分配端口段
约定一个简单规则:第 N 个 worktree 使用 3000 + N*10 开始的 10 个端口。前端用段内第一个,后端用第二个,其余留给数据库面板、Storybook 之类。
# 在主仓库目录执行
cd ~/work/myapp
mkwt() {
name="$1"; n="$2"
git worktree add "../myapp-$name" -b "feat/$name"
base=$((3000 + n*10))
cat > "../myapp-$name/.env.local" <<EOT
PORT=$base
API_PORT=$((base+1))
EXTRA_PORT=$((base+2))
EOT
echo "$name -> $base-$((base+9))" >> ~/work/ports.txt
}
mkwt login 1
mkwt billing 2
mkwt search 3
~/work/ports.txt 是你的端口登记表,后面排查时一眼就能看出谁占了哪段。记得把 .env.local 加进 .gitignore,它只属于这台机器。
步骤二:让项目真正读这些变量
很多框架的启动脚本把端口写死在 package.json 里。检查一遍,改成读环境变量,例如:
"scripts": {
"dev": "next dev -p ${PORT:-3000}",
"api": "node server.js --port ${API_PORT:-8000}"
}
同时关掉「端口被占就自动换一个」的行为(不少开发服务器有 strict port 一类选项)。宁可启动失败报错,也不要悄悄换端口,否则 Claude 拿到的地址和实际服务对不上。
步骤三:把端口写进每个 worktree 的 CLAUDE.md
Claude Code 会读取项目里的 CLAUDE.md。在每个 worktree 里加一小段,明确告诉会话:
## 本工作目录的端口
- 前端:http://localhost:3010(来自 .env.local 的 PORT)
- 后端:http://localhost:3011
- 不要使用 3000 或其他 worktree 的端口;启动失败时先报告,不要自行换端口。
- 结束任务前用 lsof 确认自己启动的进程已停止。
最后一条很重要:长会话常常起了服务就忘了关,几轮之后机器上挂着一堆孤儿进程。把「谁启动谁收尾」写进说明,比事后手动清理省事。
步骤四:tmux 里一个窗口对应一个 worktree
tmux new-session -d -s dev -n login -c ~/work/myapp-login
tmux new-window -t dev -n billing -c ~/work/myapp-billing
tmux new-window -t dev -n search -c ~/work/myapp-search
# 进入后在每个窗口里启动 claude
tmux attach -t dev
窗口名和 worktree 名保持一致,再配合 ports.txt,你从任何设备接回来都能立刻对上号。断开 SSH 或 UU 远程连接并不会影响 tmux 里的会话,这正是把它放在常开机器上的意义。
步骤五:在笔记本或平板上看预览,用 SSH 端口转发
服务只监听在 Mac mini 的 localhost 上,不要为了方便改成监听所有网卡。需要看页面时,在你自己的设备上做本地转发:
ssh -N -L 3010:localhost:3010 -L 3020:localhost:3020 user@你的机器地址
然后在本地浏览器打开 http://localhost:3010 就是 login 分支的预览。平板上可以用支持端口转发的 SSH 客户端做同样的事,或者直接 UU 远程连接到 Mac mini 桌面,在那边的浏览器里看。只转发你此刻需要的端口,用完断开。
步骤六:出问题时怎么查
- 先看谁占着端口:
lsof -nP -iTCP -sTCP:LISTEN | grep -E ':30[0-9]{2}',对照ports.txt。 - 孤儿进程:工作目录已经删了但进程还在,用
lsof -p 进程号 | grep cwd看它属于哪个 worktree,确认后再结束。 - 会话测错了服务:让 Claude 在测试前先
curl -s localhost:端口/健康检查路径并打印返回里的分支名或版本号,确认是自己的服务再继续。 - 端口段不够用:把规则从每段 10 个改成 20 个即可,重新生成
.env.local,不需要改代码。
验收清单
- 三个 worktree 同时
npm run dev,三个服务都正常起来,端口与ports.txt一致。 - 故意先占用某个 worktree 的端口,它的服务启动失败并报错,而不是换到别的端口。
- 关掉笔记本、隔一段时间再连回
tmux,所有会话仍在,服务仍在原端口。 - 通过 SSH 转发能在本地打开每个预览,Mac mini 上没有服务监听在外网卡上。
为什么放在一台常开的新加坡 Mac mini 上
并行会话、长时间运行、随时从别的设备接回来查看,这三件事都要求机器一直开着、网络稳定、环境不被别人动。笔记本合盖、换网络、系统更新重启,都会让一晚上的会话前功尽弃。整机独享的 Mac mini 可以按你的端口规则长期配置下去,tmux、worktree、.env.local 都不会被打乱。
Zumac.Ai 在新加坡提供 Mac mini M6 整机独享,接入新加坡家庭宽带,适合把 Claude Code 长期放在一个稳定合规的使用环境里运行。新客户整机租用 ¥999/月;季付、半年付、年付有折扣。交付后可用 UU 远程连接,SSH 按你的安全策略自行开启。Claude 账号与订阅由你自行准备。咨询可 Telegram @singatao。实际延迟取决于你本地的网络线路。
小结
多会话并行的瓶颈往往不在模型,而在端口这种小事。给每个 worktree 分一段固定端口,写进 .env.local 和 CLAUDE.md,禁止自动换端口,用 tmux 管会话、用 SSH 转发看预览,几个 Claude Code 会话就能在新加坡常开 Mac mini 上各干各的、互不干扰。
