用 Cloudflare Workers 自托管短链服务 Sink:s.meirong.dev 上线记
目录
为什么需要短链服务#
博客里有时要贴一些很长的 URL——GitHub 链接、Grafana 面板、API 文档之类的。长 URL 在 Markdown 里虽然无所谓,但分享到微信、邮件时看起来乱糟糟的。
另一个场景:跳转(或者外部)链接如果能统一走自己的短域名,更整洁,也方便以后追踪点击数据和做链接的安全检查和处理。
为什么选 Sink#
Sink 是一个基于 Cloudflare Workers 的无服务器短链服务,核心特性:
| 功能 | 说明 |
|---|---|
| 自定义 slug | 手动指定或 AI 自动生成 |
| Analytics | 访客统计(设备类型、地区、来源) |
| 二维码生成 | 每条短链自带 QR Code |
| 链接过期 | 可设置有效期 |
| 批量导入导出 | JSON / CSV |
| AI 辅助 | Cloudflare Workers AI 生成 slug |
| 每日备份 | R2 Bucket 定时备份 |
关键一点:Sink 100% 运行在 Cloudflare Workers,没有服务器,不需要数据库,免费额度足够个人使用。
与其他自托管短链方案(Shlink、YOURLS、Kutt)相比,Sink 不需要 K8s 部署,不占 Oracle 云的资源,而且它本来就跑在我已有的 Cloudflare 基础设施上,零额外成本。
架构#
用户 → Cloudflare DNS (s.meirong.dev)
→ Cloudflare Worker (sink)
├── KV Namespace 短链存储(slug → 目标 URL)
├── R2 Bucket 每日 00:00 UTC 自动备份
├── Analytics Engine 访客点击统计
└── Workers AI AI slug 建议
DNS 解析由 Cloudflare 自动处理,不经过 Cloudflare Tunnel,也不过 K8s 的 Traefik——这是一条独立链路。
与 Homelab 其他服务的对比:
其他服务:Internet → Cloudflare Tunnel → Traefik (K8s) → Pod
Sink: Internet → Cloudflare Worker(边缘直接响应)
部署过程#
1. 将 Sink 作为 git submodule 引入#
把 Sink 源码作为 submodule 引入 homelab 仓库,方便版本追踪和后续更新:
mkdir -p cloudflare/workers
git submodule add https://github.com/miantiao-me/Sink cloudflare/workers/sink
目录结构:
cloudflare/workers/
├── justfile # 部署命令
└── sink/ # Sink 源码(submodule)
├── wrangler.jsonc
├── server/middleware/rate-limit.ts # 自定义限速中间件
└── .env # 本地配置(gitignored)
2. 创建 Cloudflare 资源#
Sink 需要 KV Namespace 和 R2 Bucket,可以通过 Cloudflare API 直接创建(不需要单独权限):
# 创建 KV Namespace
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/storage/kv/namespaces" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"title": "SINK_KV"}'
# 创建 R2 Bucket
curl -X POST "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/r2/buckets" \
-H "Authorization: Bearer ${API_TOKEN}" \
--data '{"name": "sink"}'
把返回的 KV namespace ID 填入 wrangler.jsonc。
3. 配置 wrangler.jsonc#
在 Sink 的 wrangler.jsonc 里添加 Analytics Engine、Rate Limiting 绑定和环境变量:
{
"vars": {
"NUXT_HOME_URL": "https://s.meirong.dev",
"NUXT_REDIRECT_STATUS_CODE": "308",
"NUXT_CF_ACCOUNT_ID": "your-account-id"
},
"analytics_engine_datasets": [
{ "binding": "ANALYTICS", "dataset": "sink" }
],
"ratelimits": [
{
"name": "RATE_LIMITER",
"namespace_id": "1001",
"simple": { "limit": 30, "period": 10 }
}
],
"kv_namespaces": [
{ "binding": "KV", "id": "your-kv-namespace-id" }
],
"r2_buckets": [
{ "binding": "R2", "bucket_name": "sink" }
]
}
Rate Limiting 设置为 30 请求/10 秒/IP(等价于平均 3/s),这是 Workers Rate Limiting binding 支持的最细粒度(最小 period = 10s)。
4. 添加限速中间件#
Sink 基于 Nuxt/Nitro 构建,可以在 server/middleware/ 下添加 Nitro 中间件来接入 Workers Rate Limiting API:
// server/middleware/rate-limit.ts
export default defineEventHandler(async (event) => {
const { cloudflare } = event.context
if (!cloudflare?.env?.RATE_LIMITER) return
const ip = getHeader(event, 'cf-connecting-ip') || 'unknown'
const { success } = await (cloudflare.env.RATE_LIMITER as any).limit({ key: ip })
if (!success) {
setResponseStatus(event, 429)
setResponseHeader(event, 'Retry-After', '1')
return { error: 'Too Many Requests' }
}
})
这个中间件对每个请求都会检查 IP 限速,超限返回 429。
5. 构建和部署#
# 构建(NUXT_HOME_URL 必须在编译时注入)
NUXT_HOME_URL=https://s.meirong.dev pnpm build
# 部署
npx wrangler deploy
注意:
NUXT_HOME_URL是 Nuxt 的 public runtimeConfig,会在编译时写入 bundle,必须通过环境变量在 build 阶段注入,而不是 wrangler vars(后者只对运行时私有变量有效)。
6. 绑定自定义域名#
Wrangler 的 custom domain 路由需要 Zone Workers Routes:Edit 权限,但我的 API Token 没有这个权限。直接通过 Cloudflare API 绑定即可:
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/workers/domains" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{
"environment": "production",
"hostname": "s.meirong.dev",
"service": "sink",
"zone_id": "your-zone-id"
}'
这个绑定是持久的,不需要每次 deploy 重新设置。
7. 设置管理员 Token#
通过 Cloudflare API 设置 Worker secret(NUXT_SITE_TOKEN 是访问 /dashboard 的密码):
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/workers/scripts/sink/secrets" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"name": "NUXT_SITE_TOKEN", "text": "your-token", "type": "secret_text"}'
上线效果#
部署完成后:
- 短链跳转:访问
https://s.meirong.dev/xxx即跳转到目标 URL(308 永久重定向) - 管理面板:
https://s.meirong.dev/dashboard用 Site Token 登录,可创建/管理短链 - Analytics:每次跳转自动写入 Analytics Engine,可在 dashboard 查看访客数据
justfile 封装了常用操作:
# 更新 Sink 版本
update:
cd sink && git pull origin master && pnpm install && \
NUXT_HOME_URL=https://s.meirong.dev pnpm build && \
npx wrangler deploy
踩坑记录#
1. wrangler deploy 每次都报 custom domain 错误
这是因为 Token 缺少 Zone Workers Routes:Edit 权限,但 domain 本身已经通过 API 绑定好了,报错可以安全忽略。Worker 正常服务。
2. Analytics Engine 需要手动激活
第一次 deploy 含 analytics_engine_datasets 绑定时会报错,需要去 Dashboard → Workers & Pages → Analytics Engine 点一次 “Enable”。之后不再需要操作。
3. Rate Limiting period 最小值是 10 秒
Workers Rate Limiting binding 的 period 只支持 10 或 60,没有 1 秒选项。要做到"平均 3 次/秒",配置为 limit: 30, period: 10 即可。
4. NUXT_HOME_URL 必须在 build 时注入
Nuxt 的 public runtimeConfig(如 NUXT_HOME_URL)会在编译时写入 bundle。如果只设置在 wrangler vars 里,运行时 Worker 读的还是编译时的默认值。正确做法是在 build 命令前设置环境变量。
总结#
Sink 是一个很适合 Homelab 场景的短链服务:
- 零服务器成本:Cloudflare Workers Free Plan 100,000 请求/天,KV、Analytics、AI 全部有免费额度
- 无需额外运维:不占 K8s 资源,不需要数据库维护,R2 自动备份
- 功能完整:短链、统计、AI slug、二维码、导入导出一应俱全
- IaC 友好:源码以 git submodule 形式纳入 homelab 仓库,
just deploy一键更新
如果你也有 Cloudflare 账号和自定义域名,整个部署过程 30 分钟内可以完成。