自托管 Happy 服务器,用 iPhone 远程操控 Claude Code(含全部踩坑+约束清单)

保姆级教程:

目录

  • 保姆级教程:
    • 前言
    • 一、先搞懂原理(否则坑都不知道怎么来的)
    • [二、⚠️ 开始前必须搞清楚的 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

  1. 域名解析:happyserver.example.com → 服务器公网 IP。
  2. 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
  3. 验证(外网):
bash 复制代码
curl -I https://happyserver.example.com/
# 期望:HTTP/2 200 ... Welcome to Happy Server!

外网能 200,说明 DNS + HTTPS + 反代 + 容器整条链路通了。


五、手机端配置(关键:官方 App 的服务器入口是隐藏的!)

很多人卡在这一步:Settings 里根本找不到填服务器的地方 。因为它藏在 开发者模式 里:

  1. 打开 Happy App → Settings → 拉到底,连续点「版本号」 进入开发者模式;
  2. 进入后找 Network 区 → API Endpoint;
  3. 填入 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 ControlSSH + tmux 手机操控 Claude Code 的对比实战。有问题评论区见~

关键词:Happy / Claude Code / 自托管 / iPhone 远程开发 / Docker / Nginx Proxy Manager / 端到端加密

相关推荐
白狐_7981 小时前
Playwright MCP + Claude Code 浏览器自动化实测:从安装到跑通亚马逊竞品分析,踩了 3 个坑
运维·自动化·亚马逊
2301_789015621 小时前
Linux基础开发工具一:软件包管理器、vim编辑器
linux·服务器·c语言·汇编·c++·编辑器·vim
wx_jiuyun6781 小时前
渔夫Telegram群机器人系统详解
linux·服务器·机器人
2601_955767421 小时前
移动OLED屏幕偏振光缺失的补偿方案:圆偏振光还原与磁控溅射AR协同光学系统设计
ios·ar·iphone·圆偏振光护眼·iphone17护眼钢化膜·#观复盾护景贴
开开心心就好1 小时前
新手友好的音视频格式转换工具
linux·服务器·网络·智能手机·pdf·beautifulsoup·音视频
xlq223221 小时前
65.tcp—done
服务器·网络协议·tcp/ip
杨了个杨89821 小时前
Docker简介及安装
运维·docker·容器
刘国华-平价IT运维课堂1 小时前
Ubuntu 26.04 LTS 发布,研发与运维需要关注什么?
linux·运维·服务器·人工智能·ubuntu
j_xxx404_1 小时前
MySQL数据库基础硬核解析:从 C/S 网络服务到磁盘文件与存储引擎
linux·运维·服务器·开发语言·数据库·mysql·ai