零成本自建图床:Cloudflare Pages + D1 + Telegram + R2 完整实战

写博客的人都绕不开一个问题:图片存哪。

用第三方图床,怕运营方跑路、怕忽然收费限速;把图片直接提交进 Git 仓库,几十篇文章下来仓库能到几百 MB,克隆一次等半天。而自己买服务器跑图床,为了几张截图付一整台 VPS,怎么算都不划算。

这篇记录一套我实际跑通的方案:Cloudflare Pages 托管 + D1 存元数据 + Telegram 存文件 + R2 兜底,全程只用免费额度,不买服务器、不用备案、不付流量费。

视频版在这里,跟着操作更直观:https://youtu.be/sxYl6IyOlC4

下面把完整的项目地址、全部初始化 SQL、每一处界面路径和踩过的坑都写清楚,照着做就能复刻。

零、先说项目地址

本文用的开源项目是 CloudFlare ImgBed,MIT 协议,本文写作时版本 v2.7.8,约 6.8k star。

项目 地址
GitHub 仓库 https://github.com/MarSeventh/CloudFlare-ImgBed
官方文档 https://cfbed.sanyue.de/
初始化 SQL 原始文件 https://github.com/MarSeventh/CloudFlare-ImgBed/blob/main/database/init.sql

它有四个关键特性,决定了它值得折腾:

  1. 元数据与文件解耦 —— 数据库只存 file_id、路径、标签,真实文件写进外部存储渠道
  2. 支持六种存储后端 —— Telegram、Cloudflare R2、S3 兼容、Discord、Hugging Face、WebDAV
  3. 同构内核 —— 同一套代码可跑在 Pages、Workers、Docker 三种运行时
  4. 多渠道负载均衡 + 失败自动切换

下面所有配置都以 v2.7.8 为准。版本差异主要在后台菜单位置,参数名基本稳定。

一、为什么自建图床

理想方案得同时满足四个条件:容量足够大、长期不花钱、链接稳定、能自己管。

主流选择其实就三个方向:

方案 免费额度 主要短板
Cloudflare R2 10 GB 存储,出网全免费 需要绑卡,容量是硬上限
Telegram 频道 容量近似无限 单文件 20 MB,无 SLA,条款灰色
大厂对象存储 新用户试用额度 试用期结束按量计费,出网费贵

单独用任何一个都有明显缺陷。但这三个的短板恰好能互补——Telegram 出容量,R2 出稳定性,Cloudflare 出访问入口。

二、架构:四层各司其职

1
2
3
4
5
6
7
8
9
10
浏览器 / PicGo
│
▼
Cloudflare Pages(Functions 边缘函数)
│ ← 域名、鉴权、缓存、图片尺寸处理
├──────────► D1 数据库(元数据:文件名、路径、file_id)
│
└──────────► 存储渠道
├─ Telegram 频道(主力,容量无限)
└─ R2 存储桶(兜底,稳定 + 大文件)

关键设计是元数据与文件解耦:数据库里只存「谁、什么时候、存在哪个渠道的哪个 ID」,真实文件躺在 Telegram 或 R2 里。

两个直接好处:

  1. 换存储后端不影响已有链接 —— 链接永远是 你的域名/file/xxx,跟后端无关
  2. 可配多渠道,上传失败自动切换

一个必须提前说清的认知:这套方案的「兜底」发生在上传时,不是读取时,也不做双写。

  • ✅ Telegram 上传失败(超 20 MB、限流、抖动)时自动重试并落到 R2
  • ❌ 已存进 Telegram 的图不会自动复制到 R2,Telegram 出问题时这部分仍然会挂

理解这一点,后面的配置才有意义。

三、免费额度到底有多少

先把账算清楚,这是整套方案成不成立的前提。

资源 免费额度 折算成实际用量
Telegram 频道 无官方配额 按 300 KB 一张,几十万张量级
Telegram 单文件 20 MB(Bot API 下载上限) 超出自动分片
R2 存储 10 GB-month / 月 约 3.5 万张 300 KB 图
R2 出网流量 无限免费 相对 S3 最大的优势
R2 读操作(B 类) 1000 万次 / 月 约 33 万次/天
R2 写操作(A 类) 100 万次 / 月 约 3.3 万次/天
Pages Functions 请求 10 万次 / 天 未命中缓存时有效
Pages Functions CPU 10 ms / 次 图片处理要注意
D1 存储 500 MB / 库 元数据每条几百字节,够存几十万条
D1 写入 10 万行 / 天 每张图 1–2 行
KV(不推荐) 读 10 万 / 日、写 1000 / 日 写入是硬伤

