10 月 10 日 18:56,我的 Nuxt3 企业管理系统 my-nuxt-app 在 99 元/年的阿里云 ECS 上跑起来了。从下单到 10/20 博客挂上 HTTPS,十一天踩的坑我全记在部署档案里——这篇按「现象→根因→修法」完整放出,给同样用便宜机器的人照单排查。
机器与前置决策
- 机型:阿里云 e 实例 2 核 2G,99 元/年续费同价,深圳(长沙延迟约 20ms),公网 IP 47.112.2.228
- 系统:Ubuntu 22.04.5,不勾任何预装应用(宝塔/WordPress/Docker/LNMP 全不要,避免占用 2G 内存)
- 内存预算:nginx 64MB + node-app 384MB + mysql 512MB + ai-demos 512MB ≈ 1.2G,加系统开销很紧——开工先加 swap:
/swapfile2G 生效后才继续;装完 Docker 实测 used 242Mi / available 1.3Gi,健康 - 向量/模型推理一律走云端 API,不在本机跑——2G 内存的机器想都别想
坑 1:Docker 官方脚本被网络重置
现象:curl get.docker.com | sh 直接失败。
根因:官方安装源网络不通。
修法:apt 装 docker.io(装出来 Docker 29.6.2,够用)。别跟官方脚本较劲。
坑 2:个人版镜像加速器白名单
现象:配了阿里云专属加速器(写入 /etc/docker/daemon.json)后,nginx:alpine 秒拉,node:22-alpine、node:24-alpine 全部报 not found。
根因:个人版加速器有白名单,node 新版镜像不在名单里——不是你的 compose 写错了。
修法:加 daocloud 源 https://docker.m.daocloud.io 兜底,放进 daemon.json 的 mirrors 数组首位。
坑 3:compose 旧 command 让容器永远起不来
现象:容器在,端口不通,日志里一直循环输出等待信息。
根因:compose 的 command 还是老模板里「等 server.js 出现」的死循环——而 Nitro 生产入口根本不是 server.js。
修法:command 直接换成 node .output/server/index.mjs,配 working_dir。上线架构:/root/app 下 compose 双容器,nginx:80 → node-app:3000(注意 service 名是 node-app 不是 app,互指会报错)。
坑 4:Windows 构建产物混入 win32 原生包
现象:本地 Windows npm run build 产物传服务器,Linux 容器里起不来,报错指向 @img/sharp-win32-x64、@node-rs/jieba-win32-x64-msvc 这类包。
根因:跨平台构建,产物里带了 Windows 专属二进制。
修法(临时):从 .output/server/package.json 删掉所有 win32|msvc 条目 → 容器内 npm i --omit=dev(npmmirror 源)换 Linux 二进制。还有个 npm optionalDependencies 的 bug:@node-rs/jieba-linux-x64-gnu@2.0.2 不会自动装,要手动补装。
坑 5:无扩展名 import——dev 能跑,生产 ESM 必炸
现象:import '@node-rs/jieba/dict' 本地 dev 一切正常,生产启动直接报错。
根因:无扩展名 import 在纯 ESM 生产环境解析不了。
修法(临时):产物 sed 成 dict.js——注意 index.mjs 和 chunks/ 都要扫。
修法(根治,10/12 做的):分词器整体迁移 @node-rs/jieba → segmentit@2.0.3(纯 JS),坑 4、5 一并消灭:
- package.json 卸 jieba 装 segmentit
server/utils/jieba.ts重写:useDefault(new Segment()).doSegment取词,补 WORD_ONLY 正则滤标点nuxt.config.ts加nitro.externals.inline: ['segmentit']——CJS 命名导入的坑,产物实测炸过一次,内联解决- 新增
types/segmentit.d.ts
验证:本地 npm run build 通过,node .output/server/index.mjs 启动打出 [bm25] 索引预热完成:172 chunk/3015 词项/2688ms——生产路径真跑通分词,.output 内无 node-rs 残留。从此「本地 build → 传产物 → 重启」就完事,不用登服务器装依赖。
坑 6:Nitro 生产不读 .env
现象:明明 .env 在,生产容器里环境变量全空。
根因:Nitro 只在 dev/build 阶段读 .env,生产运行时不加载——这是文档里写着的,但踩了才有记忆。
修法:compose 加 env_file: .env 注入。顺带确认:TiDB Cloud(东京 gateway:4000)从 ECS 直连无白名单拦截,连通✓。
坑 7:certbot 续期的两个细节
10/20 给 blog.zjkun.top 上 HTTPS(A 记录 → 47.112.2.228,Let’s Encrypt 有效期到 2027-01-18):
- 证书用 docker 版 certbot webroot 方式签发;nginx 80 端口只留
/.well-known/acme-challenge/,其余 301→https - 自动续期:
/etc/cron.d/certbot-renew每日 04:30 跑certbot renew --webroot && nginx reload - 踩坑:首次 dry-run 被「残留实例锁」挡过——之前手工跑的 certbot 会话没退干净。kill 旧进程后正常。看到 lock 报错先查进程,别怀疑配置
一键部署脚本
博客现在是纯静态挂载,不动 node-app 容器:
# deploy_blog.ps1:本地 hugo 构建 → tar → scp → 服务器解压
$ErrorActionPreference = 'Stop'
& 'D:\tools\hugo\hugo.exe' -s D:\study\blog --minify -d public
if ($LASTEXITCODE -ne 0) { throw 'hugo build failed' }
tar.exe -czf D:\study\blog_dist.tar.gz -C D:\study\blog\public .
scp -o BatchMode=yes D:\study\blog_dist.tar.gz root@<server>:/root/app/blog_dist.tar.gz
ssh -o BatchMode=yes root@<server> "rm -rf /root/app/blog/*; tar -xzf /root/app/blog_dist.tar.gz -C /root/app/blog; rm /root/app/blog_dist.tar.gz; echo DEPLOYED"
改 nginx/compose 前一律先备份(blog.conf.bak-20261020b、docker-compose.yml.bak2),改坏能秒回滚。
还开着的坑(如实说)
GET /api/document仍 500:Cannot find package 'canvas'(nitro externalized 但 package.json 没声明),修法已写明待执行- 产物里 sharp 仍是原生包,「零重装部署」要等它也纯 JS 化才算完全达成
CHROMA_URL还指 localhost:8000,用到向量检索时要处理;restart: always待确认
收个尾
七个坑里六个是同一类病:「本机能跑」≠「Linux 容器里能跑」——平台二进制、模块解析、环境变量,全是跨环境的那三堵墙。2 核 2G 的机器跑 nginx+node-app 绰绰有余,重活(数据库、向量、大模型)全部外包给云 API,99 元的快乐就回来了。
上线全记录基于 2026 年 10 月 10-20 日实操档案;服务器账号与密钥细节不在本文范围。