资讯动态

Docker部署ZeroTier:远程虚拟局域网与自建中转节点

发布时间:2026/10/27 20:35:07 来源:尧图企业网站定制
远程连回家里的那台小主机一直是我自己折腾环境时最费神的一环。早年用端口映射光是把一堆服务的端口挨个往公网上扔就够让人头大后来改用现成的远程桌面工具延迟和分辨率又总是不尽如人意。直到我把 ZeroTier 这套虚拟组网方案搬进 Docker 里跑才算把家里那几台机器和办公室的机器看起来在同一个机房里这件事做顺手了。这篇内容讲的是如何用 Docker 部署 ZeroTier把分布在不同物理位置的设备拉进一个虚拟局域网让它们能像插在同一台交换机上那样互相访问。适合已经有一台常开的 Linux 小主机、想给自己的设备做点组网自连的读者也适合刚接触容器网络、想看一个容器需要真实网卡时该怎么办的实例的读者。文中会给出可以直接复制的 compose 配置、自建中转节点的完整步骤以及我在这套方案上反复折腾之后攒下的排查思路。1. 为什么我没有在宿主机上直接装而是塞进容器1.1 裸机安装与容器化各自的代价一开始我是直接在一台小主机上装的。下载安装包、执行安装脚本、systemctl enable五分钟就能跑起来体验非常顺。但用久了问题开始冒出来这台机器同时还跑着几个 Docker 服务每次升级系统时ZeroTier 的版本跟着一起动偶尔出现编译出来的内核模块和当前内核不匹配的情况网络就整个断掉而我又不一定在家。排查这类问题最难受的地方在于它坏的时候你正好需要靠它去连那台机器。容器化之后这件事的性质就变了。ZeroTier 的运行版本被我固定在镜像标签上宿主机的系统升级和它完全解耦出问题只需要docker compose down docker compose up -d配置和数据都在挂载出来的目录里重建一次几十秒。我没有追求完全不可变只是把升级这个动作从跟着系统一起被动发生变成了我自己决定什么时候做。代价也很明确容器默认跑在自己的网络命名空间里而 ZeroTier 要在宿主机上创建一张真实的虚拟网卡还要操作路由表和转发规则。这两件事都越过了容器的默认边界所以你会看到后面配置里那些cap_add、devices参数它们不是可选项是硬性前提。把这一点想明白了后面的配置就不难理解了。1.2 虚拟网卡到底是怎么出现在系统里的理解 ZeroTier 的工作层次比记住几条命令重要得多。它做的是二层数据链路层的活儿每个加入网络的节点都会拿到一张虚拟网卡网卡上有自己的 MAC 地址上层协议栈看到它和一块真实网卡没有本质区别。所以两台机器之间不只是能 ping 通 IP同一个虚拟网段里的设备发现、广播报文、组播这些通常在局域网里才有的东西也能正常工作。这个特性在实操里价值很大。比如你在家里内网跑了一套自动发现设备的服务或者某台开发板靠广播找对端把它们挪到 ZeroTier 网络里之后代码一行都不用改。这种看起来在同一台交换机下的体验是三层组网工具给不了的。底层传输上节点之间会尝试用 UDP 直连互相交换候选地址能打通就直接点对点打不通就走中继节点转发。中继分两类一类是公共服务方维护的公共根节点一类是你自己搭的。后者在跨运营商、跨地域的场景里对连接质量影响非常明显这也是后面单独拿一节讲中转节点的原因。对 Docker 来说要让这个机制成立容器里必须能访问/dev/net/tun设备来创建网卡并且要有NET_ADMIN能力去配置地址和路由。少任何一个容器启动时看起来一切正常日志也不报错但ip addr里永远看不到那张zt开头的网卡很多人就是卡在这一步。2. 部署前的三个硬条件TUN、权限、持久化2.1 确认宿主机的 TUN 设备是好的标准发行版内核一般都带了 TUN/TAP 支持但设备节点不一定存在。动手前先看一眼ls -l /dev/net/tun正常应该能看到一个字符设备。如果提示不存在手工建一个mkdir -p /dev/net mknod /dev/net/tun c 10 200 chmod 600 /dev/net/tun再检查内核模块有没有加载过lsmod | grep tun没有输出就modprobe tun手动加载一次。这一步在精简版系统、某些定制化固件上经常缺失而缺了之后容器内的表现是网络加不进去但没有任何明确报错很容易误判成账号问题或者网络 ID 填错了。我的习惯是把这几条命令写进部署前的检查清单宁可多花三十秒也别在排查阶段浪费两小时。2.2 网络模式的选择决定了上限这里有一个必须提前想清楚的选择题容器用 host 网络还是 bridge 网络。选 host容器直接共享宿主机的网络栈ZeroTier 创建的虚拟网卡会直接出现在宿主机的网络接口列表里宿主机上的其他进程、同一内网的其他机器都能直接访问到这个虚拟网段。配置最简单路由也最直观绝大多数人包括我都是这么用的。选 bridge容器有自己的网络命名空间虚拟网卡只存在于容器内部。想让宿主机访问到对端就得手工往宿主机的路由表里加规则指向容器的 IP如果还想让宿主机之外的其他设备通过这台机器进虚拟网段还得配转发。麻烦但换来的是隔离性——你可以让某些只在容器内跑的服务独占这条链路。两种模式的实际差别我列了个对照维度host 模式bridge 模式宿主机能否直接用虚拟网段可以天然支持需要手工加路由容器端口映射参数无效直接忽略与常规容器一致与其他容器共享链路共用宿主机栈可能冲突可独立指定配置复杂度低中高适用场景整机加入虚拟网络单服务独占链路如果你的目标就是让这台机器整个加入虚拟网络不用犹豫直接 host。想折腾隔离、或者宿主机的网络栈已经被其他服务占得很满再考虑 bridge。2.3 身份文件才是真正该保住的东西ZeroTier 每个节点的身份由一对密钥决定存在/var/lib/zerotier-one目录下。最关键的是identity.public和identity.secret两个文件另外networks.d目录记录了你加入过哪些网络moons.d记录了你信任的中转节点。这堆东西不挂载出来容器一重建节点身份就变了在控制台里你得重新授权一遍新成员之前配的网段规则、备注信息全都要重来。我第一次踩这个坑是在一次顺手清理无用镜像和容器的时候docker system prune把容器删了重建之后发现控制台里多了一个陌生成员老成员变成离线状态当时还以为是自己账户出了问题。所以挂载目录这件事优先级和cap_add一样高。目录权限上容器内是以 root 跑的宿主机映射出来的目录让 root 可写就够了。如果你用的是非 root 运行的用户命名空间方案记得提前把目录属主对好否则会出现容器起来了但写不进身份文件的情况日志里会一直重复身份生成的提示。3. 从空白目录到网络互通完整落地流程3.1 两套写法选一个顺手的先给 compose 的写法我目前用的就是这套services: zerotier: image: zerotier/zerotier:1.14.0 container_name: zerotier restart: unless-stopped network_mode: host cap_add: - NET_ADMIN devices: - /dev/net/tun:/dev/net/tun volumes: - ./zerotier-one:/var/lib/zerotier-one environment: - TZAsia/Shanghairestart: unless-stopped这个策略比always更适合常驻服务我手动停掉做维护的时候它不会被自动拉起来。镜像标签我建议钉住具体版本别用latest——这套东西对版本比较敏感某个版本偶尔会带来连接行为上的变化钉住版本等于把变量控制在自己手里需要升级时改一下标签重建就行。不想用 compose 的话等价的docker run是这样的docker run -d \ --name zerotier \ --restart unless-stopped \ --network host \ --cap-add NET_ADMIN \ --device /dev/net/tun:/dev/net/tun \ -v $(pwd)/zerotier-one:/var/lib/zerotier-one \ -e TZAsia/Shanghai \ zerotier/zerotier:1.14.0两个细节值得说--cap-add NET_ADMIN比--privileged好前者只给网络管理这一项能力后者是把整台宿主机交给容器没有必要。--device那一行把宿主机的 TUN 设备映射进去等价于容器内直接持有这个字符设备。这个镜像本身还支持通过环境变量自动加入网络形如ZEROTIER_JOIN_NETWORKSnetwork-id。自动化部署场景下挺方便但对第一次上手的人来说我建议先手动加观察每一步的状态变化搞清楚流程之后再上自动化。3.2 加入网络与成员授权的先后顺序容器起来之后先确认进程是活的docker exec zerotier zerotier-cli info输出类似200 info 8f3a1c2b90 1.14.0 ONLINE。这里的8f3a1c2b90就是这个节点的十位地址格式是十六进制。这个地址后面会用到建议记下来。如果显示的是OFFLINE说明它连不上网络先看容器的出站 UDP 是否被拦、宿主机能不能正常出网。然后加入网络docker exec zerotier zerotier-cli join 你的网络ID网络 ID 是十六位十六进制字符串在控制台创建网络后能看到。执行完会返回200 join OK但这只代表请求已提交不代表已经通了。接下来去控制台在成员列表里找到刚出现的那个十位地址勾选授权同时给它起一个一眼能认出来的名字。顺手在备注里写清是哪台机器、放在哪半年后再来看你会感谢当时的自己。这里有个顺序上的坑没有在控制台授权之前listnetworks的状态会一直显示ACCESS_DENIED看起来像网络故障其实是没授权。反过来授权之后客户端需要几秒到几十秒拉取配置急不得。3.3 验证链路是不是真的通了授权完成后回到命令行查状态docker exec zerotier zerotier-cli listnetworks健康的输出里应该有OK PRIVATE和分配的虚拟 IP比如10.147.x.x/24。接着看网卡# host 模式下直接在宿主机上看 ip addr show | grep -A3 zt应该能看到一张zt开头的接口带着刚分配的地址。到这一步单向的准备工作就算完成了。找另一台已经在同一个网络里的设备互相 ping 一下虚拟 IP。能通说明二层链路建立成功不通往下看排查那一节别急着改配置。还想确认一下路径是直连还是走中转docker exec zerotier zerotier-cli peers输出里会列出所有对端和根节点PATH一栏如果是DIRECT说明点对点直连成功延迟最低如果是RELAY说明走的是中转延迟会明显高一些后面关于中转节点那一节就是为这种情况准备的。4. 自建中转节点连接质量的分水岭4.1 什么情况下值得自己搭一个公共服务方维护的根节点分布在全球若干位置绝大多数时候够用。但有两类场景体感会明显变差一是你的几台设备都在同一片区域但经过公共节点绕了一大圈二是运营商之间的互联质量本来就一般中继一跳下来延迟忽高忽低。判断自己是不是属于这两种情况看peers输出里的RELAY行就够了。我之前有一组设备直连打不通全程靠公共节点中继日常操作还行但一旦传大文件、或者做着需要连续交互的操作那种时快时慢的感觉就很难受。自建一个中转节点之后同一组设备仍然不能直连但通过自己这台机器中转路径变短了体感差别相当明显。搭中转节点的前提很简单一台有公网地址、能开放 UDP 端口的机器配置要求很低几百兆内存就够。它的角色就是在打不通的时候帮忙转发一份报文本身不承载你的业务流量存储转发也是加密的。4.2 在容器里生成并按放中转节点先在一台新机器上可以是容器也可以是裸机做法一样启动一个干净的 ZeroTier 实例不要加入任何网络mkdir -p /opt/zt-moon/zerotier-one docker run -d --name zt-moon \ --network host \ --cap-add NET_ADMIN \ --device /dev/net/tun:/dev/net/tun \ -v /opt/zt-moon/zerotier-one:/var/lib/zerotier-one \ zerotier/zerotier:1.14.0等它初始化完成取到自己的十位地址然后生成配置模板docker exec zt-moon zerotier-idtool initmoon /var/lib/zerotier-one/identity.public /opt/zt-moon/moon.json打开这个文件找到roots数组里的stableEndpoints把公网地址和端口填进去格式是你的公网地址/9993stableEndpoints: [203.0.113.10/9993]接着生成正式的节点文件docker exec zt-moon zerotier-idtool genmoon /opt/zt-moon/moon.json执行完当前目录会多出一个000000xxxxxxxx.moon文件文件名里那串十六进制就是中转节点的地址。把它放进运行目录下的moons.dmkdir -p /opt/zt-moon/zerotier-one/moons.d mv /opt/zt-moon/000000*.moon /opt/zt-moon/zerotier-one/moons.d/ docker restart zt-moon重启后这台机器自己也会使用这个中转配置。注意genmoon那一步命令里如果路径写错生成的文件会落到容器内部容器一删就没了白白折腾一遍。4.3 客户端加入中转与常见失败原因在每一台需要走这个中继的设备上执行docker exec zerotier zerotier-cli orbit 中转节点地址 中转节点地址两个参数看起来重复其实是地址和身份两个位置填同一个值就行。执行后返回200 orbit OK。手动拷贝.moon文件的传统做法也行把文件丢进客户端的moons.d目录再重启效果一样。验证是否生效看peers输出里有没有出现你的中转节点ROLE是MOON。没出现的话按这个顺序查中转节点的 UDP 9993 端口在外网能否访问安全组和本机防火墙都要过一遍客户端执行orbit之后的info输出里节点地址是否和你填的一致中转节点自己的listnetworks是否为空——加入了别的网络的节点不适合再当中转容易混淆客户端和中转节点的时钟偏差过大时偶尔会出现异常顺手date对一下。有一点容易被忽略中转节点并不能创造直连。它只是提供了一条备选路径能直连的时候系统还是优先直连。所以别指望加了中转节点之后延迟归零你只是在打不穿的时候有了兜底。5. 把它当路由器用网段路由与转发5.1 让整个物理网段都变得可达一个常见的需求是家里有一整个192.168.1.0/24的物理网段我不想给每台设备都装客户端只装其中一台当网关让这台机器把整个网段带进虚拟网络。做法是在控制台的网络配置里给这台网关成员的Managed Routes加一条目标填192.168.1.0/24下一跳填这台机器的虚拟 IP。保存后其他成员的路由表里会自动多出这条规则访问这个物理网段就会先发给网关。这一步之后网关机器必须开启内核转发否则收到的包进得来出不去sysctl -w net.ipv4.ip_forward1要持久化就写进配置echo net.ipv4.ip_forward1 /etc/sysctl.d/99-zt-forward.conf sysctl -p /etc/sysctl.d/99-zt-forward.conf转发开了还不够源地址是虚拟网段的包到了物理网段对端设备不知道怎么回。所以还要在这台网关上做地址转换把源地址改成它自己在物理网段的地址。这一步的具体命令取决于你用的防火墙体系核心就是给从虚拟网卡进来的流量做一次源地址改写。写规则的时候注意把网卡名换成你实际的zt开头接口这个接口名每次不一定完全一样用ip addr确认。5.2 容器当网关时的特殊处理如果 ZeroTier 跑在 bridge 模式的容器里同时又想让它当网关那麻烦会多一层转发和改写规则要在容器的网络命名空间里做而路由要加在宿主机上。我的经验是能不用 bridge 就别用host 模式下所有事情都在同一个命名空间里办心智负担小得多。真的要在容器里做可以先确认容器内的转发开关docker exec zerotier sysctl net.ipv4.ip_forward容器默认可能继承宿主机的值也可能不继承取决于运行时配置。确认是 1 之后再谈后面的事。另外容器需要NET_ADMIN之外可能还需要NET_RAW视你用的工具而定。5.3 广播与组播带来的意外前面说过 ZeroTier 是二层网络这意味广播报文会被转发到整个虚拟网络。好处是局域网发现类功能开箱可用坏处是如果你的物理网段里本来就有大量广播挂进来之后虚拟网络里的每台设备都会收到占用宝贵的上行带宽。我在一次把整段家庭网络挂进来之后遇到过这个情况几台移动设备在虚拟网段里莫名耗电变快查下来是广播报文太频繁。解决方法是尽量缩小挂进来的子网范围或者干脆不挂整个网段只给需要的那几台机器单独装客户端。二层网络的便利和它的代价是一体两面的别默认全挂进来最省事。6. 连不上时的分层排查法6.1 从容器状态开始一层一层往外看网络不通的时候最怕东一榔头西一棒子。我的习惯是固定从最里层往外查每一层有一个明确的判断标准哪一层断了就停在哪一层不要跳着看。第一层容器进程。docker ps里状态是不是Updocker logs zerotier里有没有反复重启的痕迹。如果日志里一直在输出身份相关的内容多半是挂载目录权限不对写不进文件。第二层节点在线状态。zerotier-cli info是否ONLINE。OFFLINE的话问题在出站连通性跟虚拟网络本身无关先解决宿主机出网问题。第三层网络状态。zerotier-cli listnetworks里那个网络是不是OK。显示ACCESS_DENIED就是没在控制台授权显示REQUESTING_CONFIGURATION说明授权了但配置没拉下来等一下或者leave后重新join。第四层网卡。ip addr里有没有zt接口地址对不对。没有接口回到第 2 节检查 TUN 设备和能力参数这基本是唯一的可能。第五层IP 连通性。ping 对端虚拟 IP。不通就去看peers判断是路径问题还是对方根本没在线。这套顺序走下来九成问题能在两三分钟内定位到具体某一层比凭直觉改配置高效太多。6.2 打洞失败长什么样典型症状是两侧都显示在线控制台里都是已授权但互相 ping 不通或者 ping 通了延迟高得离谱。这时候peers输出里对端那行大概率是RELAY或者压根没出现。原因通常出在 NAT 类型上。两侧都在对称型 NAT 后面时直连很难建立。这种情况下能做的事不多一是搭中转节点让流量走一条可控的路径二是在条件允许的情况下在一侧的路由器上给这台机器做一个固定的端口映射把 UDP 端口固定下来成功率会明显提高。还有一种更隐蔽的情况某些网络环境会对 UDP 做限速或者直接丢弃而 TCP 一切正常。表现就是网页能开、文件能下但虚拟网络死活建不起来。判断方法是换一个网络环境比如用手机热点试一下同一套配置如果能通那基本可以确认是原环境对 UDP 的处理问题。6.3 一张常见症状对照表症状最可能的原因处理方向容器内看不到 zt 网卡TUN 设备未映射或缺少 NET_ADMIN检查 devices 与 cap_add状态长期 ACCESS_DENIED控制台未授权成员控制台勾选授权每次重建后成员变陌生身份目录未挂载挂载 /var/lib/zerotier-one能 ping 通但传大文件断MTU 不匹配调小虚拟网卡 MTU延迟高、抖走的是中继路径查看 peers考虑自建中转只有本机能通其他机器不行bridge 模式未加路由改 host 模式或补路由挂进来的整个网段访问不了未开转发或未做源地址改写检查 ip_forward 与改写规则表里能 ping 通但传大文件断这一条值得单独说一句。ping 的包很小穿得过任何链路但大包一旦超过路径 MTU 就会被丢弃。表现是 SSH 能连上、敲命令正常一旦scp大文件就卡死。处理方式是把虚拟网卡的 MTU 调小一点一般设到 1400 左右能解决绝大部分情况。这个坑我前后遇到两次第一次查了整整一个晚上第二次五分钟就定位了差别就在于有没有形成这张表。7. 几个我实际踩过的坑和取舍7.1 别在虚拟网络里跑对延迟敏感的东西虚拟组网的本质是叠加在公网之上的隧道无论直连还是中继延迟和抖动都比物理局域网差一个量级。日常的远程终端、文件同步、设备管理都没问题但像实时音视频通话、对时序敏感的采集控制这类场景我试过几次效果始终不理想。这类需求老老实实放在本地或者换专用方案别硬套。我的判断标准是如果这个操作对几百毫秒的抖动会在体感上体现出来就不要放在虚拟网络里。远程敲命令几百毫秒的抖动我几乎感知不到实时对讲那点抖动就变成了你刚才说什么。7.2 授权这件事要有纪律虚拟网络默认是私有模式新成员必须手工授权才能进。这个设计是对的但需要配套的纪律长期不用的成员及时移除不要留着一堆可能有用的离线成员。我见过一个网络里挂着几十个成员谁也说不清哪个对应哪台设备最后只能全部清空重来。另外同一个网络里建议只放信任等级的成员。如果只是拉一台临时设备进来做维护用完就删比先留着吧安全得多。控制台的备注字段很好用把设备位置、用途写清楚长期主义在这里体现得特别明显。7.3 更新镜像前先看一眼变更说明镜像更新这件事我一直比较保守。这套方案的特殊之处在于它同时牵扯到内核能力、网络命名空间和外部控制台三方任何一个环节的小变化都可能表现为更新后连不上了。我的做法是先在测试机器上更新跑一遍完整流程——加网络、ping 对端、传一个大文件——都正常之后再更新生产机器。真出问题时回滚很简单改回原来的镜像标签docker compose up -d身份目录还在成员不用重新授权几十秒就恢复了。这也是我坚持挂载身份目录、坚持钉住版本标签的实际收益不是纸上谈兵的最佳实践。7.4 关于日志与可观测性容器的日志默认不太啰嗦日常够用但排查疑难问题时信息量偏少。可以在控制台的网络配置里调整日志详细程度临时调到更详细的级别复现一次问题看完再调回去。注意长时间开着详细日志会积累不少内容磁盘小的机器要留意。我自己还会做一个很土但有效的动作每次调整配置之后把listnetworks、peers、ip addr三个命令的输出存一份到文件里标上日期。出问题的时候拿来对比能一眼看出是哪次改动引入了变化。这个方法在排查上次还好好的这次就不行了这类问题时特别管用。最后分享一个我一直在用的小习惯把 compose 文件、.env里放网络 ID、以及一份手写的操作笔记放在同一个目录里。笔记里只记三件事——这台机器是什么角色、加过哪些网络、和哪些设备互通过。这套东西跨了机器、跨了控制台、跨了容器三层脑子记不住那么多人名地址写在纸面上的东西才是真正靠得住的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