结论很清楚:存储不是瓶颈,请求才是。图片一旦被 CDN 缓存命中,就不再消耗 Functions 额度——所以缓存配置得好不好,直接决定这套图床能扛多少访问。

四、前置准备

4.1 账号清单

  • Cloudflare 账号,且已绑定支付方式(R2 必须绑卡才能开通,绑卡不等于收费)
  • 一个已托管在 Cloudflare 的域名(NS 指向 CF,状态 Active)——图片尺寸处理依赖它,pages.dev 不支持
  • GitHub 账号
  • Telegram 账号

4.2 Telegram:拿 Bot Token 和 Chat ID

第一步,建 Bot

  1. Telegram 搜索 @BotFather(认准蓝色勾)
  2. 发送 /newbot
  3. 输入 Bot 显示名(随意,如 MyImgBed)
  4. 输入用户名(必须以 bot 结尾,全局唯一,如 myimgbed_2026_bot)
  5. 它会回复一段:
1
2
Use this token to access the HTTP API:
7836972004:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

那串 数字:字母 就是 Bot Token,只显示一次,立刻存好。

⚠️ Token 等于 Bot 的完全控制权。如果截图发过群、贴过论坛,去 BotFather 发 /revoke 换一个,旧 Token 立即作废。

第二步,建私有频道

  1. Telegram → 新建消息 → 新建频道
  2. 名称随意,如 图床存储
  3. 关键:选「私密频道 / Private Channel」,不要选公开
  4. 建好后进频道 → 点频道名 → 管理员 → 添加管理员 → 搜索你刚建的 Bot 用户名 → 添加
  5. 权限开关里把 「管理消息 / Manage Messages」打开

Bot 没有这个权限,上传会报 not enough rights,这个报错不会告诉你是权限问题。

第三步,拿 Chat ID

  1. 在频道里发一条测试消息(打个 test)
  2. 长按该消息 → 转发 → 转发给 @VersaToolsBot
  3. 它返回一串形如 -1001234567890 的数字

前面的负号必须保留,少一个负号后面必然失败。

4.3 R2:建桶

  1. Dashboard → 左侧 R2 对象存储 → 首次进入需同意条款并启用
  2. 创建存储桶
  3. Bucket name 全局唯一,如 imgbed-mote2026
  4. Location 选 Automatic
  5. Default storage class 必须是 Standard

⚠️ 千万别选 Infrequent Access —— 它不享受 10 GB 免费额度,还会收数据取回费。

公开访问(Public access)先不用开。r2.dev 那个默认域名有速率限制,正式用要绑自定义域名,等最后一起处理。

五、Pages 部署

5.1 Fork 仓库

打开 https://github.com/MarSeventh/CloudFlare-ImgBed → 右上角 Fork → Create fork → 分支保持 main。

5.2 创建 Pages 项目

  1. Cloudflare Dashboard → 计算和 AI(Compute) → Workers & Pages
  2. 创建 → 页面下拉找到 Looking to deploy Pages? → Get started
  3. 选 导入现有 Git 存储库
  4. 首次会让你授权 GitHub,选「Only select repositories」,只授权 fork 的那个仓库

5.3 构建配置(这四项决定成败)

配置项 必须填
项目名称 cloudflare-imgbed(随意)
生产分支 main
构建命令 npm install
构建输出目录 frontend-dist
根目录(Root directory) 留空

坑位一:构建命令不是 npm run build

项目从 v2.0 起改了构建方式。更要命的是,这个仓库根目录的 package.json 里没有任何 dependencies,依赖全部放在 workspace 子包 deploy/profiles/common 里,靠 "workspaces": ["deploy/profiles/*"] 提升上来。

所以只要 npm install 没有真正执行,node_modules 就是空的,Pages 打包 Functions 时会一口气报十个错:

1
2
3
ERROR Could not resolve "@aws-sdk/client-s3"
ERROR Could not resolve "hono"
...

aws-sdk 只是第一个被打印出来的。看到这个错,不要去装某个具体的包,回去检查构建命令。

另一个判断依据是时间:从开始构建到报错只用了 7 秒。真正跑完一次 npm install 不可能这么快,说明安装环节被整个跳过了。

三种常见错法,表现完全一样:

  • 构建命令留空(Pages 不会替你装 workspace 依赖)
  • 填 npm ci(没有 lockfile,直接失败)
  • 填 npm run build(这仓库根本没有 build 脚本)

