把家里的 Mac mini 变成 24 小时在线的 Claude Code 工作站:claudecodeui + SSH 反向隧道,手机浏览器随时接管
家里的 Mac mini 是常年开机的:Claude Code 在上面同时管着五六个项目的工作区,长任务一跑就是几十分钟。问题是人不总在家——在外面想看一眼任务跑到哪了、批准一个权限请求、给它派个新活,怎么办?
目标很具体:在任何有浏览器的设备上打开一个网址,登录,就能看到并操作 Mac mini 上的 Claude Code 会话。不装客户端、不连 VPN,公司电脑和手机都能用。
这套方案上线一周,每天在用。本文按真实决策顺序写:先讲两个选型(Web 界面选什么、通道选什么),再给完整落地配置,然后是实际效果截图和运行数据,最后是已经踩过的坑。
选型一:Web 界面用什么
「浏览器里操作 Claude Code」有四条路,我逐一排除到最后一个:
官方 Web 版(claude.ai/code):最先排除。它跑在 Anthropic 的云沙箱里,操作的是沙箱里的代码副本——而我要的是操作 Mac mini 本地的工作区:本地的仓库、本地的 .env、本地已经登录好的各种 CLI。两者根本不是一个东西。
ttyd + tmux(终端 Web 化):架构上完全可行,把 tmux 会话通过 ttyd 挂到网页上就是一个远程终端。但在手机上用过五分钟就知道问题在哪:触屏敲终端是折磨,方向键、Ctrl 组合键、滚动回看历史输出,每一样都很别扭。它给你的是「一块远程屏幕」,而不是「为移动端设计的操作界面」。
code-server(VS Code Web 版):能用,但绕。它本体是编辑器,Claude Code 还得在它的集成终端里跑——等于套了一层重壳(内存占用以 GB 计)来获得一个更差的终端体验,移动端布局也基本不可用。
claudecodeui(开源,13.5k stars):最终选择。决定性的一条是它的数据模型——
claudecodeui 不维护自己的会话状态,它直接读
~/.claude/projects/下的会话 JSONL 文件。这意味着你在终端里开的 Claude Code 会话,网页上看到的是同一份;反过来在网页上发起的会话,回到电脑上claude --resume也能接着聊。Web 界面只是同一份数据的另一个视图,不产生「网页会话」和「终端会话」两套账。
这一条直接命中我的核心场景:外出时接管的就是出门前在终端里开的那个会话,而不是另起炉灶。其余加分项:
- 移动端优先的 UI:项目列表抽屉、会话流、底部输入框,是按手机屏幕设计的,不是桌面布局硬缩
- 自带认证:JWT 登录 + bcrypt 密码,登录接口天然适合在 nginx 层再加限流(后文有配置)
- 不止聊天:内置文件浏览器(带编辑)、git 面板、真·终端(WebSocket),聊天解决不了的时候还有兜底
- 开源可改:这点一周内就兑现了——上线第二天撞上一个上游 bug,fork 下来自己修了(见「踩过的坑」)
实测足迹很轻:Node 单进程,RSS 稳定在 170MB 左右,对一台还要跑正经任务的 Mac mini 来说无感。
选型二:为什么 SSH 反向隧道,而不是 Tailscale
家庭宽带没有公网 IP,这是前提。「从外面够到家里的机器」教科书答案是 Tailscale,但对这个具体需求——任何浏览器可达的 Web 入口——SSH 反向隧道在四个维度上更合适:
1. 访问端零安装(决定性)。 Tailscale 的模型是「设备入网」:每台访问设备都要装客户端、登录账号。手机可以装,公司电脑往往不让装,临时借用的设备更不可能。SSH 隧道 + nginx 的产出是一个普通 HTTPS 网址,任何有浏览器的东西都能用。「零安装」不是锦上添花,它决定了这个入口在多少真实场景下可用。
2. 链路自己可控。 Tailscale 靠 NAT 打洞直连,打不通就回落 DERP 中继——国内运营商级 NAT 加上对端网络限制,打洞成功率并不乐观,回落后流量绕 Tailscale 的海外节点,延迟和稳定性都不受你控制。SSH 隧道的链路是固定且自选的:Mac mini → 你挑的 VPS → 你。线路好不好买 VPS 之前就知道,出问题 ssh -v 一眼看穿,没有打洞、中继、第三方控制面这些「今天怎么又慢了」的玄学层。
3. 暴露面是一个端口,不是一台整机。 Tailscale 入网后整台机器对网内可达(所有监听端口),信任边界是「账号安全 + ACL 写对」。反向隧道暴露的是恰好一个端口上的恰好一个服务,且该端口在 VPS 上只监听回环——公网唯一能碰的是 nginx 上那一个 URL。审计是「三道门锁好了没」,不是「整张网的 ACL 有没有漏」。
4. 复用已有资产。 我的 VPS 上 nginx、证书自动续期、日志体系都是现成的,这套方案只新增一个 vhost 和一条隧道;Tailscale 则要引入新账号体系 + 每台设备一个常驻 agent + 一套 ACL 配置语言。
反过来,什么时候该选 Tailscale:只从自己的设备访问且每台都能装客户端;要的是整机能力(远程桌面、SMB、直接 SSH);没有 VPS 也不想维护。以及一条要诚实说的——本方案的入口是公网可达的,安全完全依赖认证那几道门;Tailscale 的服务只在私有网络内可见,天然少一类风险。Web 界面没有可靠认证的话,不要用本方案裸奔。
顺带一句 Cloudflare Tunnel:思路与本方案相同(从内网拨出、边缘做入口)且免费,没有 VPS 的话是最好替代;有 VPS 的话,自己的 nginx 在超时策略、限流、日志上都更可控。
架构
[Mac mini 家中] [VPS 公网] [任意浏览器]
claudecodeui SSH 反向隧道 nginx (TLS+限流)
127.0.0.1:18300 ────────► 127.0.0.1:18300 ────────► https://code.example.com
(只监听回环) ssh -R (只监听回环) 反向代理
三个要点:Web 界面只监听回环(局域网都碰不到);隧道由 Mac mini 主动拨出(所以家里不需要公网 IP、不需要改光猫);VPS 上的隧道落点同样只监听回环,公网唯一入口是 nginx。
以下配置中域名、IP、用户名均为占位符。
落地:Mac mini 侧
claudecodeui 安装与守护
git clone https://github.com/siteboon/claudecodeui.git ~/tools/claudecodeui
cd ~/tools/claudecodeui
npm install && npm run build
.env 三行,关键是绑回环:
SERVER_PORT=18300
HOST=127.0.0.1
NODE_ENV=production
用 launchd 做成开机自起的常驻服务,~/Library/LaunchAgents/com.example.claudecodeui.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.example.claudecodeui</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/node</string>
<string>dist-server/server/index.js</string>
</array>
<key>WorkingDirectory</key><string>/Users/YOUR_USER/tools/claudecodeui</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key><string>/Users/YOUR_USER</string>
</dict>
<key>KeepAlive</key><true/>
<key>RunAtLoad</key><true/>
<key>StandardOutPath</key><string>/Users/YOUR_USER/tools/logs/claudecodeui/server.log</string>
<key>StandardErrorPath</key><string>/Users/YOUR_USER/tools/logs/claudecodeui/server.err.log</string>
</dict>
</plist>
一个容易踩的点:launchd 环境里没有你的 shell 配置,PATH 和 HOME 必须显式写,否则 node 找不到、claude CLI 也找不到。
反向隧道,同样交给 launchd
macOS 上不需要 autossh,launchd 的 KeepAlive 就是最好的守护。~/Library/LaunchAgents/com.example.ccui-tunnel.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.example.ccui-tunnel</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>-N</string>
<string>-o</string><string>ExitOnForwardFailure=yes</string>
<string>-o</string><string>ServerAliveInterval=30</string>
<string>-o</string><string>ServerAliveCountMax=3</string>
<string>-R</string><string>127.0.0.1:18300:127.0.0.1:18300</string>
<string>tunnel@203.0.113.10</string>
</array>
<key>KeepAlive</key><true/>
<key>RunAtLoad</key><true/>
<key>ThrottleInterval</key><integer>15</integer>
</dict>
</plist>
参数为什么这么配:
-R 127.0.0.1:18300:127.0.0.1:18300:两端端口取一致省心;显式写127.0.0.1:前缀,确保 VPS 上的落点不对公网监听ExitOnForwardFailure=yes:转发建立失败时让 ssh 干脆退出,而不是揣着一条废连接假装活着——退出了 launchd 才知道要拉起重连。不配这条,断线后隧道假活,你在外面干着急ServerAliveInterval=30+CountMax=3:90 秒内判定断线退出,宽带闪断、光猫重拨靠它触发自愈ThrottleInterval=15:断网期间别疯狂重试
密钥注意:launchd 环境下 ssh 走默认路径找 ~/.ssh/id_ed25519,且不能有 passphrase(或提前进 Keychain),否则无人值守时连不上。
别让 Mac mini 睡着
sudo pmset -a sleep 0 disksleep 0 # 机器永不睡眠(显示器随意)
sudo pmset -a autorestart 1 # 断电恢复后自动开机
落地:VPS 侧
nginx:TLS + WebSocket + 登录限流
这是线上真实在跑的配置(域名脱敏)。两个容易漏的点都标了注释——WebSocket 升级头和长连接超时:
map $http_upgrade $connection_upgrade {
default upgrade;
"" close;
}
# 登录接口防爆破:每 IP 每分钟 10 次
limit_req_zone $remote_addr zone=ccui_auth:1m rate=10r/m;
server {
listen 443 ssl;
server_name code.example.com;
ssl_certificate /etc/letsencrypt/live/code.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/code.example.com/privkey.pem;
# 登录接口单独限流——入口在公网上,这是必须的
location /api/auth/login {
limit_req zone=ccui_auth burst=5 nodelay;
proxy_pass http://127.0.0.1:18300;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
proxy_pass http://127.0.0.1:18300;
proxy_http_version 1.1;
# 聊天流和内置终端都是 WebSocket,这两行不能少
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
# 长会话别被 60s 默认超时掐断
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
client_max_body_size 50m;
}
}
证书 certbot 正常签发。如果你的 443 已被其他服务占用(比如跑着 SNI 分流),把 vhost 挂到内部端口、由分流层转发即可,架构不变。
收紧隧道账号权限
我图省事复用了已有的运维账号。如果新开专用账号(更推荐),/etc/ssh/sshd_config 加一段 Match 把权限钉死:
Match User tunnel
AllowTcpForwarding remote
PermitListen 127.0.0.1:18300
PermitTTY no
X11Forwarding no
AllowAgentForwarding no
配合 nologin shell + 仅密钥登录,就算密钥泄露,对方拿到的也只是「能往你 VPS 回环的 18300 挂个转发」的权限。
验证
# VPS 上:隧道落点必须只在回环监听
ss -tlnp | grep 18300 # 应为 127.0.0.1:18300,绝不能是 0.0.0.0
# 公网直连端口必须不通
curl -m 5 http://203.0.113.10:18300/ # 应超时/拒绝
实际效果
手机浏览器打开域名,登录页(自带 JWT 认证,nginx 层还有限流):

