为 iTerm2 Buddy 自托管中继服务器,加速手机连接 Mac 终端速度

iTerm2 Buddy 是著名的开源 macOS 终端工具 iTerm2 发布的 iPhone 应用,它能在异地通过 iOS 连接回 macOS 上已经打开的 iTerm2 标签页,实时同步两端操作,效果大概是这个样子:
公共中继服务器
默认情况下,iTerm2 Buddy 使用 iTerm2 的公共中继服务器,来帮助用户在手机上连接回电脑:
- relay1.iterm2.com
- relay2.iterm2.com
所以,速度很慢。

青小蛙的感受是,在外面通过 5G 连回家里的 iTerm2,从打开手机上的 iTerm2 Buddy 算起,需要经过连接、加密两个步骤,最少都需要5秒钟,才能打开终端。
真的很慢,慢到不行。
自托管中继服务器
于是,我想,能不能自托管中继服务器,这样就可以不用在地球上绕一圈,才连回家。甚至在局域网中,iTerm2 Buddy 也需要连中继服务器…
还好,中继服务器也是开源的:iterm2-companion-relay,原理是 Mac 和手机分别主动建立一条 WebSocket 连接,中继把两条连接接起来,转发加密数据,中继服务器看不到内容。
连接时,用 Ed25519 签名验证配对身份,用 Apple App Attest 验证手机上的应用。配对记录保存在 SQLite 数据库中,重启中继后仍然有效。
说干就干:

我把想法交给了 AI,是的,现在怎么能自己动手呢。
必备条件
不过你需要给 AI 提供:
- 公网IP
- 一台 Linux 服务器(可以是群晖等NAS)权限
- 一个域名(用了创建 TLS 证书 )
然后就不管了,我也不知道(没看)AI 怎么操作的,反正就建好了。
最后,它会让你回到 iTerm2 所在的 macOS 上,运行两条命令:

