成为全栈·Node 后端篇·部署上线:从本地起服到真正对外服务

成为全栈·Node 后端篇·部署上线:从本地起服到真正对外服务

本地 pnpm start 跑通,和"用户能从公网访问你的接口"之间,隔着环境变量、数据库迁移、HTTPS、CORS、进程守护整整一串事。很多人第一次部署卡住的地方,其实不是代码,而是"我到底该配哪些东西"。

这一篇对照 node-backend 的真实代码,讲清两条部署路线(自管 Linux 与 Cloudflare Workers)、部署真正的"旋钮"------环境变量,以及一份翻车对照表:症状、根因、代码依据,照着排就能定位。

一、部署不是"把代码传上去"那么简单

本地起服和线上部署有三个本质差异,漏掉任何一个都会出事:

  1. 配置来源不同 :本地靠 .env 文件,线上靠服务器环境变量 / 平台绑定。配置一旦没注入,进程可能带着空密钥静默运行(最危险的 bug 之一)。
  2. 存储位置不同 :本地 SQLite 文件在硬盘上,上传文件在 uploads/。线上这些要么挂持久卷,要么换对象存储,否则容器/实例一重启全丢。
  3. 流量入口不同 :本地是 localhost:3000,线上要走域名 + HTTPS + 反向代理,还要处理跨域、限流、健康检查。

我们的项目还多了第四个差异:双运行时。这引出了本章的重点。

二、我们真实的两条部署路线(一套代码,两端落地)

src/ 就能确认,项目故意保留了两个入口,对应两种部署目标:

  • src/index.ts :Node 自管 Linux 部署入口。用 @hono/node-serverserve 起服务,顶层 await migrate(db) 后再 setDb(db)
  • src/worker.ts :Cloudflare Workers 部署入口。wrangler.tomlmain = "src/worker.ts"。它导出 { fetch },每次请求注入 CF 绑定(DB 即 D1、R2_BUCKET 即 R2),调用同一个 createApp(env)

两个入口复用同一套 createApp ------这是"一套代码双部署"的底层支撑(裁决 Q5 的落地)。业务路由、中间件、service 层一份代码写死,只在最外层的"怎么拿到 DB / 怎么起服务"上分叉。所以"部署"对我们来说不是选一个目标,而是两条路线都要会

两条路线对"迁移"的处理还不一样,这是真实细节、也是常见坑:

  • Node 入口在运行时 await migrate(db)------进程起来先把表建好,不怕空库。
  • Workers 入口不在运行时迁移worker.ts 注释明确写"D1 迁移在 deploy 阶段通过 drizzle-kit 应用,运行时不再迁移"。

为什么分叉?因为 Workers 是边缘无状态运行时,每次冷启动都跑一遍迁移既慢又可能在并发下打架;D1 的迁移更适合在 CI deploy 阶段drizzle-kit 一次性应用。这条差异不是我编的,是两个入口代码白纸黑字写着的------部署脚本必须分别对待。

三、环境变量:部署真正的"旋钮"

我们的配置收口在 src/config/env.tsreadEnv,它用 zod 定义了所有外部配置。部署前,先把这个 schema 读熟,它就是你的"部署清单":

ts 复制代码
// src/config/env.ts
const schema = z.object({
  JWT_SECRET: z.string().min(1, 'JWT_SECRET 必填'),
  DB_FILE: z.string().default(':memory:'),
  STORAGE_DRIVER: z.enum(['local', 'r2']).default('local'),
  CORS_ORIGINS: z.string().default('*'),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.string().default('3000'),
  // CF 绑定(本地为 undefined)
  R2_BUCKET: z.unknown().optional(),
  DB: z.unknown().optional(),
});

逐项对应部署动作:

  • JWT_SECRETmin(1) 必填。线上必须 设成强随机值。如果漏设,Node 入口在模块加载时 readEnv 直接抛错、进程退出------这是好事,宁可起不来也不要带空密钥签发 token。
  • DB_FILE :本地默认 :memory:(测试友好),但生产绝不能内存库 。Node 部署要把它指到持久卷里的真实路径(如 /data/app.db);Workers 路线根本不用这个字段,改用 DB(D1 绑定)。
  • STORAGE_DRIVERlocalr2。它决定文件落本地磁盘还是对象存储。部署到哪端,就拧到对应值。
  • CORS_ORIGINS :默认 *生产必须填白名单 (逗号分隔的具体域名),否则任意网站都能带用户 cookie 调你的接口。审阅 B05 明确:留空或 * 应被拒绝跨域。
  • PORTsrc/index.tsserve({ port }) 读它,默认 3000,反向代理 / 容器映射要对得上。

