一、引言

上一篇用 keynetworks/ztncui(ztncui-aio)搭了私有 Planet,建网、授权一直够用。等到要给不同成员分访问权限,ztncui 就不够用了,于是换成 ZTNet。这篇记录更换的过程,以及换完需要注意的地方。

二、正文

(一)先上 Docker Compose

三个服务:postgres、zerotier、ztnet,和我的实际环境一样。建好目录,写一个 .env,密码和密钥别提交进仓库。NEXTAUTH_SECRET 用 openssl rand -hex 32 生成,漏了 Compose 直接不给 ztnet 启动。NEXTAUTH_URL 我填的 http://10.10.2.1:3000;走 HTTPS 反代就换成不带端口的 https:// 域名。

POSTGRES_USER=postgres
POSTGRES_PASSWORD=请替换为高强度数据库密码
POSTGRES_DB=ztnet
NEXTAUTH_URL=http://10.10.2.1:3000
NEXTAUTH_SECRET=<通过 openssl rand -hex 32 生成的随机值>

NEXTAUTH_URL_INTERNAL 供容器在 Compose 网络内访问 ZTNet。ZT_OVERRIDE_LOCAL_CONF=true 会让 ZeroTier 容器重建 local.conf,使 ZT_ALLOW_MANAGEMENT_FROM 指定的管理网段配置生效;数据由绑定挂载目录持久化。br0 是我这边已有的外部 Docker 网络,创建方法不在这儿展开,你按自己的网络名和地址替换即可。

将下面内容保存为 compose.yml,并与 .env 放在同一目录。我的 PostgreSQL 数据目录是 /data/ztnet/postgresql/data;zerotier 与 ztnet 共享 /data/ztnet/zt1 中的 zerotier-one 数据。首次注册的用户会自动成为管理员,之后即可通过 ZTNet 管理既有控制器。

services:
  postgres:
    image: postgres:15.2-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - /data/ztnet/postgresql/data:/var/lib/postgresql/data
    networks:
      app-network:
        ipv4_address: 10.10.2.3

  zerotier:
    image: zyclonite/zerotier:latest
    restart: unless-stopped
    ports:
      - "9993:9993/udp"
    cap_add:
      - NET_ADMIN
      - SYS_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    environment:
      ZT_OVERRIDE_LOCAL_CONF: "true"
      ZT_ALLOW_MANAGEMENT_FROM: 10.10.0.0/16
    volumes:
      - /data/ztnet/zt1:/var/lib/zerotier-one
    networks:
      app-network:
        ipv4_address: 10.10.2.2

  ztnet:
    image: sinamics/ztnet:latest
    restart: unless-stopped
    depends_on:
      - postgres
      - zerotier
    ports:
      - "3000:3000"
    environment:
      POSTGRES_HOST: postgres
      POSTGRES_PORT: "5432"
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
      NEXTAUTH_URL: ${NEXTAUTH_URL}
      NEXTAUTH_URL_INTERNAL: http://ztnet:3000
      NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?请先在 .env 中设置 NEXTAUTH_SECRET}
    volumes:
      - /data/ztnet/zt1:/var/lib/zerotier-one
    networks:
      app-network:
        ipv4_address: 10.10.2.1

networks:
  app-network:
    external: true

这套 Compose 中,ZT_ADDR 和 ZT_SECRET 保持默认值即可:前者为 http://zerotier:9993,后者使用共享目录中的 authtoken.secret。接入自定义或远程的 ZeroTier 控制器时,才需要另行配置它们。替换好变量后执行 docker compose up -d,再访问 NEXTAUTH_URL 指向的地址;未设置 NEXTAUTH_SECRET 时,上述变量展开会直接阻止启动。我用的 zerotier:latest 和 ztnet:latest 都是浮动标签,上生产可以钉一个验证过的版本。

(二)ztncui 和 ZTNet 对比

ztncui 和 ZTNet 是同一类东西:给自托管 ZeroTier 控制器套的 Web UI,都能建网络、管成员。差别在完整度和维护状态:

维度ztncuiZTNet
规则与标签README 明确写了 rules、tags 编辑未实现可直接编辑 flow rules(rulesSource)、capabilities 和 tags
成员协作单一管理账号多用户、组织、个人空间
数据存储不需要独立数据库需要 PostgreSQL
控制器范围面向本机 standalone 控制器也可指向远程控制器,或用 ZeroTier Central API
周边能力官方资料未见对应功能REST API、Webhooks、备份恢复、多语言(含中文)
维护状态仓库提交 73 次,README 仍要求 Node v14提交 2600 余次,仍在更新,官方标注 BETA