补一个保险措施:设置 → 变量和机密 → 添加 NODE_VERSION=22。因为 npm 7 以前不认识 workspaces 字段,会把子包依赖全部忽略,报错一模一样。

5.4 首次部署与预期现象

点 保存并部署,等 2–3 分钟,会给你一个 xxx.pages.dev 域名。

这时候打开它大概率是白页或报错——这是正常的,数据库还没绑。环境变量一个都不用填,v2.0 之后所有业务配置都在后台。

📌 注意:构建成功后会给你两个域名,形如 97b78886.项目名.pages.dev 和 项目名.pages.dev。前面带哈希的是单次部署的预览域名,每次重新部署都会变。以后一律用不带哈希的生产域名,否则某次部署后旧链接就 404 了。

六、D1 数据库(附完整 SQL)

6.1 建库

Dashboard → 存储和数据库 → D1 SQL 数据库 → 创建数据库 → 名称填 img_d1 → 创建。

6.2 初始化表结构

点进 img_d1 → 上方切 控制台(Console) → 粘贴 SQL → 执行。

坑位二:整段粘贴会提示 No SQL statements detected.

原因是控制台的输入焦点问题——内容看着在框里,但点击执行时读的是空内容。另外从某些编辑器复制时还会丢掉换行,整段挤成一行,一旦某条语句语法错误就很难定位。

**解决办法:拆成三段,按顺序各执行一次。**下面是可以直接复制的完整内容。

