Cloudflare全家桶实战:用Workers、R2、D1搭建免服务器个人云 第一次看到 cloudflare-os 这个名字是在一个技术讨论帖里。有人把 Cloudflare 全家桶配置完后戏称自己终于装上了“Cloudflare 牌操作系统”我一开始觉得就是玩梗直到自己花了两天时间把个人站点、文件存储、数据库、API 网关全部搬到 Cloudflare 上之后才意识到这个说法其实很贴切。对独立开发者、小团队、折腾型个人站长来说Cloudflare 这套免费且全球分布的边缘网络体系确实越来越像一台“不需要关机、不用买服务器、按量计费甚至完全免费”的远程电脑。这篇内容不是 Cloudflare 的官方文档翻译而是我把自己的项目重建过程、架构选型理由、踩过的坑和排查思路整理成的一份实操笔记。如果你也想用 Cloudflare Workers、Pages、R2、D1、KV 这些服务搭建一套属于自己的轻量级云平台或者正在纠结“到底该用哪个存储、哪个数据库、怎么组织代码”这篇文章能帮你省下不少试错时间。1. 整体思路拆解为什么说这是一套“个人云系统”1.1 从“服务器操作系统”到“平台即操作系统”传统意义上操作系统是管理硬件资源、调度进程、提供文件系统的底层软件。但在云服务时代你真正关心的不再是 CPU 内核数和内存频率而是“我能不能不买服务器就运行代码”“我能不能把文件丢给某个服务然后通过 URL 访问”“我能不能用一条 SQL 查询全球分布的数据”。cloudflare-os 这个项目本质上就是这么一套组合以域名作为入口以 CDN 作为加速层以 Workers/Pages 作为计算单元以 R2/KV/D1 作为持久化层。所有组件都由 Cloudflare 托管你不用登录 SSH、不用配 Nginx、不用管 TLS 证书自动续期。你在浏览器里改一段代码推一下 Git然后全世界就能访问到更新后的版本。我在迁移自己博客的时候做过一次对比之前用一台 2C4G 的 VPS装 Nginx、MySQL、PHP还要处理防火墙、备份、证书到期提醒一个细节没弄好就可能凌晨收到告警。迁移到 Pages Workers D1 之后整个架构从“我管机器”变成了“我用平台”线上故障几乎全部由平台兜底我最只需要关注业务逻辑本身。1.2 为什么选择 Cloudflare 全家桶而不是自建全部组件选择这套方案之前我认真对比过自建基础服务虚拟机对象存储数据库的方案也对比过其他云服务商。结论是如果你接受 Cloudflare 的免费额度够用的前提那么全家桶方案在三个维度上有明显优势。第一是运维成本极低。我没有服务器可以登录也就没有内核补丁、磁盘扩容、软件源失效这些问题。每次部署只是一行命令或者一次 Git 推送版本回退也只需要在控制台点一下。第二是延迟的一致性。传统架构里如果服务器放在新加坡欧洲的访客就会得很慢。Cloudflare 的 Worker 在全球上百个边缘节点运行代码部署后是“离用户最近的地方执行”而不是“先回源到中心服务器再返回”。这意味着同一个 API 在不同地域的响应速度都能保持在百毫秒级别。第三是一切围绕域名工作流。Cloudflare 的 DNS、CDN、证书、Worker 路由、R2 自定义域名共用一套控制面和 API你不需要在五六个不同控制台之间跳来跳去。比如我配置一个 Worker 路由绑定 api.example.com它自动继承域名的 SSL/TLS 配置自动走 CDN 缓存规则自动被 WAF 安全策略覆盖。当然这套方案也有代价。最大的问题是“平台锁定”如果你把 KV、D1 的数据大量堆积迁移成本会比较高。其次是冷门 API 的文档文档质量参差不齐有些新功能迭代速度很快有时候会发现网上教程用的配置格式已经过时了。我的建议是个人项目、中小流量应用放心用涉及强合规、强定制化的项目还是需要先评估绑定风险。2. 核心组件选型解析Pages、Workers、R2、D1、KV 各司其职2.1 计算层选型Pages 适合前端Worker 适合 API很多人一开始不理解 Pages 和 Workers 的区别其实可以这么想Pages 是一个“托管静态资源 边缘渲染”的平台它擅长把你构建出来的 HTML、CSS、JS 分发到全球并且通过 Functions 提供后端能力Workers 则是一个通用的边缘函数运行时你写一段 JavaScript 或者 TypeScript把它丢到边缘执行适合做 API、网关、代理、鉴权这类逻辑。我在 cloudflare-os 这个架构里做了明确分工前端界面部署到 Pages使用静态生成框架我用的 Astro 和 Vite 项目都有构建产物直接推送上去。所有需要动态响应的接口都用 Worker 实现包括用户登录、文件上传、数据查询。如果某个页面只有极少部分动态内容我用 Pages Functions 做一个轻量入口避免为了一两个参数单独起一个 Worker 路由。Pages 的好处是自带 CI/CD你可以在控制台连接 Git 仓库每次推送自动构建。Workers 的好处是更纯粹的运行环境可以自定义路由绑定、添加定时触发器、使用更多运行时 API。两者在实际项目中往往混用Pages 管“面”Workers 管“线”。2.2 存储层选型R2 存文件D1 存关系数据KV 存高频缓存存储选择是整个架构中最容易纠结的部分因为“存哪里”直接决定了读写延迟、成本、以及后续扩展的难度。如果你的需求是存放用户头像、图片附件、生成的 PDF 或备份文件优先选 R2。R2 是对象存储兼容 S3 API但最大的亮点是它没有出口流量费。这意味着你用 R2 存放图片前端从 Cloudflare 网络上读取不会产生多次访问时的流量费用。我在实际项目里用 R2 存了大概 30 GB 的图片和一些压缩包账单一直是 0 元或者接近 0 元。如果你的需求是结构化数据比如用户表、订单表、文章表选 D1。D1 是基于 SQLite 的全球分布式关系数据库。注意它不是 MySQL但大多数基本 SQL 操作都支持而且支持在 Worker 里直接查询。对个人博客、小工具、内部管理系统来说D1 的容量和性能足够。我见过有人把几千行配置全部塞进 D1效果依然稳定。如果你的数据是“要特别快、偶尔过期也问题不大”的比如当前用户会话、接口响应缓存、全局配置标记选 KV。KV 的读写延迟极低适合做热点数据的缓存层。我在项目里把用户最新 5 条动态缓存到 KVTTL 设为 60 秒平均响应时间从 180 ms 降到了 40 ms 左右。为了方便你选型我整理了一张对照表组件适合数据访问方式免费额度概况典型场景R2文件、图片、压缩包S3 API / 公开 URL10 GB 存储、每月 100 万次读附件、头像、文件库D1结构化关系数据SQL5 GB 数据库、500 万行读取用户、订单、文章KV高频键值对全局键值读取100 GB 读取、100 万次写入会话、缓存、标识2.3 为何不选传统虚拟机或容器方案我知道很多人在思考 cloudflare-os 时会问既然要做个人云为什么不直接在云服务器上装一个 Nextcloud 或者一套 Docker我的回答是这两种方案解决的问题其实不太一样。你自建一台虚拟主机本质上是“租了一台机器”你需要为它的 CPU、内存、磁盘、带宽付费。即使利用率很低账单也不会少。而且你还得负责安全更新和备份。Cloudflare 这套方案是按“操作”和“数据量”计费我在大多数时候没有流量就没有成本这非常契合“个人项目 低峰期波动大”的特点。容器方案的缺点在于假设你在 VPS 上跑了一个 MySQL 容器备份要自己写脚本自动扩容要自己搞DDoS 防护要自己找方案。而 Cloudflare 把 DDoS 防护、WAF、TLS 都内聚到平台里了相当于送了你一套“安全操作系统”的基础能力。当然如果你的应用需要长时间占用一个进程需要自定义端口监听需要本地特殊运行时那是 Worker 目前做不到的。这种情况我建议 Worker 只做网关后端计算继续放在你自己的服务器上用 Worker 做反向代理和鉴权。这不是二选一而是可以混搭的。3. 实操过程与核心环节实现从零搭建一套 Cloudflare OS 实例接下来就是动手环节。我会按一个真实项目的实施顺序来写从域名接入开始到 Page 前端部署到 Worker 写 API再到 R2、D1、KV 集成最后整合统一的认证和缓存策略。这些步骤我基于自己的实际项目做了一些简化但核心流程是完整的。3.1 环境准备与项目初始化你需要准备的东西有这几样一个 Cloudflare 账号如果没有先去官网注册免费版即可。一个域名并把 DNS 托管到 Cloudflare。你把域名服务商的 NS 改成 Cloudflare 提供的两个地址即可首次接入大约几分钟生效。本机安装 Node.js 18以及 npm 或 pnpm。安装 Wrangler 命令行工具npm install -g wrangler。然后登录你的账号wrangler login接着创建一个工作目录初始化项目。我习惯用一个 monorepo 同时管理 front 和 worker 两部分mkdir cloudflare-os cd cloudflare-os npm init -y mkdir front worker这里我建议一开始就把代码拆成两个独立包因为 Pages 和 Workers 的构建流程不相同混在一个 package 里容易给构建脚本带来麻烦。Pages 那部分可以放一个 Vite/Astro/Hexo 等项目Worker 部分一个单独的 package.json 和 wrangler.toml。3.2 前端部署到 Cloudflare Pages我以 Astro 为例因为它的构建产物是纯静态文件非常适合 Pages。先在 front 目录初始化 Astrocd front npm create astrolatest -- --template minimal写几个页面组件然后构建npm run build构建完成后你会看到dist目录。接下来在 Cloudflare 控制台进入 Pages选择“创建项目”然后选择“直接上传”把dist内容传上去。如果你用 Git 集成就选好仓库和构建命令Cloudflare 会在每次 push 时自动构建。首次上传后系统会分配一个xxx.pages.dev域名。这时候你可以在自己的主域名中添加一条 CNAME 记录指向这个 pages.dev 地址。例如CNAME cloud.yourdomain.com - your-project.pages.dev这个 CNAME 会自动继承 Cloudflare 的 CDN 加速和 TLS 证书配置。注意如果你希望根域名也指向 Pages需要在 Pages 的自定义域设置中绑定不要手动添加 CNAME 再开 CNAME 拉平那样往往会出现重复配置。3.3 Worker API 与 R2 文件上传实现Pages 只能提供静态资源和 SSR 函数真正的业务逻辑需要 Worker 来接。创建一个 Worker绑定 R2 bucket。首先创建一个 R2 bucket。在 Cloudflare 控制台选 R2创建名为cloudfile的存储桶选择离你主要用户近的区域Free 版通常默认生成。然后写 Worker 代码。文件上传的常见设计是前端通过 API 获取一个预签名 URL然后用这个 URL 直传文件到 R2避免 API 和流量都经过额外节点。不过 Cloudflare R2 的预签名 URL 需要 S3 兼容 SDK我一个人维护一个 SDK 有点重所以项目初期直接用 Worker 中转上传。我在 worker 入口文件src/index.js中写了这样的逻辑import { AwsClient } from aws4fetch; const r2 new AwsClient({ accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY, }); export default { async fetch(request, env) { const url new URL(request.url); if (url.pathname /upload request.method POST) { const formData await request.formData(); const file formData.get(file); const filename ${Date.now()}-${file.name}; await env.CLOUDFILE.put(filename, file.stream(), { httpMetadata: { contentType: file.type }, }); return Response.json({ ok: true, url: /file/${filename} }); } if (url.pathname.startsWith(/file/)) { const key url.pathname.slice(6); const object await env.CLOUDFILE.get(key); if (!object) return new Response(Not Found, { status: 404 }); return new Response(object.body, { headers: { Content-Type: object.httpMetadata.contentType }, }); } return new Response(Not Found, { status: 404 }); }, };在wrangler.toml中声明这个存储桶name os-worker main src/index.js compatibility_date 2024-10-01 [[r2_buckets]] binding CLOUDFILE bucket_name cloudfile你还需要在 Cloudflare 控制台创建一个 R2 API Token把密钥放到 Worker 的环境变量中或者在本地.dev.vars文件里写入用于本地测试。注意不要把密钥提交到 Git。通过预签名 URL 的方式也很常见需要你使用 S3 SDK 生成一个短期 URL。个人体验下来如果上传量不大直接用 Worker 中转最省事如果上传频繁、文件体积大我用 SDK 的预签名 URL 更稳因为 Worker 在处理大文件流时也对超时比较敏感。3.4 D1 数据库与 KV 缓存接入接下来创建 D1 数据库并在 Worker 中执行建表 SQL。在项目根目录执行wrangler d1 create os-db命令执行后会生成数据库 ID你需要把它填进wrangler.toml的[[d1_databases]]段。然后创建表CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL DEFAULT , created_at INTEGER DEFAULT (strftime(%s, now)) );在 Worker 中可以通过env.DB.prepare(...)执行查询。比如async function getUser(env, email) { const result await env.DB.prepare(SELECT * FROM users WHERE email ?) .bind(email) .first(); return result; }初次使用 D1 时需要注意它默认不是强一致性的全球复制。如果你对某个数据写入后立刻要读取到最新值建议给读请求加?force1强制读主副本。我在用户资料更新场景里遇到了“更新后刷新页面显示旧值”的情况后来发现就是 D1 的读副本延迟导致的典型现象。对一个小的个人工具来说这个问题出现的概率其实很低但如果是支付、下单、验证码这类场景一定不要把读请求全部打到副本上否则会诱发奇怪的并发问题。KV 接入更简单。在wrangler.toml里加[[kv_namespaces]] binding CACHE_KV id your-kv-namespace-id然后在代码里设置一个简短的 TTL 缓存async function getCachedPosts(env) { const cached await env.CACHE_KV.get(posts:home); if (cached) return JSON.parse(cached); const posts await fetchPostsFromD1(env); await env.CACHE_KV.put(posts:home, JSON.stringify(posts), { expirationTtl: 300 }); return posts; }这里我建议把 TTL 设置成 300 秒到 600 秒太长容易造成内容更新看不到太短又无法发挥缓存效果。对于热数据我会在数据更新时主动删除 KV 键而不是干等过期这就避免“页面改了、缓存还是一天前的旧值”的尴尬。3.5 统一认证与权限隔离在 cloudflare-os 架构里认证是一个很关键的部分。好在 Cloudflare 提供了一个相对轻量的方案在 Worker 前端使用 Cloudflare Access 或者自己轮一个 JWT Cookie 的逻辑。我自己的做法是先用 Cloudflare Access 保护/admin/*路径把整个管理后台放到 Access 后面这样只有我自己的邮箱能登录。Cloudflare Access 会处理 OAuth 和策略Worker 只需读取 request headers 里的Cf-Access-Authenticated-User-Email就能知道当前登录用户是谁。如果你希望在自己的页面里实现“用户注册/登录”而不想每次都依赖 Access也有一套常见做法用户提交密码后Worker 用 Argon2id 哈希可以用 Web Crypto API 做 PBKDF2。生成 JWT设置 HttpOnly CookieSecure 标志。在受保护接口前写一个中间件解析 Cookie 里的 JWT有效期设为 7 天。需要注意的是因为 Worker 是无状态的所有会话信息要么存进 JWT 本身要么通过 KV 保存 sessionId。我比较推荐后者因为 JWT 一旦签发在过期前很难在服务端强制让用户退出。万一出现“要封禁某个用户”的需求直接把 KV 里的 sessionId 删除最方便。在实际项目中我会把所有 API 路由都加上 Router 层每个/api/*请求先走鉴权中间件再进入各自业务处理函数。不要把所有逻辑都写在fetch函数里否则没几天就会变成一个又长又难维护的魔球。4. 常见问题与排查技巧实录这个部分是我在实际搭建 cloudflare-os 过程中遇到过的真问题每一步都对应过我踩过的坑。我按照症状、原因、解决办法的方式整理成排查清单方便你直接对照。4.1 部署后页面或接口 404症状前端部署成功后访问 Pages 域名是好的但访问主域名时提示 404或者 Worker 可以运行但通过自定义路由访问 404。排查步骤先确认在 Cloudflare 控制台中的路由是否已正确绑定。Worker 路由通常需要你提供一个域名或子域名路径例如api.yourdomain.com/*。如果你只在 DNS 记录中添加了解析但没有在 Workers 路由中声明平台不会自动把流量转到 Worker。检查自定义域名是否为 Pages 绑定成功。Pages 的自定义域是独立概念如果只是手动添加了 CNAME即使 DNS 解析正常Pages 也可能不认为这个域属于它。确认 Worker 的fetch处理逻辑对根路径有响应。我见过有人只在fetch里处理/api/xxx直接访问根域名会返回 404。提示Cloudflare 的 Worker 路由支持通配符如*.example.com/*但如果同时存在多个 Worker 绑定到类似路径注意优先级。控制台里可以调整路由顺序实际运行时会按配置顺序匹配。4.2 D1 写入后读取不一致症状通过管理后台更新了一篇 post 的内容刷新页面仍然是旧内容等待几分钟后恢复正常。原因D1 是全球分布式的如果一个数据库启用了多区域复制读取可能打到某个副本节点上副本同步有一定的延迟。另外KV 缓存如果 TTL 设太长也需要等待过期。解决办法读高实时数据时在查询条件中使用强制读取主库const result await env.DB .prepare(SELECT * FROM posts WHERE id ?) .bind(id) .first({ force: true });同时在写操作完成后主动删除对应 KV 缓存键。我在写文章更新功能时会同时调用env.CACHE_KV.delete(posts: id)。这样缓存不生效不会让旧数据在页面停留。4.3 R2 上传文件时文件名乱码症状上传的图片文件名中包含中文或空格后来打开 URL 时却无法访问。原因R2 的存储键建议使用 URL 安全的字符。如果直接使用原始文件名虽然可以存但在生成访问 URL 时中文没有 URL 编码就会导致请求匹配失败。解决办法上传时一定要做编码处理。我常用的代码是const cleanName file.name .replace(/[^\w.\-]/g, _) .toLowerCase(); const key ${Date.now()}-${cleanName};这样可以保证生成的 URL 永远不需要额外编码也能避免各种系统工具在迁移时对特殊字符的解析问题。4.4 免费额度之外的计费弯路Cloudflare 免费版对于经常折腾的人是真的够用但有些隐性计费点需要提前了解一下KV 的读写次数、存储空间都有配额一旦超出会开始按次计费或直接禁止写入。个人项目一般不会触发但热更新类接口特别频繁调用时要注意可能一天几十万次写入的情况。R2 有免费 10 GB 存储和 100 万次读操作但如果你的用户天天访问大量大文件读次数会消耗得很快。我把 R2 的访问日志接入到分析里偶尔看一下用量心里有数。Workers 免费计划每天有 10 万次请求额度注意这里不区分请求复杂度每个请求都算。如果连续刷页面、跑爬虫很容易触顶。一旦触顶Worker 会返回错误页你需要检查自己的调用频率。实际上我在最初一个月遇到过“Worker 请求数超标”的情况原因是我在一个定时任务中每 5 分钟调用了 100 次 API结果一天下来就把额度烧了一大半。解决办法是减少轮询频率把多次调用合并在一个请求中一次性处理所有数据。4.5 本地调试与线上表现不一致症状本地wrangler dev运行正常部署后接口报错或者环境变量缺失。原因多数情况是环境变量没有同步。你本地读的是.dev.vars线上读的是你在 Cloudflare 控制台配置的variables和secrets。解决办法我建议在项目的wrangler.toml中把非敏感变量直接声明为默认值敏感变量设成 secrets 并在部署时手动配置。每次修改后用wrangler deploy重新部署并注意看日志输出。另外本地调试时尽量打开wrangler dev --remote这样就可以在本地代码中访问远程的 R2、D1 数据很多“数据格式不对”“缓存同步失败”的问题都通过这个命令提前暴露出来。5. 更进一步把 cloudflare-os 扩展成你的个人基础设施5.1 围绕核心系统增加自动化任务Cloudflare 提供定时触发器的功能可以让 Worker 按计划执行。我在自己的 cloudflare-os 环境中放了三个定时任务每天凌晨 2 点用 R2 里的数据生成统计摘要写入 D1。每 10 分钟拉取外部 RSS 源更新到 KV 缓存中。每周日凌晨对 D1 数据库做一次全量备份导出成 JSON 文件并存入 R2。这些任务在传统架构里需要 cron 脚本 备份策略在 Cloudflare 里就只是一段 Worker 代码绑定一个 cron Triggers 配置平台会自动触发。对于“个人云”来说这种“零维护的自动化”真的很提升幸福感。5.2 我喜欢的一些配置细节有几个配置细节可以提高这套体系的使用体验但官方文档藏得比较深容易被人忽略第一Pages 部署时建议将MAX_DURATION调大避免构建超时。大型项目构建几分钟很常见默认值会比较保守。第二Worker 绑定的 R2 存储桶如果希望当作图床建议开启“公共访问”然后把你自己的域名加为 bucket 的自定义域。这样 URL 就是images.example.com/xxx.jpg而不是pub-xxx.r2.dev/xxx.jpg品牌感更强。第三D1 建议开启 SQL 日志。控制台里面有查询日志功能排查慢查询会方便很多。虽然高级查询性能不如专门的数据库但你能通过日志快速定位是不是因为缺了索引。第四全部代码建议使用 TypeScript。我在维护初期用 JavaScript 写得很开心但项目复杂度起来之后各种环境变量和数据结构特别容易写错。改成 TypeScript 后Wrangler 也能提供类型绑定例如Env接口会自动生成开发体验完全不一样。5.3 我踩过几次坑之后的整体体会我现在对这个项目的整体体会是cloudflare-os 并不是一个严谨的产品名它更像是一种思路——把平台能力当作个人计算机的资源来规划。以前我考虑的是“我要买多大内存、多少核、多少带宽”现在考虑的是“我的数据适合放在 KV 还是 D1计算逻辑应该放在哪个边缘节点附近”。思维方式变了部署和运维的自然也就跟着变了。如果你正在考虑从传统 VPS 迁移到这套体系我建议先拿一个非核心项目试水比如把一个博客或者一个小型的下载站搬到 Pages 和 R2 上跑上一周看看账单和日志。体验过“几十万次请求但一分钱没花”的爽快感之后你大概率再也不想回到“定期给服务器打补丁”的日子了。