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:/swapfile 2G 生效后才继续;装完 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 日实操档案;服务器账号与密钥细节不在本文范围。