这套"配置单一入口 + zod 校验"的设计,本身就是部署安全网:业务代码不直接读 process.env,所有配置经 readEnv 解析校验后注入。部署时少配了关键项,启动即失败(fail fast),而不是带着残缺配置跑半夜。

四、路线 A:Node 自管 Linux 部署

适合你有一台自己的服务器(云主机 / 内网机),想完全掌控。步骤:

  1. 构建与起服pnpm installpnpm starttsx src/index.ts)。如前篇所述,生产建议用 Docker 镜像起,或由 pm2 / systemd 托管进程,保证崩溃自动重启。
  2. 持久化DB_FILE 指向挂载卷路径;STORAGE_DRIVER=localuploads/ 也挂卷,且目录要对运行用户可写。
  3. 反向代理 + HTTPS:不要直接把 3000 端口暴露公网。前面架一层 nginx / Caddy 做 TLS 终止、域名路由、 gzip。Caddy 甚至能自动签 Let's Encrypt 证书,省去手动续期。
  4. CORS 白名单CORS_ORIGINShttps://www.example.com,https://admin.example.com,和前端域名严格对应。
  5. 首部署补数据 :容器/进程起来后跑一次 pnpm seed 创建初始账号(index.ts 已自动 migrate,无需手动建表)。

这套路线的好处是"所见即所得"------和你本地开发环境几乎一致,排错直觉通用。代价是要自己管操作系统、TLS、进程守护、数据库备份。

五、路线 B:Cloudflare Workers 部署

适合想免运维、自动全球边缘加速、按量付费。步骤:

  1. wrangler.toml 配置main = "src/worker.ts"compatibility_date、在 [vars]JWT_SECRET / NODE_ENV=production / STORAGE_DRIVER,并解开注释启用 [[d1_databases]](填 database_id)与 [[r2_buckets]] 绑定。
  2. D1 迁移在 deploy 阶段 :本地 / CI 跑 drizzle-kit migrate(或 wrangler d1 migrations apply),把迁移文件应用到 D1。运行时 worker.ts 不再迁移。
  3. wrangler deploy :把 worker.ts + 打包后的代码推到边缘。createApp(env) 每次请求用注入的 D1 / R2 绑定构造。
  4. TLS 与域名*.workers.dev 自带 HTTPS,绑自定义域名也在控制台点几下。无需自己管反向代理。
  5. 存储切换STORAGE_DRIVER=r2 时,文件走 R2 绑定,Worker 本身无状态,天然利于水平扩容。

Workers 路线的代价:它是边缘无状态运行时,不能用本地文件系统(所以 uploads/ 本地磁盘模式在 Workers 上无效,必须 R2);冷启动、运行时限制(CPU 时间、子请求数)也要留意。这正是我们"双存储适配层"存在的意义------STORAGE_DRIVER 一拧,存储策略跟着部署目标切换。

六、STORAGE_DRIVER 是部署的"存储开关"

这一点值得单独强调,因为它最容易被部署时忽略。我们的附件存储是适配层 + 配置驱动

  • 部署到自管 Linux + STORAGE_DRIVER=local:文件落 uploads/,必须挂持久卷,且要注意运行用户对该目录的写权限(USER appuser 后挂载点可能属 root,要在 entrypoint 里 chown)。
  • 部署到 Cloudflare + STORAGE_DRIVER=r2:文件走 R2 绑定,Worker 无状态,卷都不用挂。

同一个 uploadAttachment 接口,部署目标不同,落点完全不同。部署清单里这一项必须和部署路线对齐,否则要么文件写不进(本地盘没挂)、要么引用了不存在的 R2 绑定(Workers 启动报错)。

七、CORS:生产别留 *

env.tsCORS_ORIGINS 默认是 *,这是为本地开发省事。但生产环境 * 配合 credentials 在浏览器里本就被禁止,且等于对全网开放跨域------审阅 B05 因此要求生产填具体白名单。部署时这一步是硬动作 :把前端管理后台域名、官网域名逗号分隔写进 CORS_ORIGINS。漏做的话,前端上线后调接口会被浏览器 CORS 拦截,表现就是"本地好端端的,一上线就跨域报错"。

八、部署翻车对照表:症状 → 根因 → 代码依据

讲完正确做法,再用一张"翻车对照表"把上面每一条落进真实症状。这些都是对照我们代码能推出来的、部署时高频出现的问题,先于读者踩坑前点破:

