七道坎:Hono + React Monorepo 部署腾讯云记
部署这件事,像搬家。你在本地住得好好的,一切顺畅,直到有一天需要让别人也能来访。于是你租了一台云上的毛坯房(Ubuntu 24.04,3.6G 内存,裸机状态),开始往里搬东西。
架构并不复杂:Nginx 在门口迎客,负责端上前端静态文件,遇到 API 请求就转交给后面的 Hono 后端;PM2 看着后端进程,防止它悄悄倒下;Let's Encrypt 发了一张门禁卡,叫 HTTPS。听起来井然有序。
然而搬家的麻烦从来不在蓝图上,而在蓝图与现实之间的缝隙里。以下是七道坎,每一道都曾让进度条停在某个尴尬的百分比。
一、Vite 与 npm workspaces 的路径幻觉
monorepo 的 package.json 里写着 "build": "npm run build -w @ap2/web",看上去很规整。Vite 的配置里也写了 envDir: path.resolve(__dirname, '../..'),指向 monorepo 根目录去读 .env。
两件各自正确的事,合在一起就出了问题。workspace 模式下执行构建,__dirname 的解析路径发生了微妙的偏移,Vite 找不到 .env,于是所有 VITE_* 环境变量静静地消失了。前端构建成功,打开页面,一片空白,控制台里冷冷地报 Missing VITE_SUPABASE_URL。
解决:不走 workspace 委托,老老实实 cd 进子目录构建:
| |
路径问题的本质是信任问题。你以为框架替你处理了,其实它只是把球踢给了操作系统。
二、PM2 与环境变量的失联
PM2 配置文件里写了 env_file: '.env',看起来合情合理。启动后,后端立刻崩溃:Missing SUPABASE_URL environment variable。
翻遍 PM2 文档才发现,env_file 根本不是标准配置项。PM2 不认识它,也不会报错,只是默默忽略。代码里的 dotenv.config() 倒是会找 .env,但 tsx 作为子进程启动时,工作目录未必是你以为的那个。
解决:在 PM2 的启动参数里用 Node.js/tsx 原生的 --env-file 标志:
| |
工具链的「看起来应该支持」和「实际支持」之间,隔着一篇你没读完的文档。
三、Nginx 与用户目录的门禁
后端跑起来了,API 正常。前端文件也在 packages/web/dist/ 里躺着。Nginx 配置了 root,指向正确的路径。然而访问首页,500。
错误日志写得很直白:stat() failed (13: Permission denied)。Nginx 以 www-data 用户运行,而 /home/ubuntu/ 的默认权限是 700,外人不得入内。
解决:
| |
一行命令,消除了一个半小时的困惑。服务器安全和可访问性之间的平衡,常常就差一个权限位。
四、腾讯云的双重城墙
ufw allow 80,443/tcp 执行完毕,防火墙规则确认无误。然而从外部 curl,依然超时。在服务器本地 curl localhost,一切正常。
原来腾讯云轻量应用服务器有两层防火墙:操作系统层的 ufw 只是内城墙,外面还有一道云平台层的防火墙(控制台 → 实例 → 防火墙),两道都要放行。
排查技巧:本地通、外部不通 → 先去云平台控制台看防火墙规则,别在 iptables 里钻牛角尖。
城墙叠城墙,安全固然是好事,只是排查问题时要记得,你面对的可能不是一堵墙。
五、国内服务器与 GitHub 的距离
安装 nvm 的第一步是从 raw.githubusercontent.com 下载安装脚本。在国内服务器上,这个域名几乎等同于不存在。curl 的进度条在 0% 停了两分钟,像一根不会动的时针。
解决:换用 gitee 镜像,Node.js 二进制文件走 npmmirror:
| |
在国内做开发,镜像不是锦上添花,是基础设施。
六、Let's Encrypt 的跨洋握手
certbot 运行,等待 Let's Encrypt 验证服务器从境外访问国内 IP 的 80 端口。第一次,超时。错误信息里写着 Timeout during connect (likely firewall problem),但防火墙确实已经开了。
不是防火墙的问题,是太平洋的问题。跨国网络链路偶尔丢包,验证服务器恰好没在超时窗口内收到响应。
解决:确认配置无误后,再跑一次 certbot。第二次,顺利通过。
有些问题的解法是等待和重试,而不是继续排查。这需要一点判断力,也需要一点耐心。
七、monorepo 的构建次序
前端构建报了一屏 TypeScript 错误:Output file '…/shared/dist/types/index.d.ts' has not been built from source file。shared 包配置了 composite: true,但没有 build 脚本,也没有人事先调用过 tsc -b。
解决:在构建前端之前,先编译 shared 包的类型声明:
| |
monorepo 的好处是共享代码,代价是共享构建顺序。忘了其中一环,后面全部塌方。
七道坎,没有一道是致命的,也没有一道是显而易见的。它们藏在文档的角落、工具链的默认行为、云平台的产品设计里,等着每一个第一次搬家的人。
记下来,不是为了炫耀踩过多少坑,而是下次搬家时,少在门口站一会儿。