很多人搜索 cloudflare-os,是想搞清楚 Cloudflare 这么大一家公司,跑在遍布全球的边缘节点的那些服务器,用的到底是什么操作系统。我也好奇过,翻了不少工程博客。后来我意识到,比起"他们用什么",更值得自己动手的是另一件事:能不能把日常工作依赖的 Cloudflare 开发环境,也固化成一套开箱即用的专用系统。这个念头,最后变成了 cloudflare-os 这个项目。
它是一个基于 Alpine Linux 的最小化系统镜像,内置 Node.js LTS、wrangler、本地 Worker 模拟器、DNS 和缓存管理脚本,还有一套统一的命令入口 cfo。它可以做成 ISO 启动盘,也可以作为虚拟机镜像或容器基础镜像使用。适合经常操作 Workers 和 Pages 的开发者、需要给团队统一 Cloudflare 工具链的运维同学,以及所有喜欢把开发环境固化成"一个确定状态"的折腾党。这篇就把我的设计思路、构建流程和踩过的坑完整写出来,照着一路做,你也能得到一个属于自己的 cloudflare-os。
1. 为什么放着现成发行版不用,偏要做一个"云厂商专用系统"
1.1 先列需求,再谈实现
做任何定制系统,我第一件事都不是选发行版,而是先问自己:这套系统开机之后到底要能干什么?cloudflare-os 的需求清单写得很具体:
- 开机之后可以直接执行 wrangler deploy,不需要再装任何依赖;
- 自带一组管理 Cloudflare 资源的命令行脚本:查 DNS 记录、加解析、清缓存、看 zone 状态;
- 有完整的本地开发环境,改完代码不用上传就能提前看到效果;
- 系统本体足够小,丢了能快速重建,不心疼。
这份清单决定了系统的边界。它不是一个通用发行版,不需要图形界面、不需要浏览器、不需要办公软件,甚至大部分硬件驱动都可以不要。整个系统只为一件事情服务:让任何一台机器在开机后都能立刻进入"预配置好的 Cloudflare 开发与运维环境"。我见过不少人做定制镜像时什么都想往里塞,最后得到一个比原版发行版还臃肿的怪物。先把边界划清楚,后面每一步都轻松。
1.2 在普通发行版上直接装工具,问题出在哪
最初我确实在 Ubuntu Server 上直接干活,跑下来有几件事特别别扭。
第一是环境漂移。今天 apt 装的是 Node 20,过两个月一个 apt upgrade 就升到 22,而 wrangler 和它的依赖对 Node 版本是有要求的,行为马上就变了。项目一多,每台机器一个样,出了问题先花半小时确认环境,而不是直接查业务逻辑。
第二是系统太重。Ubuntu 的云镜像一般 600MB 起步,而你可能只为了一两个工具用它,大部分依赖根本不会碰。每次启动还要载入一堆无用的内核模块,这种浪费在个人开发机上还能忍,在 CI 里就是实打实的构建时间成本。
第三是不可复现。在一台机器上把环境调好,想原封不动搬到另一台,要么打快照,要么照着文档再走一遍。快照带着历史包袱,文档则一定会漏掉某条命令。这类问题在小团队里会放大成事故——两个同事跑同一个部署脚本,结果一个成功一个失败,最后发现是 Node 小版本不一样。
1.3 为什么基底选 Alpine,而不是 Debian 或 Fedora
选 Alpine 有三个实际原因。
一是体积。Alpine 的基础系统只有几 MB,完整跑起来加上运行时也比其他发行版的最小安装小一个数量级。我最后做出来的镜像整体不到 400MB,传到共享存储、从网上下载都没压力。
二是包管理方式。apk 的依赖模型比 apt 和 yum 简单直接,定制 rootfs 的时候基本能预期装进去什么、不会偷偷多出什么包。对需要手工拼装系统的人来说,这种可预期性太重要了。
三是 musl libc 带来的可预测性。musl 让跨环境复制变得容易,运行时行为在大多数场景下比 glibc 系统更可预期。当然它也伴随一些兼容性代价,这个坑我在后面专门讲。
最后还有一点私心:Alpine 默认的 init 是 OpenRC,服务脚本就是纯 POSIX shell,读起来和改起来都没有门槛,不需要去理解 systemd 里那些 target、mask 的概念。
如果你所在团队指定了 Debian 系作为标准平台,也不必硬换基底。这套"把工具链定制成最小镜像"的思路完全可以平移过去,区别主要在包管理和最终的体积优化上。
2. 镜像的三层设计:从内核到 cfo 命令
定制系统最忌讳把所有东西一股脑塞进去。我给 cloudflare-os 的设计参考了一个原则:每一层只解决一类问题,层与层之间尽量不互相纠缠。最后呈现出来的是一个典型的三层结构。
2.1 第一层:基础运行时
这一层是最小可用系统的骨架,包含 Linux 内核、busybox 核心命令、OpenRC 初始化、标准 C 库、OpenSSL、CA 证书、DNS 工具和网络配置工具。
这里有一件事必须强调:CA 证书不是可选项。最小系统为了省空间经常把 ca-certificates 砍掉,结果跑任何 HTTPS 请求都会报证书错误。cloudflare-os 里几乎所有操作都要访问 Cloudflare API,这个包属于硬依赖,省了它后面全是麻烦。
2.2 第二层:Cloudflare 工具链
第二层是灵魂,核心装了四样东西:
- Node.js LTS 加 npm:运行时基础,本地开发和依赖管理都靠它。
- wrangler:官方 CLI,负责 Workers 的本地开发、预览和部署。
- Miniflare:本地 Workers 模拟器,能在不上传的情况下验证 Worker 逻辑、KV 绑定和缓存行为。新版 wrangler 的 dev 命令虽然内置了它,但我仍然会显式安装,这样在脚本和 CI 场景下更可控。
- curl 和 jq:API 调用与 JSON 解析的黄金组合,所有自动化脚本的地基。
之所以不把更多框架塞进来,是因为这几样已经覆盖了从写代码、测代码、发布到调整运维配置的完整链路,再加东西只会增加维护成本。脚手架类的工具(比如用 Hono 初始化项目)我放在用户脚本层,让每个人按自己的偏好来,而不是固化进系统。
各层的大致体积可以参考这个表:
| 层级 | 包含内容 | 预估体积 |
|---|---|---|
| 基础运行时 | 内核、busybox、OpenSSL、CA 证书、网络工具 | 约 40MB |
| Cloudflare 工具链 | Node.js LTS、npm、wrangler、Miniflare、jq | 约 330MB |
| 自动化脚本层 | cfo 命令、部署模板、DNS/缓存管理脚本 | 约 20KB |
2.3 第三层:自动化脚本层
系统里真正值钱的不是某个工具本身,而是一套名为 cfo 的命令行封装(cfo 是 cloudflare-os 的缩写,也是整套环境的使用入口)。它把高频操作收敛成几个子命令:
- cfo init:检查并输出环境信息,包括 wrangler 版本、Node 版本、API Token 是否有权限。
- cfo deploy:在指定目录下构建并发布 Worker,发布前先跑本地测试。
- cfo dns:DNS 记录的增删改查,支持从 JSON 文件批量同步。
- cfo purge:清理指定 zone 或 URL 前缀的缓存。
这一层把"会用 Cloudflare"这件事简化成"会用这几条命令"。团队里任何人都不需要去啃完整的 API 文档,就能完成日常发布和排障。把高频操作做成带语义的命令,这套系统才真正算"可用"而不是"能启动"。
3. 从 rootfs 到可引导镜像:构建全流程与三种运行形态
写实际操作之前先声明:下面这些命令都是我在 x86_64 环境下验证过的写法,其他架构差别不大,但包名和引导文件的位置可能会有变化。跟着做之前,先确认你的构建机是 Alpine。
3.1 用 apk 引导一个干净的 rootfs
第一步是先引导出目标系统的根文件系统。假设构建目录是 build/rootfs:
mkdir -p build/rootfs apk --root build/rootfs --initdb \ --repository "https://mirrors.edge.kernel.org/alpine/v3.20/main" \ --arch x86_64 add alpine-base ca-certificates \ nodejs npm openssl jq curl这条命令的意图:--root 把安装目标指向空目录,--initdb 为新的 rootfs 初始化 apk 数据库,--arch 指定目标架构,最后列出的包就是基础系统加证书、运行时和网络工具。装完后再 chroot 进去做系统配置:
chroot build/rootfs /bin/sh echo "nameserver 1.1.1.1" > /etc/resolv.conf rc-update add devfs sysinit rc-update add sysfs sysinit rc-update add networking default passwd root exit到这里你已经有一个能启动的最小系统了。我不建议在这一步停留太久,因为真正的复杂度在工具链安装和启动脚本配置上。rootfs 引导这步做对了,后面的一切才有基础。
3.2 固定 wrangler 版本并写入 release 文件
工具链的安装方式无非是在 chroot 里跑 npm 全局安装,但有一个原则必须遵守:固定具体版本,而不是装 latest。
npm install -g wrangler@4 miniflare@3 --prefix /usr/local wrangler version miniflare --versionlatest 每次构建都可能变化,这违背了"可复现"的初衷。装完之后我把版本号追加到 /etc/cloudflare-os/release 文件里。以后任何一次部署行为异常,第一件事就是 cat 这个文件,确认环境是不是和预期一致。这个习惯帮我在后面排查问题的时候省了很多时间。别小看这个动作,团队协作时"环境版本是否一致"往往是排查问题的第一道关卡。
3.3 生成 ISO 与虚拟机镜像
rootfs 准备好之后,可以用 Alpine 官方维护的 mkimage 脚本(Alpine 官方 ISO 的构建就依赖它)来生成标准镜像,也可以自己用 genisoimage 手工打包。我实际用的是后者,因为 rootfs 是手工定制的,想完全控制引导内容和卷标:
apk add genisoimage syslinux mkdir -p build/iso-root tar -C build/rootfs -c . | tar -C build/iso-root -x mkdir -p build/iso-root/isolinux cp /usr/share/syslinux/isolinux.bin build/iso-root/isolinux/ cp /usr/share/syslinux/ldlinux.c32 build/iso-root/isolinux/ genisoimage -o build/cloudflare-os.iso -R -J \ -b isolinux/isolinux.bin -c isolinux/boot.cat \ -no-emul-boot -boot-load-size 4 -boot-info-table \ build/iso-root实际动手的时候你会发现最容易漏掉的就是 ldlinux.c32 这个文件——isolinux 引导时找不到它就会报 boot failed。所以我的建议是先跑通一个小 ISO,确认引导没问题,再往里面加东西。第一次构建我卡在这里两个小时,后来才意识到是少复制了一个引导模块文件。
3.4 三种运行形态
cloudflare-os 不只是光盘镜像,实战中我常用三种形态:
- 本地虚拟机:用 QEMU 直接启动 ISO,适合验证系统行为。
- 容器基础镜像:把 rootfs 打成 tar 给 docker import,之后在 CI 流程里使用。
- 常驻开发机:沿着 OpenRC 启动流程装到硬盘,适合服务器或者专用的开发机上长期使用。
QEMU 启动这套最小系统非常快,一般来说几秒钟就能进入 shell:
qemu-system-x86_64 -m 1024 -boot d \ -cdrom build/cloudflare-os.iso \ -netdev user,id=n0 -device virtio-net-pci,netdev=n0注意:尽量给系统挂一个可写的 virtio 磁盘,别直接依赖 ISO 的只读文件系统。wrangler 在执行的时候要写大量临时目录,只读环境下会撞上一堆 read-only file system 错误。
4. 定制过程中踩过的五个坑:最小化系统的代价
这部分我按自己的真实经历整理成了一张速查表,后面每个小节再展开讲细节。这些坑有一个共同特点:单独看都不起眼,但每一个都能让系统"看似启动了却完全没法用"。
| 问题 | 典型症状 | 根源 | 解法 |
|---|---|---|---|
| musl ABI 不匹配 | 启动时报 libdl.so.2 相关错误 | npm 包只发 glibc 二进制 | 优先使用官方预编译产物 |
| 证书链缺失 | HTTPS 请求全部失败 | 精简系统时去掉了 CA 证书 | 补装 ca-certificates |
| PATH 路径不全 | rc 脚本报 command not found | OpenRC 默认 PATH 太短 | 显式写绝对路径 |
| Token 写死 | 镜像分享导致凭据泄露 | 图省事直接写入脚本 | 占位符加首次启动注入 |
| 时区未配置 | 日志时间与本地差 8 小时 | 默认 UTC | 显式设置时区 |
4.1 musl 与 Node 原生模块的兼容性
Alpine 用 musl libc,而大量 npm 包发布时只提供针对 glibc 编译好的二进制。最典型的就是 wrangler 依赖链里那些涉及文件监听和加密的模块——在 glibc 环境里一切正常,丢到 Alpine 上就会报二进制不兼容的加载错误。
解法有两个方向。第一,优先使用 wrangler 官方提供的预编译二进制,官方构建会同时出 glibc 和 musl 两个版本,别从纯 JS 源码包去装。第二,实在绕不过的依赖,只能用 npm 的 --build-from-source 重新编译,或者考虑在这一层换 glibc 底座。我在项目里基本只用了第一方向,省心。
4.2 忘装 CA 证书导致的 HTTPS 全面失败
第一次精简系统时,我把 ca-certificates 当作冗余去掉了。结果不管是 npm 拉包、wrangler 登录还是脚本调 API,全部报证书验证失败。这个问题的坑在于错误信息五花八门:有时是 self signed certificate,有时是 unable to get local issuer certificate,还有时干脆超时。很难第一时间联想到是证书缺失。
给一个排查技巧:在最小系统里跑一句wget https://www.cloudflare.com -O /dev/null,如果能完成 TLS 握手就说明证书链没问题,否则先把证书装上再说。这类"一票否决"级别的测试命令应该写进 cfo init 里,每次启动自动跑一遍。
4.3 OpenRC 服务脚本里的命令路径
OpenRC 的 service 脚本运行在非登录 shell 环境里,默认 PATH 很可能只有 /bin 和 /sbin。而 wrangler 装到了 /usr/local/bin,node 在 /usr/bin,这些路径在 rc 脚本里必须写全,否则 service 启动时直接 command not found。
我的做法是在 /etc/conf.d 里增加一个公共变量,所有服务脚本统一引用:
CF_PATH=/usr/local/bin:/usr/bin:/bin:/sbin这个坑很典型:手动在终端里跑一切正常,一旦交给 init 系统启动就找不到命令。根源只是 PATH 在非交互环境下的默认值不同,但排查起来容易怀疑人生。
4.4 API Token 不能写死在镜像里
做镜像最危险的操作是把 Token 顺手写进环境变量或脚本。镜像一旦被上传分享,Token 就等于泄露。我采用的方案是:
- 系统内只放一个凭据入口文件 /etc/cloudflare-os/credentials,构建时写入占位符;
- 首次启动时,cfo init 检测这个文件,如果是占位符就交互式输入,或者从外部环境变量注入;
- 所有脚本统一从该文件读取,不允许散落在各个命令里。
Token 权限建议尽量收敛,Workers 编辑和 DNS 编辑两个权限就够日常使用了,尽量不要拿全局管理员 Token 去跑自动化脚本。
4.5 时区不设置,日志全是 UTC
最小系统默认 UTC,问题本身不大,但排查线上日志时会很痛苦。wrangler 和 API 脚本打印的时间戳如果和你的本地时间相差好几个小时,对账时非常容易看错。我在 rootfs 里加了一步:
apk add tzdata cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime echo "Asia/Shanghai" > /etc/timezone时区按你的实际情况写,但务必显式配置,不要依赖默认值。最小系统里省掉 tzdata 看起来能省几百 KB,实际带来的心智成本远超这点空间。
5. 开箱即用的真实场景:部署、DNS 批量管理与巡检
系统做出来不是拿来把玩的。这一节讲三个我日常高频使用的场景,都是可以直接照抄的工作流。
5.1 发布一个 Worker 的标准流程
在 cloudflare-os 里发布 Worker 的流程简化成了四步:
cfo init cd /workspace/my-worker npm install cfo deploycfo deploy 内部做了三件事:先跑一遍本地测试,再调用 wrangler deploy 上传,最后通过 API 检查部署结果并回显线上路由地址。这套封装最大的价值,是把"容易忘记的步骤"固化成命令——尤其是部署前的本地测试,人肉操作时太容易跳过了。发布流程一旦标准化,出问题的概率会明显下降。
5.2 用脚本批量管理 DNS 记录
我经常要为一组新域名批量添加解析。在控制台手动点一条条加太慢,于是我把 DNS 同步逻辑封装到了 cfo dns 里。准备一个 JSON 文件:
[ {"type":"A","name":"api","content":"203.0.113.10","ttl":300}, {"type":"CNAME","name":"www","content":"example.com","proxied":true} ]然后执行同步命令,脚本会逐条比对当前记录,已存在且一致的跳过,不存在的自动创建:
cfo dns sync --zone example.com --file records.json第一次用的时候我只感叹了一句:控制台点几百条记录和脚本里两秒跑完,效率是数量级的差距。更重要的是,JSON 文件可以作为资源清单纳入版本管理,DNS 变更有了审计痕迹。
5.3 缓存清理与系统巡检
cfo purge 封装的是缓存清理能力,可以整个 zone 清理,也可以按 URL 前缀清理:
cfo purge --zone example.com --prefix /blog/再配合 crond 定时任务,还能做一套简单的巡检告警:
cfo dns diff --zone example.com --file expected-records.json | \ mail -s "DNS drift" ops@example.com邮件命令需要额外装 mailx,具体要看你的通知渠道。这里的思路是:把"预期状态"固化成文件,定时对比线上状态,任何漂移都能第一时间发现。
缓存清理属于高频且有影响的操作,建议在脚本里加一个 --yes 双重确认参数,避免顺手误清整站缓存。
5.4 团队成员如何共用这套环境
小团队使用这套环境,最简单的方式是把 ISO 放进共享存储,文档里写一条 QEMU 启动命令,每个人拉下来直接启动。环境一致性由镜像保证,本机不需要安装任何 Cloudflare 工具链。版本升级同样简单:重新构建一次镜像,大家下次启动时自然用上新工具版本,不会再出现"我这边能跑你那边不行"的争论。这套模式在协作中的价值,远大于个人使用时的便利。
6. 这套系统的边界与后续方向
6.1 哪些场景不适合它
需要先说清楚 cloudflare-os 不是什么。它没有图形界面,没有浏览器,不是拿来当桌面系统用的。它的适用场景非常聚焦:任何机器上快速进入一个预配置好的 Cloudflare 开发与运维环境。如果你希望它承载其他业务,需要重新评估体积和复杂度带来的维护成本。越聚焦的系统越容易维护,这是我在整个项目里最深的体会。
6.2 我接下来打算做的两件事
定制系统有一条最重要的经验:不要贪多。早期我往里塞过 PostgreSQL、Redis、一堆语言运行时,结果镜像越来越大,启动越来越慢,出问题也越来越难查。后来一刀切只保留和 Cloudflare 相关的东西,反而顺手了。
接下来我有两个想法在推进:一是把 cfo dns 的同步模块单独拆出来,支持多家 DNS 服务商;二是给系统加一个只读模式,平时以只读文件系统运行,需要变更时才挂载可写分区,适合把整套环境长期固定成一个控制节点。等跑通了再单独写文章,这里先留个标记。