上线症状 根因 代码依据
Node 进程一启动就退出,日志报 JWT_SECRET 必填 生产忘了设密钥,readEnv 在模块加载即抛 env.tsJWT_SECRET: z.string().min(1)
本地好端端,Worker 一请求就 no such table 忘了在 deploy 阶段跑 D1 迁移,且运行时不再迁移 worker.ts 注释"D1 迁移在 deploy 阶段应用"
Node 起服后首请求偶发 500、日志说表不存在 migrateawait 就起服(我们已 await,但若有人改成 fire-and-forget 就会中招) index.ts 顶层 await migrate(db) 后再 serve
本地能上传,线上上传接口 500 STORAGE_DRIVER 与部署目标错配:Worker 上设了 local 却无本地盘 env.ts enum 仅 local/r2,本地盘在 CF 不存在
前端一上线调接口就被浏览器 CORS 拦 CORS_ORIGINS 仍是默认 *,生产应白名单 env.ts default '*' + 审阅 B05
容器重启后文章/用户全没了 DB_FILE 用了默认 :memory:,没挂持久卷 env.ts DB_FILE default ':memory:'
管理后台登录后调接口 401 但本地正常 JWT_SECRET 生产/本地不一致,签名对不上 env.ts 单一 JWT_SECRET 收口,缺则 fallback 到旧值

这张表的每一行,都能在 env.ts / index.ts / worker.ts 里找到确切出处------它不是"别人说会出这问题",而是从我们自己的代码推出来的。部署前照着逐行核对,能把大部分上线事故消灭在发版前。

九、部署核对清单(以实测为准)

把上面所有点收成一个"部署前核对",每一项都要 ls / 读代码确认,不靠记忆:

核对项 真实来源 生产要求
JWT_SECRET 已设 env.ts min(1) 强随机,缺失即启动失败
DB 持久化 index.ts DB_FILE / worker.ts DB 挂载卷或 D1 绑定
STORAGE_DRIVER 对齐 env.ts enum local 挂盘 / r2 绑 R2
CORS 白名单 env.ts default * 填具体域名,禁 *
迁移已应用 index.ts 运行时 / worker.ts deploy 阶段 两侧策略不同,勿混
端口/域名对齐 PORT + 反向代理 与 upstream 一致

十、小结

部署上线是把"能跑的代码"变成"对外服务",核心差异在配置来源、存储位置、流量入口,以及我们的双运行时:

  1. 两条路线 :Node 自管 Linux(index.ts,运行时 migrate)+ Cloudflare Workers(worker.ts,deploy 阶段 migrate),共用 createApp
  2. 配置即清单readEnv 的 zod schema 就是部署清单------JWT_SECRET 必填、DB_FILE 不能内存库、STORAGE_DRIVER 选 local/r2、CORS_ORIGINS*
  3. 存储开关STORAGE_DRIVER 随部署目标拧,local 要挂卷、r2 要绑 R2,两侧不可混。
  4. 迁移分叉:Node 运行时建表,Workers 靠 drizzle-kit 在 deploy 阶段建,部署脚本要对号入座。
  5. fail fast :配置缺失 readEnv 直接抛错,宁可起不来也不带残缺配置跑。

下一篇({{LINK:M1-24}})我们专门拆解"一套后端双部署"这个架构决策:为什么这么设计、两个入口如何共享 createAppdb/client.ts 怎么同时适配 SQLite 和 D1,以及适配层里那些值得记一辈子的坑。


如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」

🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html

📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer

相关推荐
妙码生花5 小时前
golang 应用服务端部署(使用 systemd 服务)
开发语言·人工智能·后端·golang·node.js·php·gin
柚稚姐姐6 小时前
npm install pnpm -g npm error code EACCES npm error syscall symlink
前端·npm·node.js
FungLeo1 天前
成为全栈·Node 后端篇·容器化:给 Node 应用写一个像样的 Dockerfile
docker·node.js·dockerfile·成为全栈·后端服务容器·docker 容器
WeiXin_DZbishe1 天前
基于springboot大学生提问箱系统-计算机毕设【课程设计】72593
javascript·vue.js·spring boot·vscode·python·node.js·php
xixiaoyunya1 天前
Docker 容器化部署实战:从零搭建一套完整的 Nginx + Node.js + MySQL + Redis 项目环境
nginx·docker·node.js
晴天161 天前
npm install -f(--force)深度解析:作用原理、报错根源与风险避坑指南
前端·npm·node.js
还是大剑师兰特1 天前
Swagger 在 Node.js 项目中的安装和完整使用方法
node.js
FungLeo2 天前
成为全栈·Node 后端篇·全文搜索:从 LIKE 到全文索引
node.js·全文索引·模糊搜索·全文搜索·成为全栈·like 搜索
倾颜2 天前
AI Chat 长会话性能实践:消息虚拟化、动态高度与流式滚动设计
前端·react.js·node.js