保姆级教程:
目录
- 保姆级教程:
-
- 前言
- 一、先搞懂原理(否则坑都不知道怎么来的)
- [二、⚠️ 开始前必须搞清楚的 5 条约束(本文精华)](#二、⚠️ 开始前必须搞清楚的 5 条约束(本文精华))
- [三、服务器端部署(Docker,5 分钟)](#三、服务器端部署(Docker,5 分钟))
-
- [3.1 准备镜像](#3.1 准备镜像)
- [3.2 生成并固定 master secret(⚠️ 存好,永不更改)](#3.2 生成并固定 master secret(⚠️ 存好,永不更改))
- [3.3 启动容器](#3.3 启动容器)
- [3.4 自检](#3.4 自检)
- [四、HTTPS + 域名 + 反向代理(用 Nginx Proxy Manager)](#四、HTTPS + 域名 + 反向代理(用 Nginx Proxy Manager))
- [五、手机端配置(关键:官方 App 的服务器入口是隐藏的!)](#五、手机端配置(关键:官方 App 的服务器入口是隐藏的!))
- 六、终端配对
- [七、日常使用:控制权 & 权限](#七、日常使用:控制权 & 权限)
-
- [7.1 remote mode 与本地切换](#7.1 remote mode 与本地切换)
- [7.2 少被权限弹窗打断](#7.2 少被权限弹窗打断)
- [八、🕳️ 踩坑大全(重点中的重点)](#八、🕳️ 踩坑大全(重点中的重点))
-
- [坑① http 死活连不上](#坑① http 死活连不上)
- [坑② Settings 里找不到填服务器的地方](#坑② Settings 里找不到填服务器的地方)
- [坑③ 配对卡住、收不到消息](#坑③ 配对卡住、收不到消息)
- [坑④ 一直 401「Auth failed - invalid token」,清了又来](#坑④ 一直 401「Auth failed - invalid token」,清了又来)
- [坑⑤ 卸载重装 App 也没用,令牌还是同一个](#坑⑤ 卸载重装 App 也没用,令牌还是同一个)
- [坑⑥ 改了 secret 后全员掉线](#坑⑥ 改了 secret 后全员掉线)
- 九、故障排查速查图
- [十、选型对比:Happy 不是唯一解](#十、选型对比:Happy 不是唯一解)
- 总结
📌 本文所有域名、密钥、IP 均为占位/脱敏 示例,实操时替换为你自己的值。
约定:域名用
happyserver.example.com;密钥用<YOUR_MASTER_SECRET>(64 位随机十六进制)。
前言
把 Claude Code 跑在自己的 Mac / 服务器上,然后用 iPhone 随时随地远程操控 ,是很多人想要的工作流。开源项目 Happy(slopus/happy) 正好干这件事:它给 Claude Code 套了一层端到端加密的中转通道,手机当远程终端。
官方有云端中转,但 国内网络 + 隐私 + 用 API Key / Bedrock 的同学,更适合 自托管 。我在一台 AWS ARM 服务器上把这套完整跑通,中间踩了一堆坑(401、令牌失效、HTTPS、数据卷污染......),这篇把 部署步骤 + 约束条件 + 避坑点 一次性讲透。
本文环境:服务器 = AWS Graviton(ARM / Ubuntu)+ Docker;客户端 = iPhone 官方 App Store 版 Happy;被控端 = Mac 上的 Claude Code。
一、先搞懂原理(否则坑都不知道怎么来的)
Happy 不是远程桌面 / 不是屏幕共享 ,它是一条 加密的「文字消息转发通道」:Claude Code 始终只跑在你的 Mac 上,手机只是个「加密远程键盘 + 屏幕」;中转服务器只搬运密文,读不到你的代码和对话(master secret 只在手机上)。
#mermaid-svg-6kcmQWxGk0MoqDmF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6kcmQWxGk0MoqDmF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6kcmQWxGk0MoqDmF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6kcmQWxGk0MoqDmF .error-icon{fill:#552222;}#mermaid-svg-6kcmQWxGk0MoqDmF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6kcmQWxGk0MoqDmF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6kcmQWxGk0MoqDmF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6kcmQWxGk0MoqDmF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6kcmQWxGk0MoqDmF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6kcmQWxGk0MoqDmF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6kcmQWxGk0MoqDmF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6kcmQWxGk0MoqDmF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6kcmQWxGk0MoqDmF .marker.cross{stroke:#333333;}#mermaid-svg-6kcmQWxGk0MoqDmF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6kcmQWxGk0MoqDmF p{margin:0;}#mermaid-svg-6kcmQWxGk0MoqDmF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6kcmQWxGk0MoqDmF .cluster-label text{fill:#333;}#mermaid-svg-6kcmQWxGk0MoqDmF .cluster-label span{color:#333;}#mermaid-svg-6kcmQWxGk0MoqDmF .cluster-label span p{background-color:transparent;}#mermaid-svg-6kcmQWxGk0MoqDmF .label text,#mermaid-svg-6kcmQWxGk0MoqDmF span{fill:#333;color:#333;}#mermaid-svg-6kcmQWxGk0MoqDmF .node rect,#mermaid-svg-6kcmQWxGk0MoqDmF .node circle,#mermaid-svg-6kcmQWxGk0MoqDmF .node ellipse,#mermaid-svg-6kcmQWxGk0MoqDmF .node polygon,#mermaid-svg-6kcmQWxGk0MoqDmF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6kcmQWxGk0MoqDmF .rough-node .label text,#mermaid-svg-6kcmQWxGk0MoqDmF .node .label text,#mermaid-svg-6kcmQWxGk0MoqDmF .image-shape .label,#mermaid-svg-6kcmQWxGk0MoqDmF .icon-shape .label{text-anchor:middle;}#mermaid-svg-6kcmQWxGk0MoqDmF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6kcmQWxGk0MoqDmF .rough-node .label,#mermaid-svg-6kcmQWxGk0MoqDmF .node .label,#mermaid-svg-6kcmQWxGk0MoqDmF .image-shape .label,#mermaid-svg-6kcmQWxGk0MoqDmF .icon-shape .label{text-align:center;}#mermaid-svg-6kcmQWxGk0MoqDmF .node.clickable{cursor:pointer;}#mermaid-svg-6kcmQWxGk0MoqDmF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6kcmQWxGk0MoqDmF .arrowheadPath{fill:#333333;}#mermaid-svg-6kcmQWxGk0MoqDmF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6kcmQWxGk0MoqDmF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6kcmQWxGk0MoqDmF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6kcmQWxGk0MoqDmF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6kcmQWxGk0MoqDmF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6kcmQWxGk0MoqDmF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6kcmQWxGk0MoqDmF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6kcmQWxGk0MoqDmF .cluster text{fill:#333;}#mermaid-svg-6kcmQWxGk0MoqDmF .cluster span{color:#333;}#mermaid-svg-6kcmQWxGk0MoqDmF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6kcmQWxGk0MoqDmF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6kcmQWxGk0MoqDmF rect.text{fill:none;stroke-width:0;}#mermaid-svg-6kcmQWxGk0MoqDmF .icon-shape,#mermaid-svg-6kcmQWxGk0MoqDmF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6kcmQWxGk0MoqDmF .icon-shape p,#mermaid-svg-6kcmQWxGk0MoqDmF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6kcmQWxGk0MoqDmF .icon-shape .label rect,#mermaid-svg-6kcmQWxGk0MoqDmF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6kcmQWxGk0MoqDmF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6kcmQWxGk0MoqDmF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6kcmQWxGk0MoqDmF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 💻 Mac (happy CLI 包着 claude)
☁️ 你的中转服务器 happy-server
📱 iPhone (Happy App)
WebSocket 端到端加密
WebSocket 端到端加密
输出加密回传
回传密文
你打字 / 看输出
只转发密文
读不到内容
claude 进程
真正干活
记住这张图:后面所有「连不上 / 401」的坑,本质都是这条链路上某一环没对齐。
二、⚠️ 开始前必须搞清楚的 5 条约束(本文精华)
这 5 条没搞清楚,99% 会卡住:
| # | 约束 | 说明 |
|---|---|---|
| 1 | 必须 HTTPS,且是受系统信任的证书 | iOS 的 ATS 机制禁止明文 http ;自签证书除非把 CA 装进 iPhone 并信任,否则也不行。最省事:域名 + Let's Encrypt。 |
| 2 | HANDY_MASTER_SECRET 一旦定下,永远别改 |
它是服务器签发 / 校验登录令牌(JWT)的种子。改了 = 所有已配对账号的令牌全部失效(401)。 |
| 3 | 数据卷 /data 不能「脏」 |
卷里若残留「用旧密钥建的账号」,新密钥校验不过 → 一直 401。换密钥务必清卷重来(见踩坑④)。 |
| 4 | 手机不能带着「云端旧令牌」连自建服务器 | App 之前在官方云建过号,令牌是云端密钥签的,你的服务器认不了 → 401。必须重新建号(见踩坑⑤)。 |
| 5 | 反向代理必须开 WebSocket | 不开 WS,实时消息通道建不起来,表现为「能登录但收不到消息 / 配对卡住」。 |
三、服务器端部署(Docker,5 分钟)
3.1 准备镜像
bash
git clone https://github.com/slopus/happy.git
cd happy
# 仓库根目录自带 standalone Dockerfile(内置 PGlite + 本地存储,无需额外装 PG/Redis)
docker build -t happy-standalone .
3.2 生成并固定 master secret(⚠️ 存好,永不更改)
bash
openssl rand -hex 32
# 输出一串 64 位十六进制,妥善保存,后面用 <YOUR_MASTER_SECRET> 代表它
3.3 启动容器
bash
docker run -d --name happy-server \
-p 13005:3005 \
-e HANDY_MASTER_SECRET=<YOUR_MASTER_SECRET> \
-e PUBLIC_URL=https://happyserver.example.com \
-v happy-data:/data \
--restart unless-stopped \
happy-standalone
关键点:
-p 13005:3005(容器内监听 3005)、PUBLIC_URL填你的 HTTPS 域名、--restart unless-stopped保证重启自恢复。
3.4 自检
bash
curl -I http://127.0.0.1:13005/
# 期望:HTTP 200,页面 Welcome to Happy Server!
curl http://127.0.0.1:13005/health
# 期望:{"status":"ok",...}
四、HTTPS + 域名 + 反向代理(用 Nginx Proxy Manager)
iOS 必须 HTTPS,所以前面挂一层反代,自动签 Let's Encrypt 证书。
#mermaid-svg-cxzfEbdi6joRNKe0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cxzfEbdi6joRNKe0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cxzfEbdi6joRNKe0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cxzfEbdi6joRNKe0 .error-icon{fill:#552222;}#mermaid-svg-cxzfEbdi6joRNKe0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cxzfEbdi6joRNKe0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cxzfEbdi6joRNKe0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cxzfEbdi6joRNKe0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cxzfEbdi6joRNKe0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cxzfEbdi6joRNKe0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cxzfEbdi6joRNKe0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cxzfEbdi6joRNKe0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cxzfEbdi6joRNKe0 .marker.cross{stroke:#333333;}#mermaid-svg-cxzfEbdi6joRNKe0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cxzfEbdi6joRNKe0 p{margin:0;}#mermaid-svg-cxzfEbdi6joRNKe0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cxzfEbdi6joRNKe0 .cluster-label text{fill:#333;}#mermaid-svg-cxzfEbdi6joRNKe0 .cluster-label span{color:#333;}#mermaid-svg-cxzfEbdi6joRNKe0 .cluster-label span p{background-color:transparent;}#mermaid-svg-cxzfEbdi6joRNKe0 .label text,#mermaid-svg-cxzfEbdi6joRNKe0 span{fill:#333;color:#333;}#mermaid-svg-cxzfEbdi6joRNKe0 .node rect,#mermaid-svg-cxzfEbdi6joRNKe0 .node circle,#mermaid-svg-cxzfEbdi6joRNKe0 .node ellipse,#mermaid-svg-cxzfEbdi6joRNKe0 .node polygon,#mermaid-svg-cxzfEbdi6joRNKe0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cxzfEbdi6joRNKe0 .rough-node .label text,#mermaid-svg-cxzfEbdi6joRNKe0 .node .label text,#mermaid-svg-cxzfEbdi6joRNKe0 .image-shape .label,#mermaid-svg-cxzfEbdi6joRNKe0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cxzfEbdi6joRNKe0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cxzfEbdi6joRNKe0 .rough-node .label,#mermaid-svg-cxzfEbdi6joRNKe0 .node .label,#mermaid-svg-cxzfEbdi6joRNKe0 .image-shape .label,#mermaid-svg-cxzfEbdi6joRNKe0 .icon-shape .label{text-align:center;}#mermaid-svg-cxzfEbdi6joRNKe0 .node.clickable{cursor:pointer;}#mermaid-svg-cxzfEbdi6joRNKe0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cxzfEbdi6joRNKe0 .arrowheadPath{fill:#333333;}#mermaid-svg-cxzfEbdi6joRNKe0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cxzfEbdi6joRNKe0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cxzfEbdi6joRNKe0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cxzfEbdi6joRNKe0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cxzfEbdi6joRNKe0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cxzfEbdi6joRNKe0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cxzfEbdi6joRNKe0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cxzfEbdi6joRNKe0 .cluster text{fill:#333;}#mermaid-svg-cxzfEbdi6joRNKe0 .cluster span{color:#333;}#mermaid-svg-cxzfEbdi6joRNKe0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cxzfEbdi6joRNKe0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cxzfEbdi6joRNKe0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cxzfEbdi6joRNKe0 .icon-shape,#mermaid-svg-cxzfEbdi6joRNKe0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cxzfEbdi6joRNKe0 .icon-shape p,#mermaid-svg-cxzfEbdi6joRNKe0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cxzfEbdi6joRNKe0 .icon-shape .label rect,#mermaid-svg-cxzfEbdi6joRNKe0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cxzfEbdi6joRNKe0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cxzfEbdi6joRNKe0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cxzfEbdi6joRNKe0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTPS 443
http 13005
📱 iPhone
Nginx Proxy Manager
Let's Encrypt 证书
✅ WebSocket
happy-server 容器
:3005
- 域名解析:
happyserver.example.com→ 服务器公网 IP。 - NPM 新建 Proxy Host :
- Domain Names:
happyserver.example.com - Forward Hostname/IP:容器 IP 或
127.0.0.1,端口13005 - ✅ 勾选 Websockets Support(约束⑤,必开!)
- SSL 选项卡:Request a new SSL Certificate + Force SSL
- Domain Names:
- 验证(外网):
bash
curl -I https://happyserver.example.com/
# 期望:HTTP/2 200 ... Welcome to Happy Server!
外网能 200,说明 DNS + HTTPS + 反代 + 容器整条链路通了。
五、手机端配置(关键:官方 App 的服务器入口是隐藏的!)
很多人卡在这一步:Settings 里根本找不到填服务器的地方 。因为它藏在 开发者模式 里:
- 打开 Happy App → Settings → 拉到底,连续点「版本号」 进入开发者模式;
- 进入后找 Network 区 → API Endpoint;
- 填入
https://happyserver.example.com,保存。
App 内部逻辑:自定义服务器地址存在
custom-server-url,退出登录也不会被清掉,所以填一次即可。
六、终端配对
Mac 上:
bash
export HAPPY_SERVER_URL=https://happyserver.example.com
happy
终端出二维码 → 手机 Happy 扫码 → 配对成功后,Mac 显示 Remote Mode - Claude Messages,手机即可发消息控制。
配对 / 认证的完整时序如下:
📱 iPhone ☁️ happy-server 💻 Mac (happy CLI) 📱 iPhone ☁️ happy-server 💻 Mac (happy CLI) #mermaid-svg-BkGyDdPDYbESqGtL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BkGyDdPDYbESqGtL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BkGyDdPDYbESqGtL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BkGyDdPDYbESqGtL .error-icon{fill:#552222;}#mermaid-svg-BkGyDdPDYbESqGtL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BkGyDdPDYbESqGtL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BkGyDdPDYbESqGtL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BkGyDdPDYbESqGtL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BkGyDdPDYbESqGtL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BkGyDdPDYbESqGtL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BkGyDdPDYbESqGtL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BkGyDdPDYbESqGtL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BkGyDdPDYbESqGtL .marker.cross{stroke:#333333;}#mermaid-svg-BkGyDdPDYbESqGtL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BkGyDdPDYbESqGtL p{margin:0;}#mermaid-svg-BkGyDdPDYbESqGtL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BkGyDdPDYbESqGtL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-BkGyDdPDYbESqGtL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-BkGyDdPDYbESqGtL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-BkGyDdPDYbESqGtL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-BkGyDdPDYbESqGtL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-BkGyDdPDYbESqGtL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-BkGyDdPDYbESqGtL .sequenceNumber{fill:white;}#mermaid-svg-BkGyDdPDYbESqGtL #sequencenumber{fill:#333;}#mermaid-svg-BkGyDdPDYbESqGtL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-BkGyDdPDYbESqGtL .messageText{fill:#333;stroke:none;}#mermaid-svg-BkGyDdPDYbESqGtL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BkGyDdPDYbESqGtL .labelText,#mermaid-svg-BkGyDdPDYbESqGtL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-BkGyDdPDYbESqGtL .loopText,#mermaid-svg-BkGyDdPDYbESqGtL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-BkGyDdPDYbESqGtL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-BkGyDdPDYbESqGtL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-BkGyDdPDYbESqGtL .noteText,#mermaid-svg-BkGyDdPDYbESqGtL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-BkGyDdPDYbESqGtL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BkGyDdPDYbESqGtL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BkGyDdPDYbESqGtL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BkGyDdPDYbESqGtL .actorPopupMenu{position:absolute;}#mermaid-svg-BkGyDdPDYbESqGtL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-BkGyDdPDYbESqGtL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BkGyDdPDYbESqGtL .actor-man circle,#mermaid-svg-BkGyDdPDYbESqGtL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-BkGyDdPDYbESqGtL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用 HANDY_MASTER_SECRET 校验手机令牌 POST /v1/auth/request (终端公钥) 200,返回待批准 + 出二维码 用手机账号令牌批准该终端 校验通过(200) 配对完成,建立加密 WebSocket 发消息(密文) 转发密文 → 喂给 claude claude 输出(密文) 回传显示
实时盯日志确认:
bash
docker logs -f happy-server
# 配对成功:手机的 /v1/account/profile、/v1/sessions 返回 200(不是 401)
七、日常使用:控制权 & 权限
7.1 remote mode 与本地切换
- 手机发消息 → Mac 自动进 Remote Mode(手机主控);
- 想在 Mac 上敲命令:键盘按任意键 → 切回本地完整交互式 claude;
- 同一个会话,谁动谁接管,一键来回切。
7.2 少被权限弹窗打断
手机端最烦的「限制」其实是权限确认弹窗,启动时放宽即可:
bash
happy -p bypassPermissions # 或 -p acceptEdits
# 注意:不同版本参数可能不同,先 happy --help 确认
真正手机上用不了的,只有需要方向键交互的菜单 (如
/resume列表选择、plan 模式选项),这类操作按键切回 Mac 本地做。
八、🕳️ 踩坑大全(重点中的重点)
坑① http 死活连不上
现象 :export HAPPY_SERVER_URL=http://ip:port 手机一直连不上。
原因 :iOS ATS 禁明文 http。
解决:换 HTTPS 域名(见第四节)。
坑② Settings 里找不到填服务器的地方
原因 :官方 App 把入口藏进了开发者模式。
解决:连点版本号 → Network → API Endpoint(见第五节)。
坑③ 配对卡住、收不到消息
原因 :反代没开 WebSocket。
解决 :NPM Proxy Host 勾上 Websockets Support。
坑④ 一直 401「Auth failed - invalid token」,清了又来
现象 :手机请求都打到服务器了,但每个 /v1/... 都 401,日志 invalid token。
根因之一 :数据卷里残留了用「旧 / 不同 HANDY_MASTER_SECRET」建的账号 ,当前密钥校验不过。
解决(⚠️ 会清空数据,从零开始):
bash
docker rm -f happy-server
docker volume rm happy-data
# 用固定不变的 secret 重新 run(见 3.3)
我本人最后就是靠清卷重建彻底解决 401 的。
坑⑤ 卸载重装 App 也没用,令牌还是同一个
现象 :退出登录 / 卸载重装后,日志里手机发的令牌一字不变 。
根因 :iOS 钥匙串(Keychain)在 App 卸载后不会被删 ,旧云令牌被一直复用。
解决 :App 内 Settings → Account → 底部红色 Log out (这才会清钥匙串),然后在自建服务器上重新建号;光卸载重装无效。
坑⑥ 改了 secret 后全员掉线
原因 :违反约束②,JWT 签名密钥变了。
解决 :永远不要改 HANDY_MASTER_SECRET;非改不可就当全新服务器,清卷 + 所有端重新建号。
九、故障排查速查图
#mermaid-svg-RkkSTzfpzc9Ldhvg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RkkSTzfpzc9Ldhvg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RkkSTzfpzc9Ldhvg .error-icon{fill:#552222;}#mermaid-svg-RkkSTzfpzc9Ldhvg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RkkSTzfpzc9Ldhvg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RkkSTzfpzc9Ldhvg .marker.cross{stroke:#333333;}#mermaid-svg-RkkSTzfpzc9Ldhvg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RkkSTzfpzc9Ldhvg p{margin:0;}#mermaid-svg-RkkSTzfpzc9Ldhvg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RkkSTzfpzc9Ldhvg .cluster-label text{fill:#333;}#mermaid-svg-RkkSTzfpzc9Ldhvg .cluster-label span{color:#333;}#mermaid-svg-RkkSTzfpzc9Ldhvg .cluster-label span p{background-color:transparent;}#mermaid-svg-RkkSTzfpzc9Ldhvg .label text,#mermaid-svg-RkkSTzfpzc9Ldhvg span{fill:#333;color:#333;}#mermaid-svg-RkkSTzfpzc9Ldhvg .node rect,#mermaid-svg-RkkSTzfpzc9Ldhvg .node circle,#mermaid-svg-RkkSTzfpzc9Ldhvg .node ellipse,#mermaid-svg-RkkSTzfpzc9Ldhvg .node polygon,#mermaid-svg-RkkSTzfpzc9Ldhvg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RkkSTzfpzc9Ldhvg .rough-node .label text,#mermaid-svg-RkkSTzfpzc9Ldhvg .node .label text,#mermaid-svg-RkkSTzfpzc9Ldhvg .image-shape .label,#mermaid-svg-RkkSTzfpzc9Ldhvg .icon-shape .label{text-anchor:middle;}#mermaid-svg-RkkSTzfpzc9Ldhvg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RkkSTzfpzc9Ldhvg .rough-node .label,#mermaid-svg-RkkSTzfpzc9Ldhvg .node .label,#mermaid-svg-RkkSTzfpzc9Ldhvg .image-shape .label,#mermaid-svg-RkkSTzfpzc9Ldhvg .icon-shape .label{text-align:center;}#mermaid-svg-RkkSTzfpzc9Ldhvg .node.clickable{cursor:pointer;}#mermaid-svg-RkkSTzfpzc9Ldhvg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RkkSTzfpzc9Ldhvg .arrowheadPath{fill:#333333;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RkkSTzfpzc9Ldhvg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RkkSTzfpzc9Ldhvg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RkkSTzfpzc9Ldhvg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RkkSTzfpzc9Ldhvg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RkkSTzfpzc9Ldhvg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RkkSTzfpzc9Ldhvg .cluster text{fill:#333;}#mermaid-svg-RkkSTzfpzc9Ldhvg .cluster span{color:#333;}#mermaid-svg-RkkSTzfpzc9Ldhvg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RkkSTzfpzc9Ldhvg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RkkSTzfpzc9Ldhvg rect.text{fill:none;stroke-width:0;}#mermaid-svg-RkkSTzfpzc9Ldhvg .icon-shape,#mermaid-svg-RkkSTzfpzc9Ldhvg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RkkSTzfpzc9Ldhvg .icon-shape p,#mermaid-svg-RkkSTzfpzc9Ldhvg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RkkSTzfpzc9Ldhvg .icon-shape .label rect,#mermaid-svg-RkkSTzfpzc9Ldhvg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RkkSTzfpzc9Ldhvg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RkkSTzfpzc9Ldhvg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RkkSTzfpzc9Ldhvg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
没到
到了但 401
是
否
到了 200 但卡住
出问题了
外网 curl
能 200 吗?
查 DNS / 安全组 443 / 反代配置
手机请求
到服务器了吗?
看 docker logs
查 App 的 API Endpoint
是否填对 HTTPS 域名
令牌是不是
云端旧令牌?
App 内 Log out → 重新建号
坑⑤
清数据卷重建
secret 保持不变
坑④⑥
反代开 WebSocket
坑③
| 现象 | 先查 | 解决方向 |
|---|---|---|
| 外网 curl 不通 | DNS / 安全组 / 反代 | 解析、放行 443、NPM 配置 |
Welcome 出不来 |
容器健康 | docker logs、/health |
| 全部 401 | 令牌 / 密钥 / 数据卷 | 坑④⑤⑥ |
| 配对卡住 | WebSocket | NPM 开 WS |
| 手机连不上但浏览器能开 | 已知 Issue #501 | App 自托管支持有未修 bug,优先清卷 + 重新建号 |
十、选型对比:Happy 不是唯一解
| 方案 | 有无 remote mode 限制 | 后端要求 | 适合场景 |
|---|---|---|---|
| Happy(自托管) | 有(手机主控时部分受限) | 任意:API Key / Bedrock / 订阅 | 国内网络、隐私、非订阅后端 |
官方 Remote Control (claude --remote-control) |
无,本地 + 手机同时输入 | 仅 claude.ai 订阅,不支持 API Key / Bedrock | 用订阅且能稳连 claude.ai |
| SSH + tmux(Termius / Blink) | 无,就是原生终端 | 任意 | 要零限制、所有交互命令都能用 |
一句话:用 claude.ai 订阅 + 网络能连 → 官方 Remote Control 更香;用 API Key / Bedrock 或国内网络受限 → Happy 自托管不可替代;要完全零限制 → SSH + tmux。
总结
自托管 Happy 的难点不在部署,而在 那 5 条约束:HTTPS 必须、secret 不可变、数据卷不能脏、手机别带云端旧令牌、反代开 WS。把这几条捋顺,401 和「连不上」基本都能避开。
如果这篇帮你少踩了坑,点个 赞 👍 + 收藏 ⭐ + 关注 。后续会再写 官方 Remote Control 和 SSH + tmux 手机操控 Claude Code 的对比实战。有问题评论区见~
关键词:Happy / Claude Code / 自托管 / iPhone 远程开发 / Docker / Nginx Proxy Manager / 端到端加密