第一段:5 张表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
value TEXT,
metadata TEXT NOT NULL,
file_name TEXT,
file_type TEXT,
file_size TEXT,
upload_ip TEXT,
upload_address TEXT,
list_type TEXT,
timestamp INTEGER,
label TEXT,
directory TEXT,
channel TEXT,
channel_name TEXT,
tg_file_id TEXT,
tg_chat_id TEXT,
tg_bot_token TEXT,
is_chunked BOOLEAN DEFAULT FALSE,
tags TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
category TEXT,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS index_operations (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
timestamp INTEGER NOT NULL,
data TEXT NOT NULL,
processed BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS index_metadata (
key TEXT PRIMARY KEY,
last_updated INTEGER,
total_count INTEGER DEFAULT 0,
last_operation_id TEXT,
chunk_count INTEGER DEFAULT 0,
chunk_size INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS other_data (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
type TEXT,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

第二段:12 个索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE INDEX IF NOT EXISTS idx_files_timestamp ON files(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_files_directory ON files(directory);
CREATE INDEX IF NOT EXISTS idx_files_channel ON files(channel);
CREATE INDEX IF NOT EXISTS idx_files_file_type ON files(file_type);
CREATE INDEX IF NOT EXISTS idx_files_upload_ip ON files(upload_ip);
CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_files_tags ON files(tags);

CREATE INDEX IF NOT EXISTS idx_settings_category ON settings(category);

CREATE INDEX IF NOT EXISTS idx_index_operations_timestamp ON index_operations(timestamp);
CREATE INDEX IF NOT EXISTS idx_index_operations_processed ON index_operations(processed);
CREATE INDEX IF NOT EXISTS idx_index_operations_type ON index_operations(type);

CREATE INDEX IF NOT EXISTS idx_other_data_type ON other_data(type);

第三段:4 个触发器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TRIGGER IF NOT EXISTS update_files_updated_at
AFTER UPDATE ON files
BEGIN
UPDATE files SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;

CREATE TRIGGER IF NOT EXISTS update_settings_updated_at
AFTER UPDATE ON settings
BEGIN
UPDATE settings SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key;
END;

CREATE TRIGGER IF NOT EXISTS update_index_metadata_updated_at
AFTER UPDATE ON index_metadata
BEGIN
UPDATE index_metadata SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key;
END;

CREATE TRIGGER IF NOT EXISTS update_other_data_updated_at
AFTER UPDATE ON other_data
BEGIN
UPDATE other_data SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key;
END;

全部带 IF NOT EXISTS,重复执行安全,缺表就再跑一遍。

执行时的三个要点:

  1. 先单击编辑框,看到光标在里面,再 Ctrl+A → Ctrl+V,确认内容真的在框里
  2. 用 Ctrl+Enter 直接执行,比点按钮更可靠
  3. 每段成功的标志是出现 Executed N statements 之类的提示

验证表是否建好

切到 表(Tables) 标签,或者执行这条查询:

1
SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;

应该返回 5 行:

1
2
3
4
5
files
index_metadata
index_operations
other_data
settings

(可能额外多一个 _cf_KV,那是 Cloudflare 自带的内部表,每个 D1 都有,不影响使用。)

命令行方案(控制台始终不认时的备选)

1
npx wrangler d1 execute img_d1 --remote --file=./init.sql

首次会要浏览器登录授权 Cloudflare 账号,之后一条命令跑完全部建表。

6.3 为什么必须用 D1,不用 KV

这是硬性依赖,不能省。图片实体存在 Telegram / R2 里,但文件名、路径、file_id、标签、上传时间这些信息必须存在数据库里。没有数据库,Telegram 返回的 file_id 无处保存,下次访问就找不到图,上传接口直接报 500。

选哪个数据库是可以选的:

选项 结论
KV 能用,但每天只有 1000 次写入(等于每天最多传 1000 张),且不支持大文件上传
D1 推荐,免费 500 MB,支持大文件,性能略差但对图床无感

6.4 绑定(变量名写死,不能错)

Pages 项目 → 设置(Settings) → 绑定(Bindings) → 添加 → 选 D1 数据库:

字段 值
变量名称 img_d1
D1 数据库 img_d1

坑位三:绑定变量名是代码里写死的

绑定类型 变量名 说明
D1 数据库 img_d1 必填
R2 存储桶 img_r2 必填
KV 命名空间 img_url 用 D1 时不需要

写错不会报”变量名不正确”,而是应用启动失败或后台进不去,报错信息毫无指向性。这三个名字建议直接抄。

6.5 必须手动「重试部署」

绑定本身不会触发构建。改完设置回 部署(Deployments) → 最新那条 → ... → 重试部署,等 1–2 分钟。

不重试,代码里读不到新绑定。

七、绑定 R2

Pages 项目 → 设置 → 绑定 → 添加 → R2 存储桶:

字段 值
变量名称 img_r2
R2 存储桶 imgbed-mote2026(你的桶名)

保存 → 回 部署 → 重试部署。

绑成功后回后台 系统设置 → 上传设置,应该能看到 Cloudflare R2 这一栏自动出现(显示为 R2_env),说明变量名读对了。

八、后台配置

访问 https://你的域名/dashboard(老版本是 /systemConfig),管理端默认免密。

8.1 第一件事:设密码

现在任何人知道域名就能进后台删你的图。立刻去 系统设置 → 安全设置 → 认证管理,设两套:

类型 用途
管理端认证(用户名 + 密码) 登录 /dashboard 后台
用户端认证(上传认证码) 登录 /login、PicGo 和 API 调用

这两套是独立的,别搞混:管理员密码管后台,上传认证码管传图。

8.2 配 Telegram 渠道

系统设置 → 上传设置 → Telegram 渠道 → 添加渠道:

字段 填什么
渠道名称 TG-主渠道
Bot Token 第 4.2 步拿到的 Token
Chat ID -100...(负号保留)
代理 URL 留空(CF 边缘通常能直连)
启用状态 打开

8.3 配 R2 渠道

在 Cloudflare R2 区域点 R2_env 卡片上的 编辑:

字段 值
容量上限 9.5(GB)
公开访问链接 留空(不开图片审查就不需要)

这个上限一定要设。 不设就是无限额开放,某天误传一堆大图就开始计费了。设 9.5 而不是 10,是留 5% 余量——计费按 GB-month 计算,卡边容易踩线。

8.4 设默认渠道

系统设置 → 网页设置 → 客户端设置:

字段 选
默认渠道类型 Telegram
默认渠道名称 TG-主渠道

8.5 负载均衡保持关闭

R2 卡片和 S3 卡片右上角都有 负载均衡 开关,保持关闭。

理由:兜底渠道一旦参与日常分流,就不叫兜底了——R2 那 10 GB 会被日常小图慢慢吃掉。

九、验证:三个必测项

部署完不能只看页面能打开,要验证三条链路。

9.1 上传落点

新开标签打开 https://你的域名/ → 用上传认证码登录 → 传一张小图。

两个验证点:

  • 文件卡片渠道显示 Telegram
  • 打开 Telegram 私有频道,能看到这张图

第二点是最硬的验证 —— 它同时证明 Bot 有发消息权限、Chat ID 正确、Token 有效。三样错任何一个,上传都会失败并提示 chat not found 或 not enough rights。

9.2 R2 通道本身可用

上传时把渠道下拉手动切到 Cloudflare R2,传一张。

三个验证点:

  • 卡片渠道显示 R2
  • 点图能打开,复制链接能访问
  • CF 控制台 → R2 → 桶 → 对象列表出现该文件,路径形如 2026/10/29/xxx.png

9.3 兜底是否真的兜底

把 TG-主渠道 卡片右上角的开关关掉 → 回上传页,渠道保持默认(Telegram)→ 传一张。

  • 显示成功且落在 R2 → 兜底生效
  • 报错「无可用渠道」→ 说明这个版本只在单次上传失败时切换,默认渠道被禁用不会改道

测完立刻把 TG 开关打开。

⚠️ 这两张测试图会永久留在 R2(不做双写)。10 GB 额度里这点体积无所谓,想保持干净测完删掉即可。

十、缓存:省额度的关键

图片链接形如 /file/xxx.png,由边缘函数返回。默认每次访问都要跑一次函数,消耗每日 10 万次的额度。

10.1 三类请求的默认行为

路径 谁处理 默认缓存
/、/dashboard、JS/CSS Pages 静态托管 ✅ 自动缓存,命中率极高
/file/xxx.png Pages Functions ⚠️ 取决于源站 Cache-Control
/upload、/api/*、/login Functions ❌ 不该缓存

命中缓存 = 不触发 Function = 不消耗每日额度。

10.2 加一条 Cache Rule

CF Dashboard → 选中域名 → 缓存 → Cache Rules → 创建规则:

项 值
规则名称 cache-images
匹配条件 URI Path 开头是 /file/
缓存资格 符合条件(Eligible for cache)
Edge TTL 忽略源站,设为 1 年
Browser TTL 1 天

表达式写法:

1
starts_with(http.request.uri.path, "/file/")

别对 /api/* 和 /upload 加任何缓存规则,否则会出现”传了图但列表不更新”这种诡异问题。

10.3 验证缓存是否命中

打开任意一张图,F12 → Network → 看响应头 cf-cache-status,刷新两次:

第一次 第二次 结论
MISS HIT ✅ 规则生效
DYNAMIC DYNAMIC ❌ 规则没匹配上
MISS MISS ⚠️ 匹配上了但没存住,源站返回了 no-store 或 Set-Cookie

十一、缓存的副作用:删图不生效

这是唯一让我不适的地方。图删了,但浏览器和 CDN 还存着旧副本,链接照样打开。

三种处理方式:

方案 操作 评价
配 API 凭据自动清 后台填 CF_ZONE_ID + CF_EMAIL + CF_API_KEY 最省事,但要交出全局密钥
手动清 CF → 缓存 → 清除缓存 → 输 URL 多一步,零风险
等自然过期 什么都不做 接受延迟

如果想手动清,可以用这个 curl:

1
2
3
4
5
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "X-Auth-Email: $CF_EMAIL" \
-H "X-Auth-Key: $CF_API_KEY" \
-H "Content-Type: application/json" \
--data '{"files":["https://img.你的域名.com/file/xxx.png"]}'

我选了手动清。 理由:项目要求填的是 CF_API_KEY,也就是全局 API 密钥,权限等于整个 Cloudflare 账号——能改 DNS、能删域名、能看所有账单。个人图床删图频率不高,为省这一步交出一个全账号密钥,风险收益不划算。真要上方案 1,务必定期轮换。

十二、接 PicGo

PicGo 走图床自己的 /upload 接口,这样渠道分配和数据库记录都保留。

12.1 装插件

PicGo → 插件设置 → 搜索安装 picgo-plugin-web-uploader。

12.2 配置图床

PicGo → 图床设置 → Web图床:

字段 值
API 地址 见下方
POST 参数名 file
自定义 Body 留空
图片键名 0.src

API 地址(一行到底,不要换行):

1
https://img.你的域名.com/upload?authCode=你的上传认证码&uploadChannel=telegram&returnFormat=full

三个参数的作用:

  • authCode —— 上传认证码
  • uploadChannel=telegram —— 固定走 TG;想临时存 R2 改成 cfr2
  • returnFormat=full —— 必须加,否则返回 /file/xxx 相对路径,粘到文章里就是死链

12.3 两个容易出错的地方

  1. 图片键名必须是 0.src

    接口返回的是数组 [{"src": "...", "publicUrl": "..."}],不是对象。写成 src 或 data.src 都会取不到值,PicGo 提示”上传成功但无法获取图片地址”。

  2. 域名用自定义域名,不要用 pages.dev

    以后换域名,所有历史链接不会跟着变。

12.4 排错对照

现象 原因
401 / 认证失败 authCode 错,或用户端认证码没保存成功
上传成功但链接是 /file/xxx returnFormat=full 漏了
无法获取图片地址 图片键名不是 0.src
chat not found Chat ID 错,或 Bot 不在频道里
not enough rights Bot 不是管理员,或没开「管理消息」权限
超 20 MB 报错 正常,TG 单文件上限;PicGo 里开「上传前压缩」

十三、自定义域名

用子域名,别占主站:

  1. Pages 项目 → 自定义域 → 设置自定义域 → 输入 img.你的域名.com
  2. 同一 CF 账号下会自动加 CNAME,不用手动配 DNS
  3. 等状态从「正在初始化」变 Active,证书自动签发

然后做两件容易漏的事:

① 把新域名加进白名单

后台 → 安全设置 → 访问管理 → 放行域名:

1
cloudflare-imgbed-moteblog.pages.dev,img.你的域名.com

两个都要留 —— 旧的 pages.dev 还在用,删了会导致旧链接全部 403。英文逗号分隔,别加空格。

② 让新链接默认走新域名

系统设置 → 网页设置 → 全局设置 → 默认 URL 前缀:

1
https://img.你的域名.com/file/

已传过的图链接不会变,仍然走旧域名,但内容一样能访问。

可选:CF Dashboard → 选中域名 → Images → 转换 → 启用,即可支持 ?width=800&fit=cover 这类参数。有免费额度,超出按次计费,不开也完全不影响使用。

十四、安全清单

部署完这六项逐条过一遍:

  • 管理端账号密码(默认无密码,任何人可进)
  • 上传认证码
  • 关闭远端遥测 —— 默认开启,会上报你的域名和访问链接给项目开发者
  • 开启域名过滤,只放行自己的域名(防盗链)
  • 建一个只含 upload 权限的 API Token,设 90 天过期 + 过期自动删除
  • 白名单模式保持关闭 —— 它是应急手段(被大规模盗链时临时开),日常用会非常麻烦

API Token 创建后只显示一次,弹窗关掉就再也看不到。点完立刻复制到密码管理器。

十五、常见报错速查

报错 / 现象 原因 解法
Could not resolve "@aws-sdk/client-s3" 依赖没装上 构建命令改 npm install,加 NODE_VERSION=22
部署成功但访问 404 / 白页 输出目录错 必须是 frontend-dist
No SQL statements detected. 控制台输入焦点问题 拆三段执行,或 Ctrl+Enter
后台进不去 / 500 D1 表没建全,或绑定名写错 查 5 张表,核对 img_d1
绑定后不生效 没重试部署 部署页手动重试
/file/* 一直是 DYNAMIC Cache Rule 没匹配 检查表达式与规则启用状态
删图后链接还能打开 CDN 缓存 手动清缓存
上传 chat not found Chat ID 错 重新用 @VersaToolsBot 取,负号保留
上传 not enough rights Bot 权限不足 设为管理员并开「管理消息」
R2 开始计费 没设容量上限 / 选了 IA 存储类 设 9.5 GB,确认是 Standard

十六、这套方案适合谁

适合:个人博客、笔记站、文档站,图片以中小尺寸截图为主,日访问量在几万次以内,愿意花 40 分钟做一次性配置。

不适合:

  • 需要 SLA 的生产业务 —— Telegram 渠道没有可用性承诺,且其服务条款明确禁止把服务当作文件托管或 CDN
  • 单文件经常超过 20 MB —— TG 这层会频繁触发分片
  • 日访问量稳定超过 10 万次 —— 要么上 Workers 付费计划,要么让图片直接走 R2 公开域名

十七、最后一句实话

这套方案的本质是用运维复杂度换成本。

它确实不花钱,但换来的是三个长期不确定性:

  1. Telegram 随时可能变策略,或被判定违规封 Bot —— 所有图瞬间失效且无法申诉
  2. Cloudflare 免费额度可能调整,10 万次/日不是承诺
  3. 缓存的副作用要自己兜,删图不即时生效这件事会一直存在

所以真正重要的图片,我还是会往 R2 那 10 GB 里放一份。Telegram 存的是”图”,R2 存的是”底”。


本文记录于 2026-10-29,项目版本 CloudFlare ImgBed v2.7.8。Cloudflare 与 Telegram 的政策和额度可能变化,以官方文档为准。