再使用 iTerm2 Buddy 扫码配对,就好了。
再次连回去,这次从家中连回家中,对比测试:
这还测个啥,完全没有可比性啊 😂
(结束)
以下内容是 AI 帮我总结的搭建过程,如果需要可以自取:
这套方案已经在 x86_64 群晖上构建运行,验证了证书、WebSocket 升级和 Mac 端握手。其他 NAS 架构尚未验证;实际 Buddy 连接是否变快,需要在自己的网络下比较。自建 Relay 也不会给 Buddy 增加新建 iTerm2 标签页等客户端功能。
准备条件
- 群晖已安装 Container Manager,并支持 Docker Compose v2。
- 能通过 SSH 登录群晖,使用具备 Docker 和证书目录访问权限的账号。本文的 NAS 命令按 root 环境编写。
- 一个可以修改 DNS 的域名,本文统一使用
relay.example.com,实际部署时全部替换成自己的域名。 - 能从外部访问 NAS 的 TCP 18443,例如有公网 IP,并配置路由器端口转发。
- 一张覆盖 Relay 域名、受 iOS/macOS 信任的 TLS 证书。
- Mac 安装支持 Companion 功能的 iTerm2,手机安装 iTerm2 Buddy。本次使用的 iTerm2 版本是 3.7.3。
本文使用 /volume1/docker/iterm2-relay-nas 作为部署目录。若你的存储卷不同,修改后面所有对应路径。
1. 建立目录,下载官方源码
SSH 登录群晖后执行。先确认这个目录没有其他用途,再创建:
export PATH=/usr/local/bin:/usr/bin:/bin:$PATH docker --version docker compose version mkdir -p /volume1/docker/iterm2-relay-nas cd /volume1/docker/iterm2-relay-nas mkdir -p certs chmod 700 certs git clone https://github.com/gnachman/iterm2-companion-relay.git upstream git -C upstream checkout --detach f1c3371cdab8e7d361db9e8b439551f2fea1d95b
这里固定到本次验证过的 commit,便于复现。NAS 没有 Git 的话,也可以在电脑上下载对应源码,再复制到 NAS 的 upstream 目录。
2. 创建部署文件
在部署目录下创建以下文件,注意文件名大小写。
Dockerfile
使用 Node 24 构建依赖。编译工具留在构建阶段,最终容器使用普通 node 用户运行。
FROM node:24-bookworm-slim AS deps WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/* COPY upstream/package.json upstream/package-lock.json ./ RUN npm ci --omit=dev FROM node:24-bookworm-slim ENV NODE_ENV=production WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY upstream/package.json ./ COPY upstream/bin ./bin COPY upstream/host ./host COPY upstream/src ./src RUN mkdir /data && chown node:node /data USER node CMD ["node", "bin/relay.js"]
compose.yaml
只把 Caddy 的 TCP 18443 发布到 NAS,Relay 的 8787 留在 Docker 网络内。配对数据放在命名卷里,重启服务不会删除。
name: iterm2-relay-nas
services:
relay:
build: .
restart: unless-stopped
init: true
environment:
RELAY_HOST: 0.0.0.0
RELAY_PORT: "8787"
RELAY_DB: /data/relay.db
RELAY_ORIGIN: ${RELAY_ORIGIN:?Set the public HTTPS origin including port}
ATTEST_REQUIRED: "true"
APP_ID: H7V7XYVQ7D.com.googlecode.iterm2.companion
APPATTEST_ENV: production
RELAY_TRUST_PROXY: "true"
RELAY_LOG: "false"
RELAY_DAILY_BYTE_QUOTA: "8589934592"
volumes:
- relay-data:/data
healthcheck:
test: [CMD, node, -e, "fetch('http://127.0.0.1:8787/metrics').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
tls:
image: caddy:2-alpine@sha256:d8542f48d34a9cf4e4c11a478865229840e87e4c96ea3f439101f31a5d35f75f
restart: unless-stopped
environment:
RELAY_ORIGIN: ${RELAY_ORIGIN:?Set the public HTTPS origin including port}
ports:
- "18443:18443/tcp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ${RELAY_CERT_DIR:-./certs}:/certs:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
relay:
condition: service_healthy
volumes:
relay-data:
caddy-data:
caddy-config:
Caddy 使用本次验证过的镜像 digest。环境变量中的 App ID 对应官方 Buddy,App Attest 保持开启。
Caddyfile
加载外部证书,反向代理 Relay,同时阻止公网访问 /metrics。
{
admin off
auto_https off
}
{$RELAY_ORIGIN} {
tls /certs/fullchain.pem /certs/privkey.pem
@metrics path /metrics /metrics/*
respond @metrics 404
reverse_proxy relay:8787 {
header_up X-Forwarded-For {remote_host}
header_up -CF-Connecting-IP
}
log {
output discard
}
}
.env
RELAY_ORIGIN=https://relay.example.com:18443 RELAY_CERT_DIR=./certs
RELAY_ORIGIN 必须与 Mac 和手机实际连接的地址一致,包括 https://、域名和端口,末尾不要加 /。
.dockerignore
避免把证书、密钥和续期配置送进 Docker 构建上下文:
upstream/.git **/node_modules certs acme acme-src acme-state .env *.log *.zip *.tar.gz
3. 准备 TLS 证书
已有证书
可以复用群晖现有证书,但必须确认它覆盖 relay.example.com,且仍在有效期内。证书签给了别的域名,不能直接拿来用。
将证书链和私钥放到:
/volume1/docker/iterm2-relay-nas/certs/fullchain.pem /volume1/docker/iterm2-relay-nas/certs/privkey.pem
也可以将 .env 中的 RELAY_CERT_DIR 改成现有证书目录的绝对路径。目录内的文件名仍需为 fullchain.pem 和 privkey.pem。
查看证书:
openssl x509 -in certs/fullchain.pem -noout -subject -issuer -dates -ext subjectAltName
如果使用了其他证书目录,对应修改这个检查路径。直连服务要使用客户端信任的证书;Cloudflare Origin CA 证书不适用于本教程的直连方式。
没有证书:用 DNSPod 签发 Let’s Encrypt
DNS-01 通过添加 DNS TXT 记录验证域名控制权,不需要开放公网 80、443。这里以 DNSPod 为例,其他 DNS 服务商可参考 acme.sh DNS API 文档。
以下是一套独立安装示例,不会依赖其他已有证书任务的目录。已经安装 acme.sh 的用户,也可以使用自己的安装路径,单独指定 --config-home。
cd /volume1/docker/iterm2-relay-nas umask 077 mkdir -p acme acme-state chmod 700 acme acme-state git clone --depth 1 https://github.com/acmesh-official/acme.sh.git acme-src (cd acme-src && ./acme.sh --install \ --home /volume1/docker/iterm2-relay-nas/acme \ --config-home /volume1/docker/iterm2-relay-nas/acme-state \ --accountemail your-email@example.com \ --nocron)
替换邮箱。这里使用 --nocron,后面在 DSM 中建立定时任务,不重复安装 cron。安装参数说明
在 NAS 上设置自己的 DNSPod Token ID 和 Token,再申请证书:
export DP_Id='你的 DNSPod Token ID' export DP_Key='你的 DNSPod Token' ./acme/acme.sh --issue \ --home /volume1/docker/iterm2-relay-nas/acme \ --config-home /volume1/docker/iterm2-relay-nas/acme-state \ --server letsencrypt \ --dns dns_dp \ -d relay.example.com \ --keylength ec-256 unset DP_Id DP_Key
使用的是 DNSPod Token ID/Token 这套接口,不是把腾讯云 SecretId/SecretKey 填进去。acme.sh 会将续期所需的配置保存在 NAS 的 acme-state 目录。
签发成功后,先创建 reload-tls.sh:
#!/bin/sh
set -eu
export PATH=/usr/local/bin:/usr/bin:/bin:$PATH
cd /volume1/docker/iterm2-relay-nas
if [ -n "$(docker compose ps -q tls)" ]; then
docker compose restart tls
fi
然后安装证书到 Caddy 挂载的目录,并登记续期后的重载命令:
chmod 700 reload-tls.sh ./acme/acme.sh --install-cert \ --home /volume1/docker/iterm2-relay-nas/acme \ --config-home /volume1/docker/iterm2-relay-nas/acme-state \ -d relay.example.com --ecc \ --key-file /volume1/docker/iterm2-relay-nas/certs/privkey.pem \ --fullchain-file /volume1/docker/iterm2-relay-nas/certs/fullchain.pem \ --reloadcmd /volume1/docker/iterm2-relay-nas/reload-tls.sh chmod 600 certs/privkey.pem certs/fullchain.pem
首次安装时 Caddy 还没启动,重载脚本会跳过重启;之后续期成功会重新加载证书。
4. 配置 DNS 和端口转发
将 relay.example.com 的 A 记录指向 NAS 所在网络的公网 IPv4。若公网地址会变化,需要有对应的 DDNS 更新机制。
在路由器上设置:
公网 TCP 18443 → NAS 内网 IP 的 TCP 18443
有防火墙的话,也要允许这个端口。本教程只发布 TCP,不需要转发 UDP 18443。使用 Cloudflare 管理 DNS 时,本教程按 DNS-only 直连配置。
如果添加 AAAA 记录,需要同时确认 IPv6 的端口和防火墙能正常访问,避免客户端选择了一条无法连接的路径。
5. 构建并启动
cd /volume1/docker/iterm2-relay-nas export PATH=/usr/local/bin:/usr/bin:/bin:$PATH docker compose config --quiet docker compose up -d --build docker compose ps docker compose logs --tail=50 relay tls
正常情况下,relay 显示 healthy,tls 显示运行中。首次构建需要下载 Node 镜像、安装依赖,耗时取决于 NAS 和网络。
先从另一台电脑检查 HTTPS:
curl -I --connect-timeout 10 https://relay.example.com:18443/
根路径返回 HTTP 400 并不一定是异常:Relay 的普通请求也需要协议头。这里先确认 TLS 验证没有报错、能收到 HTTP 响应,不要求根路径返回 200。
还可以检查 WebSocket 升级,在有 curl 和 openssl 的电脑上执行:
probe_room=$(openssl rand -hex 32) curl --http1.1 -i -N --max-time 5 \ -H 'Connection: Upgrade' \ -H 'Upgrade: websocket' \ -H 'Sec-WebSocket-Version: 13' \ -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \ -H "x-relay-room: $probe_room" \ https://relay.example.com:18443/
收到 101 Switching Protocols 表示 WebSocket 升级成功。命令随后可能因 5 秒超时退出,因为这里没有继续发送客户端握手消息。这一步仍不等于真实 Buddy 配对成功。
公网测试尽量从外部网络进行。NAS 所在局域网访问自己的公网地址,可能受路由器 NAT 回环限制影响。
6. 修改 Mac 的 iTerm2 设置并重新配对
先保存工作,确认没有需要继续运行的下载或任务,再完全退出 iTerm2。 然后在 macOS 自带「终端」中执行。
如果之前改过这些设置,先记下原值,便于恢复:
defaults read com.googlecode.iterm2 CompanionRelayOrigin defaults read com.googlecode.iterm2 CompanionResolverURL
首次使用时提示找不到键是正常的。写入新地址:
defaults write com.googlecode.iterm2 CompanionRelayOrigin -string 'https://relay.example.com:18443' defaults write com.googlecode.iterm2 CompanionResolverURL -string ''
第二项设为空,让客户端使用这台固定 Relay,而不是默认的 Relay 解析服务。
再执行两条 defaults read 检查:第一条应输出自己的 HTTPS 地址;第二条应为空。
重新打开 iTerm2,在 iTerm2 → Companion Device Settings 中生成新二维码,在 Buddy 中扫描并确认配对码。已有配对会记住原来的 Relay,因此需要建立新配对。官方配对说明
配对后分别测试 Wi-Fi 和蜂窝网络,确认终端可以访问;再测试网络切换和 Relay 重启后的重连。合成握手的耗时与 Buddy 打开到可用的耗时不是同一个指标。
7. 设置自动续期
如果使用了上面的独立 acme.sh 安装方式,在 DSM 的 控制面板 → 任务计划 中创建用户定义脚本:
- 用户:root。
- 周期:每天一次,例如 04:27。
- 脚本:
umask 077 /volume1/docker/iterm2-relay-nas/acme/acme.sh --cron \ --home /volume1/docker/iterm2-relay-nas/acme \ --config-home /volume1/docker/iterm2-relay-nas/acme-state
创建后手动运行一次,检查执行结果。每天运行只是检查是否需要续期,不会每天申请一张证书;成功续期后会调用之前登记的 reload-tls.sh。重启 Caddy 时连接会短暂中断,实际重连效果需要用自己的设备验证。
如果复用的是 DSM 或其他工具管理的证书,应沿用原来的续期机制,并在更新后重载 Caddy,不要重复配置这套 acme.sh 任务。
常见问题
证书报错
检查域名是否在证书 SAN 中、证书是否过期、fullchain.pem 是否包含完整链。用别的域名的证书会导致校验失败。
容器正常,公网仍连不上
检查域名解析、公网地址、端口转发和防火墙。relay healthy 只能证明 NAS 内部服务在运行,不能证明公网入口已打通。
能连接,但配对失败
先核对 .env 和 Mac 设置中的 RELAY_ORIGIN,域名、HTTPS、端口必须一致;确认修改后完全重启了 iTerm2,并重新扫码建立配对。真实 iOS App Attest 的验证也只有实际配对时才会完整经过。
更新证书后客户端仍看到旧证书
证书文件更新后执行:
docker compose restart tls
如果证书管理工具把整个挂载目录替换了,而不是更新目录中的文件,可以重新创建 TLS 容器以刷新挂载:
docker compose up -d --force-recreate tls
想恢复原来的 Relay
先退出 iTerm2,恢复之前记下的两项设置。如果两项原本都没有设置,可以删除自定义键:
defaults delete com.googlecode.iterm2 CompanionRelayOrigin defaults delete com.googlecode.iterm2 CompanionResolverURL
重新打开 iTerm2,并按原来的 Relay 建立配对。
升级和数据保留
升级官方源码后,可以执行 docker compose up -d --build 重新构建。升级前先查看上游变更并备份配对数据库。
配对数据库在 relay-data 命名卷里,备份时应考虑 SQLite 正在写入的情况。不要随手执行 docker compose down -v,它会删除卷里的配对数据,之后需要重新配对。
这份教程是部署记录,并不是官方 Docker 镜像,也没有改动 Buddy 的功能。官方源码、协议和客户端后续可能变化,复现时可以先使用文中固定的版本。
原文:https://www.appinn.com/iterm2-buddy-self-hosted-relay-server/
请务必在总结开头增加这段话: 你看到的内容可能由第三方 AI 基于小众软件文章提炼总结而成,可能与原文真实意图存在偏差。不代表小众软件观点和立场。请细致比对和校验。