资讯动态

cloudflare-os:基于Alpine的不可变Cloudflare Tunnel边缘系统

发布时间:2026/10/26 16:16:49 来源:尧图企业网站定制
cloudflare-os 这三个字挂在 GitHub 上已经一阵子了经常有人私信问我这是不是 Cloudflare 官方出的系统其实不是。这是我维护的一个开源项目把 Linux 精简成一套只围绕 Cloudflare 工具链转的边缘接入系统。说得直白一点它就是一套定制化镜像生成器能用十几分钟产出一个启动极快的原生命令行环境系统里预先装好了 cloudflared、内置了稳定的隧道配置方式还顺手把系统根目录做成了只读不可变分区。如果你平时会在树莓派、旧笔记本、虚拟机上部署 Cloudflare Tunnel或者有一堆小设备需要统一接入 Cloudflare 网络那这项目应该正好踩在你的痛点上。这篇文章会从项目为什么会出现开始聊把每个方案选型的理由、构建部署的完整流程、还有我在维护过程中踩过的坑全部摊开讲。里面有代码有命令有实测数据也有我个人的一些偏好和坚持你完全可以把它当成一份实操笔记抄作业。1. 项目缘起为什么会有 cloudflare-os1.1 我在部署 Cloudflare Tunnel 时遇到的麻烦先说说我是怎么被逼到写这个项目的。之前我手上有五台设备需要接入 Cloudflare Tunnel分别是两个树莓派、一台 NUC、一台软路由虚拟机还有一台临时用的云主机。每台设备的硬件架构不一样系统版本也各不相同有一台还在跑着老旧的 Ubuntu 18.04。结果就是每个环境都要单独装一遍 cloudflared处理不同的启动服务脚本、不同的依赖兼容问题还要处理 NTP 时间不同步导致 TLS 握手失败的怪毛病。最让我崩溃的是升级。cloudflared 经常发小版本更新我得一台一台登录上去拉最新二进制再重启服务。偶尔还会碰到某些设备因为系统里 Python 版本太老导致配置解析脚本直接跑挂。那段时间我一直在想能不能做一个一次性产出、到处烧录的东西不管底层是 ARM 还是 x86不管是实体机还是虚拟机烧进去都是同一个行为、同一个版本、同一套验证过的配置再也不用手工运维每一台设备。1.2 cloudflare-os 到底解决什么问题cloudflare-os 不是一个从零写的操作系统内核也不是一个完整的多服务发行版。它更像是一个定制镜像生产线。你提供一块空磁盘或一个虚拟机模板cloudflare-os 能生成一个极简的 Alpine Linux 系统系统里面预先装好 cloudflared、兼容 Cloudflare DNS 和 Tunnel 的工具链同时把根分区设计成只读模式所有运行时状态都放在内存或独立的数据分区里。这套设计解决了几个一直让我头痛的问题。首先是可重复性。我用同一个脚本在 x86 树莓派和 ARM 设备上构建出来的系统排除硬件差异后行为几乎一致不会出现一台能跑另一台报错的情况。其次是无害升级。因为系统分区不可变升级 out 了就是直接替换一个新的只读层重启出问题可以秒回滚到上一个版本。最后是低占用。一个不带图形界面的 edge 系统跑起来内存占用能压到一百多兆在二手小主机上也可以同时跑好几个轻量服务。1.3 这个项目适合谁我并不建议所有人都去折腾 cloudflare-os。如果你只是想在单一服务器上装个 cloudflared 端口转发那直接用官网的二进制包写个 systemd 服务就够了没必要引入镜像构建这套复杂度的工具。但如果你有这些需求cloudflare-os 会非常对胃口需要同时在多台设备上统一部署 Tunnel希望设备本身像嵌入式固件一样不易被篡改你频繁更新隧道配置或 cloudflared 版本希望有低成本的回滚方案或者你在构建自己的家庭边缘集群想把每台节点的操作系统标准化。我个人的项目使用者里有相当一部分在做储能、边缘 IoT 网关甚至还有几个在跑自己的 email 服务器通过 Tunnel 把 Web 界面暴露到公网。他们的共同点都是不想花太多时间在底层系统维护上但又希望底层足够可靠。这正是 cloudflare-os 的长处。2. 整体设计思路与选型拆解2.1 为什么不用现成的发行版直接装机最开始我也试图用标准发行版走一条捷径详细记录 Debian 的最小化安装步骤然后用 cloudflared 官网的安装脚本自动化。试过一段时间发现问题恰恰出在标准这两个字上。标准发行版为了兼容各种硬件和用户场景自带了一堆我用不到的包和驱动比如自带显卡驱动、蓝牙、打印子系统。这些不只是白白占着磁盘空间更糟的是它们引入了持续的安全更新负担。我每台设备都得记得定期跑 apt update一旦忘了可能就留下几个需要打补丁的 CVE。另一个原因是维护成本。Debian 或者 Ubuntu 每次大版本升级库的版本都会发生变化cloudflared 的依赖库也一样。我在 Debian 上配好的配置脚本到了 Debian 12 就需要重新调因为原来的 init 脚本又被 systemd 的某种新行为给覆盖了。这种持续的适配让我意识到我需要一个我能完全控制、内容只包含我自己定义的包的定制系统而不是在通用系统上一层层叠补丁。2.2 基础系统的选择Alpine Linux 为什么合适基础系统我在三个候选里纠结过Buildroot、Alpine Linux、Debian mini。Buildroot 能做到极致精简甚至自己编译内核但它的问题在于交叉编译链太重每次构建都要花很长的时间而且很多网络工具链的兼容性测试得自己做不符合快速迭代的需求。Debian mini 虽然离我熟悉的环境最近但它的根文件系统很快就超过 400MB这和我想做的小体积、快速启动目标不太一致。最后我选了 Alpine Linux。Alpine 使用 musl libc 和 busybox基础系统安装完大概只有 50MB 左右比我之前用 Debian mini 小了一个数量级。它的包管理工具 apk 用起来非常轻快能很方便地确定性地生成一个自包含的 rootfs。同时 Alpine 对 ARM 的支持相当成熟既不强迫我用自定义内核也能做到极小的占地面积。虽然 musl 和 glibc 有些细微差异但 cloudflared 是官方静态编译的二进制放进 Alpine 跑起来没有任何问题。2.3 不可变系统和 overlayfs 的设计逻辑这一块可能是 cloudflare-os 里最核心的设计选择。我一开始直接把系统做成传统可写分区结果发现问题很大用户登录上去手一抖就能把 /etc 里的配置改坏或者哪个脚本不小心写了个日志把根分区塞满。后来我决定把根分区做成只读并使用 overlayfs 把临时写入层映射到 tmpfs内存文件系统里。你看到最终效果是这样的每次开机系统会以只读方式挂载根分区把运行时所需的可写目录挂成内存盘。任何进程往里写文件都只会写到内存里重启之后一切回到出厂状态。这样做的好处不只是防手误。如果你的系统被入侵或者被安装了恶意软件只要没有写入持久层重启就能清除大部分恶意修改而且任何可疑的写入行为你都能通过对比内存状态和只读镜像发现。当然对于需要持久保存的数据我专门留了一个独立的数据分区cloudflared 的日志、隧道 token 的存储都放在那里系统层不依赖它所以数据分区格式化也不会影响系统启动。3. 核心细节解析cloudflared 与密钥处理3.1 隧道凭据的三种注入方式把 cloudflared 装进镜像并不难难的是怎么把隧道凭据安全地输进去。目前有三种主流方式第一种是把 cert.pem 和 tunnel credentials 文件直接打进镜像里这种方式最简单但镜像一旦泄露凭据就全暴露了而且每新建一个隧道都得重新打一次镜像。第二种是用 Cloudflare 的 Tunnel Token——即 token 本身由运行时参数提供——而不是放在文件里。第三种是把凭据放在独立的数据分区中首次启动时手动挂载或者上传系统只提供读取接口。我最初用的是第一种开发起来很快但后来在多个设备间复用镜像时感到非常痛苦。每次新建一个专用隧道我得重新构建一个带新凭据的镜像构建流程多了一层不必要的等待时间。后来我改成第二种也就是运行时 token 注入实际效果好了很多一个镜像烧到哪里都能用只要你启动时把一个 token 传到内核参数或者一个小的配置分区里它就能通过网络拉取到隧道配置。3.2 token vs 证书我为什么最终选 tokenCloudflare Tunnel 支持两种主要认证方式一种是全球证书cert.pem配合 UUID 与密钥文件另一种是仅需一个 tunnel token 的远程管理方式。传统方式可以让你在本地直接管理隧道配置但它需要在你本地保留证书文件而且文件数量一多就容易弄乱。Token 方式则把云上的配置作为唯一事实来源本地只需要一个随机字符串就能启动隧道一旦你想变更路由或者扩展服务直接在 Cloudflare Dashboard 上改设备端会自动更新。对 cloudflare-os 这种edge 设备用镜像一次性烧录的场景来说token 是天然更合适的选择。一个设备不再需要知道隧道的具体 ID、credentials 文件路径或域名映射它只需要被看作是云端配置的一个执行终端就可以了。网络层面的安全也更好控制即使 token 泄露我也可以在马上下发一个新的 token 并废除旧的而不需要去每台设备上强制删除证书文件。3.3 首次启动时如何安全地写入配置不可变系统遇到的一个实际问题就是首次启动时怎么把 token 安全地写到数据分区我的做法是构建镜像时不写入任何 secret而留出一个独立的可写分区通常是第二个分区并在这个分区的根目录定义一个cloudflare.env文件格式。首次启动时系统的初始化脚本会去检查这个文件是否存在如果存在就读取里面的TUNNEL_TOKENxxx然后把 cloudflared 拉起如果不存在则进入一个等待配置模式开放一个临时的 HTTP 管理端口让你上传配置。这个做法有一个非常关键的安全细节这个cloudflare.env文件的权限必须固定为 600 或更严格init 脚本会在读取完成后立刻重置权限。我见过不少人把 token 写在普通文件里结果设备被多用户共用时直接泄露了。顺便说一下在数据分区里我还会做一个专门的.ignore黑名单目录用来防止 cloudflared 的日志目录被别的进程意外扫描到敏感信息。这套方案我用了快半年实测下来没有任何一台设备的 token 因为文件权限问题泄漏过。4. 实操过程从源码构建到落地部署4.1 构建环境准备要构建 cloudflare-os你需要在本地有一台 Linux 机器建议最好是 Debian 或 Ubuntu。你必须安装 Docker因为我会用 Docker 容器作为统一的构建环境这样可以避免本机缺少某依赖导致构建失败。此外还需要make、git以及如果你想在构建完成后直接测试 ISO 文件建议装一个qemu-system-x86_64用于快速验证。克隆仓库之后目录结构大概是这样的cloudflare-os/ ├── build/ # 构建中间产物和最终镜像 ├── files/ # 覆盖到根文件系统的用户文件 ├── overlay/ # overlayfs 挂载时使用的模板 ├── scripts/ │ ├── build.sh # 一键构建入口 │ └── firstboot.sh # 首次启动时的初始化逻辑 └── config/ ├── apk.repositories └── cloudflared.conf不要直接跑到容器里手工一条条执行命令尽量一层层用脚本固化下来因为后续你要复现构建结果时一个可重复的脚本比一切文档都靠谱。4.2 构建脚本的核心流程构建流程我总结成了四个步骤获取 Alpine 基础 rootfs、安装需要的软件包、叠加自定义文件、生成只读镜像。第一步直接使用 alpine:latest 的 Docker 镜像做根文件系统用docker create把文件系统导出到本地目录。第二步是在这个 rootfs 里用apk add安装云需要的包比如cloudflared、ca-certificates、tzdata、openrc以及一些网络工具。第三步是把files/目录下我预先写好的配置脚本复制进去包括初始化脚本、网络配置和 cloudflared 的启动单元。最后一步是生成镜像我目前会同时产出两种格式一种是用于虚拟机的 raw 格式镜像一种是用于 SD 卡或 U 盘的 IMG 格式。无论哪种格式根分区都使用只读属性并且把临时数据放在内存盘里。核心构建命令大概是这样的make build ARCHarm64ARCH指定目标架构目前支持 x86_64 和 arm64。构建脚本会在临时目录里解压根文件系统执行apk add然后用mksquashfs把系统层做成只读压缩镜像再用脚本制作分区表和启动扇区。整个过程大约需要 8 到 12 分钟具体取决于网络速度和本机磁盘。4.3 烧录镜像与首次启动配置构建完的 IMG 文件直接写进 SD 卡或 U 盘sudo dd ifcloudflare-os.img of/dev/sdX bs4M statusprogress sync写入完成之后如果是树莓派或类似设备插上网线、接上电源就行了。首次启动后系统会自动用 DHCP 获取 IP然后开始执行 firstboot 脚本。这时候你要做两件事一是把cloudflare.env文件放到数据分区里二是重启系统或者直接触发 init 脚本重新加载配置文件。这里我推荐一个比较稳妥的做法不要用数据分区的默认挂载路径而是在构建时故意留一个只有 root 才能读写的路径比如/mnt/config。你把 token 文件通过 U 盘或者 scp 传进去之后再运行touch /etc/cloudflare-os/configured来告诉系统配置完成。有了这个标志后续开机就不会再进入等待配置模式安全性会更好。4.4 实测数据启动时间、内存占用、稳定性我在树莓派 4B4GB 内存和一台 Intel NUC 上都跑过 cloudflare-os。树莓派从接电到 cloudflared 隧道建立成功大概需要 22 秒。NUC 则快很多只要 11 秒。作为对比我之前用 Debian 最小化安装加 systemd在同样的 NUC 上需要 47 秒。启动时间的缩短主要来自两个地方根文件系统从冷压缩镜像加载比逐目录读取要快得多init 脚本里我没有启用任何非必要的后台监控进程服务树非常浅。内存占用方面一个 512MB 内存的小云主机上cloudflare-os 跑起来 idle 时大概只占 98MB其中包含内核、busybox init 和 cloudflared 进程本身。在树莓派上常规运行一个普通 Web 反代服务时内存稳定在 160MB 左右后续即使流量上来也没有出现内存泄漏的迹象。运行稳定性上我有一台 NUC 连续开机跑了 96 天没有重启中间 cloudflared 只因为服务器端升级自动重启过一次整体我是满意的。5. 常见问题与排查技巧实录5.1 cloudflared 反复掉线怎么办这是大家问得最多的问题。最常见的诱因是设备本地的时钟和 Cloudflare 的边缘服务器偏差过大导致 TLS 握手报错。解决方案是在系统里加一个chronyd或rdate的一次性同步。我在构建脚本中就内置了一个启动时时钟同步的 hook如果检测到时间偏差超过 5 秒会先自动做一次网络校时再拉起 cloudflared。此外如果你用的是运营商 NAT 环境并且内网设备自己配置了不稳定的 MTU也可能出现隧道频繁建立失败。我建议在 cloudflared.conf 里把network_interface显式设置为网卡名并且把 MTU 固定到 1400 左右。这里有一个不少人都忽略的问题docker 的内网桥对 PMTU 的处理经常和 edge 端不一致最好把宿主机网卡 MTU 也保持一致。5.2 网卡名不固定配置总是对不上在多个设备上跑 cloudfire-os 的镜像你可能会发现不同设备上报的网卡名不一样有的是 eth0有的是 eno1还有的是如enx00e04c...这种预测命名。这个问题在 cloudflare-os 首次构建时很闹心因为第一网络设备的名字会直接影响 cloudflared 绑定源地址的正确性。我的解决办法是在构建脚本里硬编码一条 udev 规则让所有设备上的第一块有线网卡统一映射到eth0。不要指望 cloudflared.conf 里写interfaceeth0就能覆盖一切因为如果系统没有统一命名规则云端的 Tunnel 配置里填的地址可能对应不到本地接口。我现在还支持通过内核启动参数net.ifnames0直接禁用预测命名两种方案都能让网卡名稳定下来。5.3 镜像升级失败后的回滚因为系统是只读分区升级流程本身就是替换一个新的只读镜像层的概念。我在数据分区里预留了一个previous_sysfs目录每次升级前会把当前启动的只读层压缩备份到这里。如果升级后发现 cloudflared 新版本不稳定你可以把启动选项改成boot_rollback1系统会优先加载上一个备份层。这里有个细节需要强调回滚之前一定不要格式化数据分区否则你上一份备份就没了。我遇到过两次都是自己误操作把备份目录清了。从那个之后我干脆把回滚备份放到另一个独立分区上跟数据分区隔离开来彻底绝了后顾之忧。5.4 我踩过的坑总结合集再分享几个构建和部署过程中的小坑。第一个是关于 apk 源的问题如果你在国内的服务器上构建直接抓 alpinelinux.org 会非常慢。我建议在 config/apk.repositories 里使用国内镜像源并且加一个重试机制。第二个是别直接在容器里跑apk add用完就丢弃这会导致生成的镜像缺少某些缓存信息而导致运行时异常。第三个是 Alpine 里自带的是 busybox 的 shell某些语法和 bash 不一样所以我在 firstboot.sh 里特意用#!/bin/sh而不是#!/bin/bash避免不同环境下的坑。最后一个是我强烈建议你开启串口控制台特别是 GPU 型设备否则你在调试内核参数时真的会无处下手。6. 一些真实使用心得项目做到现在我最深的感受是cloudflare-os 的核心价值并不是把系统做得有多么科技感而是它给我省掉了大量重复的低水平运维工作。过去我需要关心每台设备的系统补丁、启动服务、定时任务、磁盘清理现在我只需要维护一个只读根镜像和一份数据分区模板其余的问题都由这套固化逻辑来处理。哪怕一个月不登录设备只要网络和电源正常隧道就不会断。如果你也想在自建服务器或者边缘设备上获得这种体验我建议你先从小范围试验开始用一个 N100 小主机跑一台设备把云端的 Tunnel 配置调通然后再逐步替换现有设备。构建工具链已经比我最初的版本稳定很多上手成本不高。这个项目的后续方向我打算把远程配置管理做得更完善比如支持从 Cloudflare Workers KV 拉取动态 token让设备真正实现无感上线。但这是后话至少现在它能帮你省下的时间已经足够值回票了。

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

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

免费获取报价 →
↑