表格里的提交数是抓取时的量级,会随时间变化。上一篇的私有 Planet 里,ztncui 建网络、批成员都没问题,真正卡住我的是规则和标签改不动——而这恰好是我要经常动的两个配置,只能绕开控制台手动改。ZTNet 把这两块补进了界面,顺带界面也更合我眼缘。官网和仓库分别在 ztnet.network 和 sinamics/ztnet。

两边界面放一起看更直观。先是 ztncui 的成员管理页(官方截图):

ztncui 的成员管理页:白底表格,成员名、授权、网桥逐项平铺

再是我现在用的 ZTNet 网络详情页(本机 v0.7.18 界面):

ZTNet v0.7.18 的网络详情页:侧栏导航加卡片布局,成员表每行直接挂操作按钮

个人认为相比ZTNet,ztncui更复古。

(三)能不能沿用已有Zerotier控制器

ZTNet 的迁移说明里说:两个 ZeroTier 实例不能合并,它们本质上是各自独立的实体;但 ZTNet 可以配置成管理另一个 ZeroTier 实例,不必用它自带的那个。

所以我直接使用了之前创建的 ZeroTier 控制器实例,zerotier-one 数据目录、identity 和 Planet 都不动;ztnet 与 zerotier 两个容器共用同一份 /data/ztnet/zt1,等于 ZTNet 接在老控制器上做前端,全程只有一个实例,不涉及合并。

按官方文档,接进去之后还有个收尾动作:ZTNet 会把数据库里没有归属的网络标成 unlinked,需要在 Admin => Controller 页面把它们指派给用户,网络才算纳入管理。

动手前我把整个挂载目录备份了一份。换完之后网络、成员、控制器身份、Planet 原样都在,客户端什么都不用改,直接就上来了;

(四)异地灾备考量

我考虑过为这套控制器做异地灾备:在主中心之外维护一套备中心,两边运行心跳检测;主中心故障时,备中心自动启动并接管,主中心恢复后再自动切回。

控制器目录的同步交给 Syncthing 即可覆盖,把 /data/ztnet/zt1 同步到备中心。真正卡住的是 PostgreSQL 数据的实时同步:没有找到简单好用的自托管开源方案,现有的要么过于复杂,要么开源版本限制严格;而且选型还要考虑后续可能引入其他数据库的实时备份,不能只为 pg 挑一个。

切换这一层也没走通。官方客户端不支持运行时更换 planet:zerotier-cli 没有相关子命令,替换 planet 文件后必须重启服务才能生效。开源侧也没有现成方案,ZerotierFix 和官方移动端都只支持静态导入 planet,docker-zerotier-planet 的「多 planet 支持」还停留在路线图上。

也试过把主备两个中心写进同一个 planet:按官方设计,一个 planet 最多可以写 4 个根、每个根最多 32 个端点,客户端理应在主根不可达时自动改用备根,切换和切回都是协议原生行为。实际效果不好,经常连不上,这条路也堵住了。几个条件凑不齐,方案最终搁置,控制器目录和数据库仍以保证可恢复备份为底线。

(五)把 flow rules 当作配置来维护

ZTNet 可以直接编辑 ZeroTier 的 flow rules(rulesSource),并管理 capabilities 与 tags。我的需求是让某些成员只能访问指定 IP 的指定端口,且不能 ping 机器,其余流量默认拒绝——这类规则按成员拆 capability 就能表达,以前在 ztncui 上没法维护,只能绕开控制台手动改,现在直接在界面里完成。规则的语法和逐段拆解见《ZeroTier 03:用实例理解 flow rules》。

(六)选择建议

单控制器下只需建网和成员授权时,ztncui 的轻量和较少依赖仍有吸引力。需要在一个自托管界面中管理多人、多组织空间,并持续维护 rulesSource、capabilities 和 tags 时,ZTNet 更贴合这类工作流。ztncui 少一个数据库;ZTNet 要养一套 PostgreSQL,还得接受 BETA 的运维和升级风险。

三、总结

换到 ZTNet,主要是为了能在界面上直接改 flow rules,顺便审美也是对上了。还是上一篇那套控制器,备份完直接沿用数据目录,网络、成员、身份、Planet 一个没丢,客户端也没重新 join。用完记得把数据库、控制器目录和 rulesSource 一起备份。

四、文献引用

最后修改:2026 年 09 月 14 日
如果觉得我的文章对你有用,请随意赞赏