部署
把 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 构建时用的是占位值。