这篇文章记录一次真实的 apps/api 子站部署过程。这个 API 子站使用 Hono 作为 Web 框架,最终跑在 Cloudflare Workers 上。它不是一个只有 HTTP 路由的空服务,实际运行时还会依赖几类外部资源:
1D1 数据库:保存用户、会话、角色、订阅等数据2R2 存储桶:保存用户头像3环境变量:保存 origin、模型配置、OAuth 配置4Secrets:保存 JWT 密钥、GitHub Secret、LLM API Key
我们要完成的事情很明确:把本地已经能跑起来的 API 子站部署到 Cloudflare,并让 admin / web 两个前端子站都能正常访问它。
我们先进入 API 子站目录,后面的 Wrangler 命令都在这里执行:
1cd apps/api
然后确认 Wrangler 当前已经登录到正确的 Cloudflare 账号:
1pnpm wrangler whoami
如果登录状态正常,会看到类似这样的账号信息:
1Account ID: 629f5983e568d0d6b2439f06a93f8ca4
接着查看当前账号下已有的 D1 数据库:
1pnpm wrangler d1 list
这次要使用的生产数据库是:
1name: ai-agent-production-auth2uuid: ba264b43-5d69-4b40-8ba8-375be3dcaebf
如果你的账号里还没有这个生产 D1,可以先创建一个:
1pnpm wrangler d1 create ai-agent-production-auth
创建成功以后,Wrangler 会输出一段数据库信息。这里最重要的是 database_id,后面要把它写入 wrangler.jsonc。
头像会放在 R2 里,所以还需要准备一个生产 bucket。如果还没有创建,可以执行:
1pnpm wrangler r2 bucket create ai-agent-production-avatars
API 子站部署到 Cloudflare Workers 时,核心配置文件是:
1apps/api/wrangler.jsonc
我们把 production 环境里真正要用到的 D1、R2、环境变量都写在 env.production 下面。本次最终的关键配置如下:
01{02"name": "api",03"main": "src/index.ts",04"compatibility_date": "2026-04-22",05"env": {06"production": {07"workers_dev": true,08"d1_databases": [09{10"binding": "DB",11"database_name": "ai-agent-production-auth",12"database_id": "ba264b43-5d69-4b40-8ba8-375be3dcaebf",13"migrations_dir": "migrations"14}15],16"r2_buckets": [17{18"binding": "AVATAR_BUCKET",19"bucket_name": "ai-agent-production-avatars"20}21],22"vars": {23"APP_ENV": "production",24"ADMIN_ORIGIN": "https://ai-agent-admin.pages.dev",25"WEB_ORIGIN": "https://ai-agent-web-66e.pages.dev",26"ACCESS_TOKEN_TTL_SEC": "900",27"REFRESH_TOKEN_TTL_SEC": "2592000",28"DEEPSEEK_BASE_URL": "https://api.deepseek.com/v1",29"DEEPSEEK_MODEL": "deepseek-chat",30"GITHUB_OAUTH_CLIENT_ID": "<github-oauth-client-id>",31"GITHUB_OAUTH_CALLBACK_URL": ""32}33}34}35}
这段配置里有几个地方需要特别留意,否则很容易出现本地能跑、线上绑定不到资源的问题。
Wrangler 的环境配置有一个容易被忽略的规则:
1env.production.d1_databases2env.production.r2_buckets
不会自动继承顶层的:
1d1_databases2r2_buckets
所以只在顶层写 D1 / R2 还不够。只要部署时带了 --env production,生产环境就必须在 env.production 里单独配置 D1 / R2。
我们再看代码侧。项目里访问绑定资源时,用的是:
1c.env.DB2c.env.AVATAR_BUCKET
因此 production 环境里的 binding 名也要保持一致:
1{2"binding": "DB"3}
和:
1{2"binding": "AVATAR_BUCKET"3}
这里不要把 binding 名改成 ai_agent_production_auth 或 ai_agent_production_avatars。资源本身可以叫生产名称,但代码里读取的是 c.env.DB 和 c.env.AVATAR_BUCKET,所以 binding 名必须和代码保持一致。
如果执行迁移过程中看到这个报错:
1The database 33333333-3333-4333-8333-333333333333 could not be found [code: 7404]
问题出在 env.production.d1_databases[0].database_id 还停留在占位符:
133333333-3333-4333-8333-333333333333
正确做法是通过 wrangler d1 list 或 wrangler d1 create 拿到真实 ID,再写回 production 环境配置。
本次真实生产 ID 是:
1ba264b43-5d69-4b40-8ba8-375be3dcaebf
1"workers_dev": true
打开 workers_dev 以后,部署成功时 Cloudflare 会给 Worker 分配一个默认的 workers.dev 访问域名。
本次部署后的 API 地址是:
1https://api-production.1832064870.workers.dev
wrangler.jsonc 适合放非敏感配置,但不适合放密钥。
像下面这些值,都应该通过 Cloudflare Worker Secret 来保存:
1JWT_ACCESS_SECRET2JWT_REFRESH_SECRET3DEEPSEEK_API_KEY4GITHUB_OAUTH_CLIENT_SECRET
我们在 production 环境里逐个设置 secret:
1cd apps/api23pnpm wrangler secret put JWT_ACCESS_SECRET --env production4pnpm wrangler secret put JWT_REFRESH_SECRET --env production5pnpm wrangler secret put DEEPSEEK_API_KEY --env production6pnpm wrangler secret put GITHUB_OAUTH_CLIENT_SECRET --env production
每条命令执行后,Wrangler 都会提示输入对应的值。
这里要多提醒一句:secret 不要写进 Markdown、Git、聊天记录或日志里。部署文档里只记录变量名就够了,真实值应该只交给 Cloudflare 保存。
D1 的迁移文件放在:
1apps/api/migrations
我们要操作的是 Cloudflare 上的远程生产数据库,所以迁移命令要带上 --env production 和 --remote:
1cd apps/api2pnpm wrangler d1 migrations apply ai-agent-production-auth --env production --remote
这几个参数可以这样理解:
1ai-agent-production-auth D1 数据库名称2--env production 使用 wrangler.jsonc 里的 env.production3--remote 操作 Cloudflare 远程资源,不是本地模拟 D1
如果你看到类似 warning:
1There is a d1_databases binding at the top level, but not on env.production
这个 warning 的意思是:顶层虽然配置了 D1,但 production 环境没有单独配置 D1 binding。遇到这种情况,要回到 wrangler.jsonc,确认 env.production.d1_databases 确实存在。
如果你看到:
1database could not be found [code: 7404]
这时我们可以按几个方向排查:先看 database_id 是否还停留在占位符,再确认数据库名称有没有写错;如果这两项都没问题,就继续确认当前 Wrangler 登录的是不是正确的 Cloudflare 账号,以及命令里有没有带 --env production。
正式部署之前,我们可以先做一次 dry-run:
1cd apps/api2pnpm wrangler deploy --env production --dry-run
dry-run 不会真正发布 Worker,但会帮我们检查配置、入口文件、构建产物和绑定资源。
如果配置没有问题,输出里会出现 bindings 列表,例如:
1env.DB (ai-agent-production-auth) D1 Database2env.AVATAR_BUCKET (ai-agent-production-avatars) R2 Bucket3env.APP_ENV ("production") Environment Variable4env.ADMIN_ORIGIN ("https://ai-agent-admin.pages.dev")5env.WEB_ORIGIN ("https://ai-agent-web-66e.pages.dev")
这段输出很有价值。它能直接告诉我们:线上 Worker 最终拿到的 D1、R2 和环境变量,是否就是我们期望的那一组资源。
dry-run 通过以后,就可以正式部署 API:
1cd apps/api2pnpm wrangler deploy --env production --minify
部署成功后,会看到类似这样的输出:
1Uploaded api-production2Deployed api-production triggers3https://api-production.1832064870.workers.dev4Current Version ID: bb6a160e-4f37-4736-8932-1a4ea8278a38
这里有两个信息需要记下来:
1API URL2Current Version ID
其中 API URL 后续要写入 admin / web 的生产环境变量:
1NEXT_PUBLIC_API_BASE_URL=https://api-production.1832064870.workers.dev
GitHub OAuth 的回调地址要填 API 子站地址:
1https://api-production.1832064870.workers.dev/auth/web/github/callback
这里不要填 Web 子站地址。GitHub 回调回来以后,真正处理 code、换取 GitHub access token、创建系统登录态的地方,是 API 子站。
当前代码里,GITHUB_OAUTH_CALLBACK_URL 是可选的:
1callbackUrl: env.GITHUB_OAUTH_CALLBACK_URL2?? new URL('/auth/web/github/callback', c.req.url).toString()
所以 production 里可以先这样写:
1"GITHUB_OAUTH_CALLBACK_URL": ""
空字符串会被环境变量解析逻辑当成未配置。这样运行时就会使用当前 API 请求域名,自动拼出 callback URL。
不过 GitHub OAuth App 后台仍然必须配置最终回调地址:
1https://api-production.1832064870.workers.dev/auth/web/github/callback
API 里 CORS 会检查:
1const allowedOrigins = new Set([env.ADMIN_ORIGIN, env.WEB_ORIGIN])
所以 admin / web 部署完成以后,要把真实 Pages 生产域名写回 wrangler.jsonc。
本次最终使用的是这两个 origin:
1ADMIN_ORIGIN=https://ai-agent-admin.pages.dev2WEB_ORIGIN=https://ai-agent-web-66e.pages.dev
这里特别容易写错。Web 的实际域名不是猜出来的 ai-agent-web.pages.dev,而是 Cloudflare 实际分配的:
1https://ai-agent-web-66e.pages.dev
修改 WEB_ORIGIN 后,需要重新部署 API:
1cd apps/api2pnpm wrangler deploy --env production --dry-run3pnpm wrangler deploy --env production --minify
如果只改配置文件但不重新部署,线上 Worker 仍然会使用旧 origin,浏览器请求时就会遇到 CORS 问题。
把整个过程连起来看,推荐按这个顺序部署:
011. 创建 D1 / R2022. 配置 apps/api/wrangler.jsonc033. 设置 production secrets044. 执行 D1 migrations055. dry-run API066. deploy API077. 拿到 API workers.dev 域名088. 配置 admin / web 的生产 API 地址099. 部署 admin / web 到 Pages1010. 拿到真实 Pages 生产域名1111. 回写 API 的 ADMIN_ORIGIN / WEB_ORIGIN1212. 重新 deploy API1313. 配置 GitHub OAuth callback URL
这样安排会更稳一些。API 先有稳定地址,前端构建时就能写入正确的 API 地址;等前端部署完成以后,再把真实 Pages 域名同步回 API 的 CORS origin;GitHub OAuth 放在最后配置,也能避免一开始就把 callback URL 猜错。
查看当前登录账号:
1pnpm wrangler whoami
查看 D1:
1pnpm wrangler d1 list
创建 D1:
1pnpm wrangler d1 create ai-agent-production-auth
创建 R2:
1pnpm wrangler r2 bucket create ai-agent-production-avatars
设置 secret:
1pnpm wrangler secret put JWT_ACCESS_SECRET --env production2pnpm wrangler secret put JWT_REFRESH_SECRET --env production3pnpm wrangler secret put DEEPSEEK_API_KEY --env production4pnpm wrangler secret put GITHUB_OAUTH_CLIENT_SECRET --env production
执行 D1 迁移:
1pnpm wrangler d1 migrations apply ai-agent-production-auth --env production --remote
部署前检查:
1pnpm wrangler deploy --env production --dry-run
正式部署:
1pnpm wrangler deploy --env production --minify
API 生产地址:
1https://api-production.1832064870.workers.dev
最近一次部署版本:
1bb6a160e-4f37-4736-8932-1a4ea8278a38
前端生产地址:
1Admin: https://ai-agent-admin.pages.dev2Web: https://ai-agent-web-66e.pages.dev
GitHub OAuth 回调地址:
1https://api-production.1832064870.workers.dev/auth/web/github/callback
这次部署完成以后,apps/api 会作为一个独立的 Cloudflare Worker 对外提供服务;admin 和 web 仍然按静态前端子站部署到 Cloudflare Pages。两边通过明确的环境变量、API 地址和 CORS origin 连接起来,部署关系就清楚了,也方便后面继续维护。