部署

把 ShipKit 部署到 Cloudflare Workers:创建 D1 和 R2、推送 secret、执行迁移、部署、绑定域名,并为生产环境接好 OAuth、webhook 和邮件。

一个 ShipKit 部署就是一个 Cloudflare Worker。预渲染的营销页从它的静态资源里直接返回,其余请求由代码处理;它通过 wrangler.jsonc 里的 binding 使用 Cloudflare 提供的三样东西:一个 D1 数据库(DB)、一个 R2 存储桶(BUCKET)和两个限流器。每天一次的 cron 触发器负责跑维护任务。除此之外,没有别的服务器要管。

用 Claude Code 的话,/deploy 会完成本页的全部步骤,只向你索要生产环境的值。下面写的就是它执行的那些步骤。

首次部署之前

登录 Cloudflare:

bunx wrangler login
bunx wrangler whoami

给项目起名。wrangler.jsonc 里有三处写着 shipkit——name(Worker 名)、database_name 和 bucket_name——package.json 的 name 也是。把它们改成你的项目名。所有脚本都通过 binding 名 DB 访问数据库,其他地方不用跟着改。

然后过一遍 src/config/app-config.ts,里面全是占位值:

  • url:生产环境的源地址,决定 canonical、hreflang 链接和 sitemap。
  • emailFrom 和 supportEmail:每封邮件的发件地址和 Reply-To(见下面的清单)。
  • googleClientId:三元表达式的第二个分支是生产环境用的 id,One Tap 在浏览器里需要它。
  • legalEntity:会原样印在服务条款和隐私政策页上。

src/config/ 里的其他配置见配置。

创建 D1 和 R2

bunx wrangler d1 create <database_name>

把输出的 database_id 填进 wrangler.jsonc,替换 REPLACE_ME。然后:

bunx wrangler r2 bucket create <bucket_name>
bun run cf-typegen
bun run db:migrate:remote

即使删掉了文件功能,也要保留这个 R2 存储桶:用户头像也存在里面。

Secret

本地开发读 .dev.vars;部署后的 Worker 读的是你用 wrangler secret put 推上去的 secret。.dev.vars 里的内容不会进入生产环境。

bunx wrangler secret put APP_URL
bunx wrangler secret put BETTER_AUTH_SECRET
# ……每个键一次

更喜欢用文件的话,把生产环境的值放在 .prod.vars(已加入 gitignore),用 bunx wrangler secret bulk .prod.vars 一次推送。无论哪种方式,都在密码管理器里留一份。

Secret是否需要说明
APP_URL必需https://your-domain,结尾不带斜杠
BETTER_AUTH_SECRET必需生产环境重新生成一个:openssl rand -base64 32
GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET必需缺了它们应用无法启动
PAYMENT_PROVIDER要收款时stripe 或 creem;留空则购买按钮保持关闭
STRIPE_SECRET_KEY、STRIPE_WEBHOOK_SECRET使用 Stripe 时用生产 endpoint 的 webhook secret,不是 stripe listen 打印的那个
CREEM_API_KEY、CREEM_WEBHOOK_SECRET使用 Creem 时
RESEND_API_KEY实际上必需没有它就不会发出任何邮件,包括登录验证码
GITHUB_CLIENT_ID、GITHUB_CLIENT_SECRET可选要么都配,要么都不配
DEMO_MODE永远不要设只用于单独的公开演示部署

完整列表和每个键的用途见配置。环境变量在 Worker 第一次读取时校验:缺少必需的键或格式不对,会抛出带键名的错误,在 Worker 日志里可以看到。

部署与检查

bun run deploy

这条命令会构建、预渲染营销页、生成 sitemap,然后执行 wrangler deploy。在终端里检查结果:

curl -s -o /dev/null -w "%{http_code}" https://your-domain/            # 200
curl -s -o /dev/null -w "%{http_code}" https://your-domain/dashboard   # 307,跳到 /login
curl -s https://your-domain/pricing | grep -c hreflang                 # 3

