
30秒速读:手机连回家里 Mac 终端要绕远路,打开要等好几秒。本篇讲自托管中继的部署与配对方法。
iTerm2 Buddy 是著名开源 macOS 终端 iTerm2 推出的 iPhone 应用:它能在异地通过 iOS 连接回 macOS 上已打开的 iTerm2 标签页,两端实时同步。但默认走 iTerm2 官方的公共中继服务器,连接速度很慢——实测从打开 App 到终端可用,经常要 5 秒以上。
好消息是,中继服务器本身也是开源的(iterm2-companion-relay),完全可以自托管一台,把”绕地球一圈”的链路缩短到直连,手机连 Mac 终端的速度会有质的提升。本文是一份完整可复现的自托管部署教程。

iTerm2 Buddy能做什么:手机远程连接 Mac 终端
iTerm2 Buddy 并不是”iPhone 版终端”——真正的终端会话始终跑在你的 Mac 上,手机 App 只是一个远程镜像。它通过中继服务器在 Mac 与 iPhone 之间转发加密数据,实现异地实时同步操作。
默认情况下,Buddy 使用 iTerm2 官方的公共中继服务器:
- relay1.iterm2.com
- relay2.iterm2.com
公共中继远在海外,国内用户从 5G 网络连回家里的 Mac,要经过”连接、中继、加密”多个步骤,延迟明显。实测从点开手机上的 iTerm2 Buddy 算起,最少也要 5 秒才能进入终端,慢到影响使用。

为什么要自托管中继
既然中继服务器开源了,最直接的优化就是自己搭一台:Mac 和手机都直连你自己的服务器,不再绕远路;甚至在局域网内,也可以避免流量先出国再回来。实测对比:自建中继后,家中 Wi-Fi 下从家中连回家中,连接几乎是秒开,和走公共中继完全没有可比性。
原理很简单:Mac 端和手机端分别主动建立一条 WebSocket 连接到你的中继服务器,中继把两条连接接起来,只做加密数据的转发——中继服务器本身看不到终端内容。连接时用 Ed25519 签名验证配对身份,用 Apple App Attest 验证手机上的应用是否为正版 Buddy;配对记录保存在 SQLite 数据库中,重启中继后依然有效。
自托管中继的工作方式如上文所述:Mac 端与手机端分别连接中继,中继只转发加密数据。
准备条件
- 一台 Linux 服务器(群晖等 NAS 也可,需支持 Docker Compose v2);
- 公网 IP(或能从外部访问到服务器的网络环境),并配置路由器端口转发;
- 一个可修改 DNS 的域名,用于申请 TLS 证书;
- 一张覆盖中继域名、受 iOS / macOS 信任的 TLS 证书;
- Mac 上安装支持 Companion 功能的 iTerm2(本次验证版本 3.7.3),手机上安装 iTerm2 Buddy。
本教程以群晖 NAS 为例,部署目录统一使用 /volume1/docker/iterm2-relay-nas,域名统一使用 relay.example.com(实际部署时全部替换成自己的域名),对外只发布 TCP 18443 端口。

