来源:
intranet-tunnel/docs/AGENTS-ARCHIVE.md→## NAS(飞牛 OS)客户端 Docker 部署(2026-09-13)(原文 2629 字符)
NAS(飞牛 OS)客户端 Docker 部署(2026-09-13)
目标:把内网穿透客户端用 Docker 部署到内网 NAS(<内网IP>,飞牛 OS / FNOS), 连上云服务器并端到端测试。结果:一次成功,三条隧道全部打通。
落点与配置
- NAS 路径沿用其既有约定
/vol1/1000/docker/<项目名>(uid 1000 即当前用户);
这与云服务器的 /home/docker/<项目名> 是两套约定,不要混用。
- 交付模板
deploy/docker/client/docker-compose.yml只加了一行volumes
(挂 ./tunnels.json),其余照用。
- 客户端凭据不走面板 API(云上
AUTH_CAPTCHA_ENABLED=true,图形验证码挡住自动化),
用本文档前面记过的直接写库法:容器内取 TOKEN_SALT, openssl dgst -sha256 -hmac 算摘要,INSERT ... ON CONFLICT (client_id) DO UPDATE。 Token 明文只落在 NAS 的 client.env(权限 600),服务器上的临时脚本执行后立即删除。
三条可复用的判据
TUNNELS与TUNNELS_FILE能力不同(文档一度把两者写成同一件事):
TUNNELS 只认 name:type:local_port[:domain] 简写 —— 写 JSON 会报 config: TUNNELS 项 ... 的本地端口非法;而且它的 local_addr 被硬编码为 127.0.0.1 (client/internal/config/config.go 的 ParseInlineTunnels 里 LocalAddr: "127.0.0.1")。 容器里的 127.0.0.1 是容器自己 ⇒ 容器场景只能用 TUNNELS_FILE。 已回写 client/.env.docker、deploy/docker/client/client.env.example 与该目录 README (原先两处的 JSON 示例照抄必然启动失败)。
- 域名按
custom_domain全库查占用,跨客户端会被拒绝:ensureStaticTunnels里
GetTunnelByDomain 命中且 owner.ClientID != clientID 时跳过该静态隧道 (日志 域名已被其它客户端占用,忽略静态隧道)。实测 NAS 上 fn(5666)、 qb(8085)、portainer 已被离线客户端 pc1 占着,所以 NAS 客户端只能用新前缀 (本次用 fnos、moviepilot)。
- 落在泛域内的新隧道,云端零改动:域名按前缀声明、服务端用
TUNNEL_DOMAIN补全,
泛域块复用 ⇒ 边缘配置指纹不变、reload 次数不增,内置反代规则也随登录自动装载 (本次没有触发 POST /api/proxies/reload,也没有重启 tunnel-server)。 改完 tunnels.json 只需 docker compose restart tunnel-client; 而改 client.env 仍必须 up -d --force-recreate(env_file 只在创建容器时读)。
验证方法与工具坑
- 端到端的强判据:不只看状态码,而是把经隧道的响应与NAS 本机直连后端的响应
做 SHA256 逐字节比对(/dashboard、/manifest.json 两条路径全等), 再叠加 X-Proxy-By: intranet-tunnel。这样能排除"公网某处恰好有同名页面"。
- 流量记账查的是
stats表(`tunnel_id, client_id, bytes_in, bytes_out, connections,
recorded_at),tunnels 表没有任何 bytes 列。⚠️ 该表存的是窗口增量而非累计值 (实测同一隧道 out` 539533 → 28250 → 2945 递减),解释"面板实时流量"时按这个口径。
- ⚠️ Workbench CLI 的
exec会破坏含双引号的命令(受控实验:
echo "A B" > /tmp/t2.txt; cat /tmp/t2.txt 只回显 A,换单引号正常)。 对策:命令一律用单引号,SQL 内层用 $$...$$(双引号被吞后 $$ 会在远程 shell 中 展开成 PID,查询直接失败);psql 的 \d 也会因引号被剥离退化成 d 而报语法错。
ssh_upload的本地路径围栏只放行用户 home(本环境会话工作区为空),
给 NAS 传文件前要先暂存到 %USERPROFILE% 下的临时目录,传完即删。
遗留
- NAS 上本来就有飞牛系统自带的 frpc(
/var/apps/frpc,非容器),与本客户端并存、
互不抢端口(本客户端只出站)。日后排查"某域名走哪条通道"时要记得有两套。
- 客户端镜像
1.0.4与服务端1.1.1差 5 个版本(客户端源码自 v1.0.4 起未变更,
所以只是 tag 号旧)。另因服务端自签证书,客户端须 TLS_INSECURE=true。
- 库中残留离线客户端
pc1、mobile-alert-test,其中pc1占着三个指向 NAS 的域名。
来源:
intranet-tunnel/deploy/docker/client/README.md→## 九、实测记录:飞牛 OS(FNOS)NAS(原文 3098 字符)
九、实测记录:飞牛 OS(FNOS)NAS
2026-09-13 在一台飞牛 OS(FNOS,Linux 6.18)NAS 上按本说明部署并跑通。 机型无关,前提是 Linux + Docker Engine 28.5 / Compose v5.1,且当前用户在 docker 组内。
落点与文件:
/vol1/1000/docker/intranet-tunnel-client/ # 沿用 NAS 既有的 /volN/<uid>/docker/<项目> 约定
├── client.env # 权限 600;SERVER_ADDR / CLIENT_ID / TOKEN / TLS_INSECURE
├── docker-compose.yml # 本目录的模板(volumes 里多解开一行 tunnels.json 挂载)
├── tunnels.json # 静态隧道,local_addr 写 host.docker.internal
└── intranet-tunnel-client-images-1.0.4.tar
上面是当时(1.0.4)的落点记录,如实保留。自 1.2.0 起部署目录还应有
init.sh,并会生成data/(配置库),见第三节与第五节。
实测结果:
- 容器
Up (healthy)、RestartCount=0;日志已连接服务端并登录成功、static_tunnels=N。 - 服务端
/healthz的online由 0 变 1。 - 三条隧道端到端可用(响应头均带
X-Proxy-By: intranet-tunnel):
uptime-kuma、飞牛 fnOS 面板(5666)、MoviePilot(40900)。
/healthz的tunnels是「本地静态 + 服务端下发」的合计:本次 3 条本地- 1 条服务端下发 = 4,比
tunnels.json里的条数多(见第七节)。 - 新增隧道不需要改云端任何配置:域名都落在
*.tunnel.sushike.cloud泛域内,
边缘 nginx 按泛域复用,配置与隧道条数解耦。改完 tunnels.json 只需 docker compose restart tunnel-client,不必 down。
两个容易吃亏的地方:
- 域名不能跨客户端复用。服务端按
custom_domain查占用,同域名已属于
另一个客户端时,静态隧道会被忽略(日志: 域名已被其它客户端占用,忽略静态隧道)。要接管旧前缀,先在服务端删掉原记录。
- 改
client.env必须up -d --force-recreate(env_file只在创建容器时读取),
而改 tunnels.json 用 restart 就够——两者要求不同,别互相套用。
自 1.2.0 起,隧道的权威来源是部署目录
data/下的配置库:tunnels.json只在首次建库时播种,之后在 Web 界面上增删改即可, 改完点「重启连接」或等下一次自动重连生效(比改文件 +restart更直接)。
⚠️ 升级 / 重启前先 export DOCKER_CONFIG=/tmp/dsh-docker-config
根因不在 compose,而在家目录的属主。 实测那台 NAS 上 /home/ZYJ 的属主是 root:root(权限 755),普通用户建不了 ~/.docker,于是 compose 报
mkdir /home/ZYJ/.docker: permission denied
并挂起约 60 秒才返回。⚠️ 只读子命令(ps / config / logs)完全不受影响, 所以很容易据此判定「compose 没问题」,把排查方向带偏。
# 临时绕过:把 Docker 的配置目录指到一个一定可写的位置
export DOCKER_CONFIG=/tmp/dsh-docker-config
# 根治(需 root):把家目录还给该用户。
# 用户名换成你部署时用的那个,组名以你的系统为准(这台 NAS 上是 Users)。
sudo chown <用户名>:Users /home/<用户名>
export只对当前 shell 有效:换个终端(或每次新开会话)都要重新设一遍。 想一劳永逸,就走上面那条chown根治。
⚠️ stale 容器名:up 一直报 Conflict,可那个容器「看不见」
compose up 被中断之后(工具超时、但远程进程没被杀掉也会造成同样的结果), dockerd 的名字注册表里可能留下 <旧容器ID前12位>_<服务名> 这种占用,症状很反直觉:
docker inspect/docker rm都报 no such container;docker ps -a里也列不出它;- 但
up一直报
Conflict. The container name "/<旧ID>_tunnel-client" is already in use by container "..."。
处置顺序是先删再 up:
pgrep -af 'docker compose' # 先看有没有残留的 compose 进程
kill -9 <PID> # 有就杀掉
docker rm -f tunnel-client # 再删掉现有容器
docker compose up -d
为什么要「先删」:容器还在时,compose 会先把新容器建成 <旧ID>_<名字> 再改名, 正好撞上那个 stale 名;先把容器删掉,compose 就不必做这次重命名,从而绕开它。
⚠️ 隧道域名有两支泛域,实测只有一支生效
*.t.sushike.cloud 与 *.tunnel.sushike.cloud 是两支不同的泛域, DNS、证书、边缘 nginx 三层各自独立 —— 配了一支不等于另一支可用。 本次 NAS 验证中的实测结果:
<服务名>.t.sushike.cloudTLS 握手失败(SEC_E_WRONG_PRINCIPAL),
且这个名字解析到两个 IP;
- 全部验证最终走
*.tunnel.sushike.cloud成功。
隧道域名的完整形式要与服务端的 TUNNEL_DOMAIN 对应:客户端侧只写前缀时, 由服务端按 TUNNEL_DOMAIN 补全 —— 所以换域名改的是服务端那一处, 不是逐条隧道去改。