登录后左侧抽屉是项目列表——这里列出的就是 ~/.claude/projects 里的真实项目,终端里用过 Claude Code 的目录自动出现,带会话数和星标:

点进项目打开会话,消息流、工具调用(Read/Bash 折叠展示)、底部输入框和模型选择,在手机上是完整可操作的——看状态、批权限、发指令都在这个界面完成:

桌面浏览器则是完整双栏布局,右上还能切换文件浏览器、git 面板和内置终端:

一周运行数据(真实读数,不是估计):
| 指标 | 实测 |
|---|---|
| 隧道稳定性 | 单条 ssh 进程连续在线 5 天,错误日志 0 行,零人工干预 |
| claudecodeui 内存 | RSS ≈ 170MB(连续运行 3.5 天后读数) |
| 断线自愈 | 心跳 90s 判死 + launchd 秒级拉起,感知上「两分钟内自动恢复」 |
| 手机端实际用途 | 看任务进度、批准权限请求、发后续指令;重活回电脑 |
踩过的坑
<synthetic> 占位符当成模型名显示。 上线第二天遇到:某个会话在 API 报错后,claudecodeui 的会话列表把模型名显示成了 <synthetic>。查下来是 Claude Code 的行为——它会往会话 JSONL 里写入本地合成的占位行(API 错误占位等),这些行的 model 字段是 "<synthetic>",而 claudecodeui 扫描会话模型时不加区分地采信了。fork 之后修掉(尖括号包裹的值一律视为占位符跳过),三个 commit 解决。这也是「开源可改」在选型里值一分的原因:闭源工具遇到这种碍眼 bug 只能等。
launchd 的环境是干净的。 两个 plist 都因此栽过小跟头:不写 PATH 时 node/claude 找不到;ssh 密钥带 passphrase 时无人值守拉不起来。规律是一条:launchd 里跑的东西,所有依赖显式声明,别指望 shell 配置。
WebSocket 头漏配的症状很迷惑。 页面能打开、能登录、项目列表也有,但聊天发不出去、终端连不上——因为 HTTP 请求都正常,只有 WebSocket 升级被 nginx 吃了。如果你看到「界面正常但一切实时功能都死着」,先查 Upgrade/Connection 两个头。
安全清单
上线前逐项过:
- Mac mini 的 Web 界面只监听
127.0.0.1 - VPS 隧道落点只监听
127.0.0.1(ss -tlnp验证) - 公网直连隧道端口不通,只有 443 经 nginx 可达
- nginx 对登录接口限流(本方案入口在公网,防爆破不是可选项)
- 隧道账号最小权限(nologin + Match 限制 + 仅密钥)
-
ExitOnForwardFailure=yes已配置 - claudecodeui 密码强度足够且不复用
常见问题 FAQ
家里宽带没有公网 IP,这方案能用吗?
能,这正是反向隧道的意义。连接方向是 Mac mini 主动拨出到 VPS,家里这头只要能上网就行——公网 IP、端口映射、光猫桥接统统不需要。
网页上看到的会话和终端里的是同一份吗?
是。claudecodeui 直接读 ~/.claude/projects/ 的会话文件,不维护独立状态。终端开的会话网页能接着看,网页发起的会话回电脑 claude --resume 也在。
隧道断了需要人工干预吗?
不需要。ServerAliveInterval 心跳 90 秒内发现断线,ExitOnForwardFailure 保证 ssh 干脆退出,launchd KeepAlive 随即拉起。实测五天宽带闪断数次,全部自愈,错误日志零行。
为什么不直接在 VPS 上跑 Claude Code?
工作区在 Mac mini 上:本地仓库、本地凭据、登录好的各种 CLI。而且 mini 的算力内存是买断的,VPS 同规格月月花钱。这个架构里 VPS 只当「门牌」,最便宜的机器都够。
和 Cloudflare Tunnel 比呢?
同思路(内网拨出 + 边缘入口)且免费,没有 VPS 时是最佳替代。差异:流量过 Cloudflare 边缘(多一个第三方,部分地区速度不稳)、WebSocket 长连接受其超时策略约束。有自己的 nginx 就更可控。
手机上真能干活吗?
分场景。看进度、批权限、发指令、读 diff:完全够用,这占外出场景的九成。大段写代码:不现实,屏幕和键盘摆在那。实际模式是「移动端监工派活,重活回电脑」。