自定义域名

部署完成后,Worker 立刻可以通过 workers.dev 子域名访问,做冒烟测试足够了。正式域名需要取消 wrangler.jsonc 里 routes 那一行的注释,填上你的域名:

"routes": [{ "pattern": "your-domain.com", "custom_domain": true }],

该域名的 zone 必须在同一个 Cloudflare 账号下。也可以在控制台里绑定(Workers → 你的 Worker → Settings → Domains & Routes)。Google 登录只在用户实际访问的那个源地址上有效,所以请在邀请任何人之前完成这一步。

www 跳转到主域名,最简单的做法是加一条 Cloudflare 重定向规则。如果加不了,infra/www-redirect/ 里有一个只做这件事的小 Worker:在它的 wrangler.jsonc 里填上域名,然后运行 bun run deploy:www。

上线清单

这些配置都在仓库之外,也是新部署“只能用一半”的常见原因。

Google。 在本地用的同一个 OAuth 客户端里,把 https://your-domain 加到“已获授权的 JavaScript 来源”,把 https://your-domain/api/auth/callback/google 加到“已获授权的重定向 URI”。One Tap 检查的是来源列表,跳转登录检查的是重定向列表。

GitHub(如果启用了)。把 OAuth app 的回调地址设为 https://your-domain/api/auth/callback/github。

支付。 用 Stripe 的话,在 https://your-domain/api/webhooks/stripe 建一个 webhook endpoint,订阅 checkout.session.completed、checkout.session.async_payment_succeeded、invoice.paid、invoice.payment_failed、customer.subscription.updated、customer.subscription.deleted、charge.refunded、credit_note.created 和 charge.dispute.closed,再把它的签名密钥推上去。正式收款意味着一套 live 模式的商品目录,src/config/plans.ts 里的价格 id 也要全部换掉。用 Creem 的话,把 webhook 指向 https://your-domain/api/webhooks/creem,并换成生产环境的 API key 和商品 id。详见支付。

邮件。 在 Resend 里验证你的发信子域名(模板默认用 send.<your-domain>,这样它的 SPF 和 DKIM 记录不会和收信服务冲突),把 emailFrom 设成该子域名下的地址,并确保 supportEmail 能收到信——客户回复收据时就会发到这里。没有 RESEND_API_KEY 时,邮件验证码只会写进 Worker 日志,依赖验证码登录的用户就登不进来。详见邮件与通知。

第一个管理员。 先登录一次,然后:

bunx wrangler d1 execute DB --remote \
  --command "UPDATE user SET role='admin' WHERE email='you@example.com'"

后续部署与迁移

schema 有变化时,部署分两步,顺序不能反:

bun run db:migrate:remote
bun run deploy

先迁移。如果代码需要的列数据库里还没有,每个用到它的请求都会失败。迁移永远是手动执行的,仓库里没有任何东西会替你跑。见数据库。

通过 CI 部署

.github/workflows/ci.yml 会在每次 push 和 pull request 时跑 typecheck、lint、单元测试、构建和 Playwright 测试。push 到 main 时还会接着部署——前提是你给了它凭据:在 GitHub 仓库里建一个名为 production 的 environment,把 CLOUDFLARE_API_TOKEN 和 CLOUDFLARE_ACCOUNT_ID 加为它的 secret。没有这两个值时,部署任务会输出“已跳过”并正常结束。这个 token 需要能部署 Worker、能读取你的 D1 数据库。

CI 从不执行迁移。部署前它会列出远程迁移,只要有未应用的就拒绝继续。先自己运行 bun run db:migrate:remote,再重新触发工作流(gh workflow run CI,或者点 Run workflow 按钮)。

运行时的 secret 从不经过 CI,它们留在 wrangler secret put 放的地方。CI 构建时用的是占位值。

部署之后的日志、定时任务和限流见运维,常见故障见排错。