部署步骤
1. 建立目录,下载官方源码
SSH 登录 NAS 后执行(先确认目录没有其他用途):
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
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(避免把证书、密钥送进构建上下文):
upstream/.git
**/node_modules
certs
acme
acme-src
acme-state
.env
*.log
*.zip
*.tar.gz
3. 准备 TLS 证书
已有证书:可复用 NAS 现有证书,但必须确认它覆盖你的中继域名且在有效期内。把证书链和私钥放到:
/volume1/docker/iterm2-relay-nas/certs/fullchain.pem
/volume1/docker/iterm2-relay-nas/certs/privkey.pem
检查证书:
openssl x509 -in certs/fullchain.pem -noout -subject -issuer -dates -ext subjectAltName
注意:Cloudflare Origin CA 证书不适用于本教程的直连方式,必须使用受客户端信任的证书。
没有证书:用 DNSPod 签发 Let’s Encrypt(DNS-01 验证,不需要开放 80/443):
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 [email protected]
--nocron)
设置 DNSPod 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
签发成功后创建 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
然后安装证书并登记续期重载:
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
4. 配置 DNS 和端口转发
将中继域名的 A 记录指向 NAS 所在网络的公网 IPv4(公网地址会变化的话,需要配套 DDNS)。在路由器上设置:公网 TCP 18443 → NAS 内网 IP 的 TCP 18443,有防火墙也要放行该端口。本教程只发布 TCP,不需要转发 UDP。
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 显示运行中。从另一台电脑检查 HTTPS:
curl -I --connect-timeout 10 https://relay.example.com:18443/
根路径返回 HTTP 400 不一定是异常(Relay 的普通请求也需要协议头),这里先确认 TLS 验证无报错、能收到 HTTP 响应即可。
再检查 WebSocket 升级:
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 升级成功。公网测试尽量从外部网络进行,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 重启后的重连。
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。
常见问题
证书报错:检查域名是否在证书 SAN 中、证书是否过期、fullchain.pem 是否包含完整链。用别的域名的证书会导致校验失败。
容器正常,公网仍连不上:检查域名解析、公网地址、端口转发和防火墙。relay healthy 只能证明 NAS 内部服务在运行,不能证明公网入口已打通。
能连接,但配对失败:先核对 .env 和 Mac 设置中的 RELAY_ORIGIN,域名、HTTPS、端口必须一致;确认修改后完全重启了 iTerm2,并重新扫码建立配对。
更新证书后客户端仍看到旧证书:执行 docker compose restart 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 命名卷里,不要随手执行 docker compose down -v,它会删掉卷里的配对数据,之后需要重新配对。
版本说明
中继服务器 iterm2-companion-relay 开源免费,无二进制发行版,需按本教程自行构建部署。本教程已在 x86_64 群晖上验证通过(源码固定 commit f1c3371,其他 NAS 架构尚未验证)。自建 Relay 不会给 Buddy 增加”新建 iTerm2 标签页”等客户端新功能,它只负责更快地连通两端。
下载
下载方式:iTerm2 中继服务器(iterm2-companion-relay)为开源项目,GitHub 仓库提供完整源码,按上文教程自行构建部署即可,无需网盘中转。
软件信息速览
| 版本 | iterm2-companion-relay,源码固定 commit f1c3371 |
| 大小 | 以官网为准 |
| 平台 | Linux 服务器或群晖 NAS,需 Docker Compose v2 |
| 语言 | 以官网为准 |
| 更新日期 | 以官网为准 |
版本对比与推荐
| 版本 | 特点 |
| 固定 commit f1c3371(本教程验证版) | 步骤与配置都按这个版本验证过,复现最稳,推荐使用 |
| 直接拉取最新主干(未固定) | 可能包含未验证的变更,步骤不一定完全对得上 |
推荐:固定 commit f1c3371 的验证版,升级前先看上游变更并备份配对数据库。
安装步骤
- 按原文部署步骤建好目录并拉取固定版本的官方源码。
- 写好部署文件与证书,确认中继域名、端口与证书覆盖范围一致。
- 构建并启动容器,确认中继服务健康、反向代理运行正常。
- 在 Mac 端写入自建中继地址,重新扫码配对后再分别测试 Wi-Fi 与蜂窝网络。
装完必改设置
中继地址设置
入口:Mac 端终端里写入中继地址的两项设置。体感差别:地址指向自建服务器后,手机与 Mac 不再绕远路连接,打开终端的等待时间会明显缩短。
配对与重连设置
入口:Mac 端的 Companion Device Settings 与手机端配对流程。体感差别:重新配对成功后,配对记录保存在服务器数据库里,重启中继后依然有效,不用每次都重配。
证书与端口检查设置
入口:服务器上的证书目录与路由器端口转发设置。体感差别:证书覆盖域名且端口转发打通后,手机在外网也能连回家里的中继,不会卡在连接阶段。
常见问题与注意事项
- 系统要求是什么?需要一台支持 Docker Compose v2 的 Linux 服务器或 NAS、公网访问条件、可改 DNS 的域名与受信任的 TLS 证书,Mac 端需支持 Companion 功能的终端版本,手机端需安装对应应用。
- 容器正常但公网连不上怎么办?先查域名解析、公网地址、端口转发与防火墙,中继健康只能证明 NAS 内部服务在运行,不能证明公网入口已打通。
- 能连接但配对失败怎么办?先核对服务器与 Mac 两边的中继地址是否完全一致,再完全重启终端并重新扫码配对。
- 升级后配对丢了怎么办?配对数据在命名卷里,不要随手执行会删卷的操作,升级前先备份配对数据库,丢了只能重新配对。

关注「软件教程管家」
回复软件名即取即下:网盘下载链接 + 提取码,一步直达,不用再跳转。
本站资源仅供个人学习交流,请勿用于商业用途。软件与资料版权归原作者所有;如有侵权,请通过公众号「软件教程管家」联系我们,我们将在 24 小时内下架处理。查看免责声明 ›







