在 VPS 上执行 npm run build、pnpm build 或 Docker 镜像构建,Next.js 突然只打印 Killed,或者出现 JavaScript heap out of memory?先别把 NODE_OPTIONS 直接设成 8192,也别反复重跑。V8 堆达到上限、Linux 内存不足和构建容器受限,是不同的问题。 调错边界,可能让原来的构建失败变成网站或数据库也被杀掉。
本文按“保留失败信息 → 确认真实内存限额 → 区分原因 → 调整构建 → 验证部署”的顺序排查,命令面向 Linux VPS。目标是恢复可靠的生产构建;next dev 能启动,不能代替 next build 的验收。
| 失败信息 | 优先检查 |
|---|---|
FATAL ERROR、Reached heap limit、JavaScript heap out of memory | V8 堆使用、已有 Node 参数,以及项目构建负担 |
只有 Killed 或退出码 137 | 内核/容器 OOM 证据,也检查手动终止、CI 取消和监督进程 |
Docker 显示 OOM 或 cgroup 的 oom_kill 增加 | 真实构建容器、BuildKit 或 CI 作业的资源边界 |
ENOSPC、No space left on device | 磁盘或 inode,不能用增加 Node 堆解决 |
| TypeScript、模块缺失或应用预渲染错误 | 按具体构建错误处理,未必与内存有关 |
退出码 137 通常与 SIGKILL 有关,但不是 OOM 的专用错误码。只凭它无法判断是宿主机缺内存、容器到限额,还是构建被其他机制终止。
在项目目录里确认使用的是已安装依赖中的 Next.js:
node --version
./node_modules/.bin/next --version
./node_modules/.bin/next build --help
node -p "require('./package.json').scripts.build"
先看实际脚本是不是单纯的 next build,还是还包含类型检查、代码生成、数据库迁移或其他任务。报错发生在安装依赖阶段,也不能直接归因于 Next.js 编译。使用项目锁文件和原有包管理器,避免排障时顺便把依赖版本升级了一遍。
需要复现时,保留日志和真实返回值。以下是 npm 项目的例子;pnpm 项目应换成原来的构建命令:
build_exit=0
npm run build > build-memory.log 2>&1 || build_exit=$?
tail -n 80 build-memory.log
printf 'build exit code: %s\n' "$build_exit"
记下失败时间和最后阶段,例如编译、类型检查或静态页面生成。不要让 tee 等日志管道的成功返回值掩盖构建失败,也不要在现有生产实例的运行目录中随意反复覆盖 .next。
在 VPS 上查看内存、Swap、磁盘与较大的进程:
free -h
swapon --show
df -h
ps -eo pid,ppid,rss,comm --sort=-rss | head -n 20
重点看可用内存,而不只是总内存或“used”。一台 4 GB VPS 如果同时运行数据库、应用、监控和多个构建,能留给这次构建的空间可能远小于 4 GB。进程 RSS 快照也不能精确表示所有并行进程的瞬时峰值。
在一次可控复现期间采样:
vmstat 1 10
观察后续采样中的换入/换出、CPU 和等待情况。采样可能错过很短的峰值,所以要结合失败时的日志;单次 free -h 正常不代表构建时没有压力。
在宿主机读取与故障时间接近的内核日志:
sudo journalctl -k --since "20 minutes ago" --no-pager \
| grep -Ei 'out of memory|oom-kill|killed process'
核对被杀的 PID、进程名和时间,是否对应这次 Node 或构建工作。没有输出不能完全排除 OOM:日志权限、保存方式和容器可见范围都会影响结果。容器内通常不能直接读取宿主机内核日志。
如果确有宿主机 OOM,先减少同时构建的任务,避免构建与数据库高峰重叠。不要停止关键服务来赌一次构建成功;需要持续稳定地构建大型项目时,应评估更大的构建环境或 CI。
宿主机还有内存,并不代表构建所在的容器或作业还有额度。对于你明确使用的独立构建容器,可以检查状态和资源配置:
docker inspect build-container --format '{{json .State}}'
docker inspect build-container --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
docker stats --no-stream build-container
把 build-container 替换为实际容器名称或 ID。OOMKilled、退出码与配置限额有助于判断;如果只是容器内某个子进程被杀,或者容器已经被删除,也不能只凭一个状态字段否认发生过 OOM。
Docker 的内存限制与 Swap 有各自的规则。容器内 free 展示的值不能可靠代替实际允许使用的资源,详见 Docker 资源限制文档。不要禁用 OOM killer 或取消所有限额来保护构建,否则压力可能转移到宿主机其他服务。
在构建使用的 cgroup v2 环境中,如果下面这些文件对应当前工作负载且可读,可记录构建前后变化:
cat /proc/self/cgroup
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/memory.events
memory.max 是该 cgroup 的硬上限;值为 max 只表示这一层没有设置上限,祖先 cgroup 仍可能限制它。memory.events 中的 oom_kill 记录 OOM 杀进程事件;结合构建前后增量、并行工作负载与时间判断,不能拿一个历史累计值直接归因于当前构建。路径需要按实际挂载与 cgroup 布局确认,不能在宿主机根目录读一次就认定它代表某个构建任务。字段定义见 Linux cgroup v2 文档。
docker compose up 后的应用容器,与执行镜像构建的 Builder 不是同一个资源边界。只修改应用服务的 mem_limit,未必影响 docker build 中的 RUN npm run build。
先识别实际 Builder:
docker buildx ls
docker buildx inspect
若使用 docker-container driver,BuildKit 运行在专用容器里,可以有自己的 memory、memory-swap 等 driver 设置;其他 driver、远端 Builder 和 CI Runner 也有各自限制。按实际环境调整,参考 Docker container driver 文档。不要把线上应用容器的状态当作镜像构建是否 OOM 的证明。
--max-old-space-size 控制 V8 老生代内存,单位是 MiB。它不是整个 Node 进程、所有子进程或构建容器的总内存上限;原生模块、Buffer、编译器及其他进程还会消耗内存。含义见 Node.js CLI 文档。
如果日志明确是 JavaScript 堆耗尽,而实际资源预算允许调整,可先检查已有 NODE_OPTIONS 中是否有同类参数,再做一次临时测试:
NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=2048" npm run build
2048 只是语法示例,不是 2 GB VPS 的推荐配置。 应根据真实构建额度、当前堆上限、其他进程和原生内存占用选择值;示例值甚至可能低于当前默认堆上限。已有相同或相关的堆参数时应先整理,避免重复与冲突。该环境变量写法只影响这次命令,并保留原字符串中的其他参数。
同一限额下,多个 Node 子进程可能各有自己的堆;给每个进程一个很大的预算,会使总峰值超限。如果原来是系统或容器 OOM,盲目提高 V8 上限通常不会增加可用物理内存,反而可能更早触发系统杀进程。
先确认当前 Next.js 版本和使用的 Bundler。当前官方文档说明 Turbopack 已成为默认选择,而 --webpack 可以切换到 Webpack;旧版本行为不同,应以项目版本和 CLI 帮助为准。参考 Next.js Turbopack 文档。
常见的 experimental.webpackMemoryOptimizations、Webpack build worker 与 Webpack 缓存建议,是 Webpack 构建路径的设置,不能自动解决 Turbopack 的内存问题。Turbopack 包含 Rust 实现,构建中的非 V8 内存也不会因为修改 Node 堆参数就获得更多系统额度。
按失败阶段减少实际工作:
- 检查大型依赖、重复依赖和不必要的服务端/客户端导入,使用匹配 Bundler 的分析工具。
- 静态页面生成时,避免一次把巨大数据集全部加载进内存;审查预渲染数据量与项目需要生成的页面规模。
- 检查插件是否额外启用 source map、分析、代码生成或多次编译;按实际用途减少不必要步骤。
- 暂停同一 Runner 上其他构建,保留真实工作负载的内存采样,不要同时开多个“重试”。
具体诊断工具与版本条件见 Next.js 内存使用指南。实验性调试选项与 build worker 可能存在兼容限制,应单独在构建环境验证,不要把不同版本的优化参数一次全部合并到配置里。
对于确认与 Node 堆有关的问题,官方指南也提供 heap profile 方法:
node --heap-prof node_modules/next/dist/bin/next build
它用于诊断,不是减少内存的开关;直接调用 Next CLI 也可能绕过原脚本的预处理,所以要保持需要的前置步骤和环境。成功结束后再查看生成的 profile;被强制杀死时不能保证得到完整文件。分析或堆快照可能额外消耗内存和磁盘,不适合在已经撑满的生产主机上首先尝试。
不要把 ignoreBuildErrors、跳过必要检查或调试模式生成的产物当作上线修复。若类型检查需要独立执行,应确保 CI 中的检查通过后才发布。
先用 swapon --show 确认现有 Swap,再评估是否符合 VPS、文件系统和容器的限制。Swap 可能缓冲短暂的内存压力,也可能让构建长时间换页、拖慢同机业务;容器没有使用 Swap 的额度时,宿主机添加 Swap 未必解决它的限额问题。
不要覆盖已有 Swap 文件,也不要在磁盘紧张时直接照抄几 GB 的创建命令。宿主机 OOM 和 Swap 的基础处理可看 VPS 内存不足与 OOM 排查。如果每次构建都靠大量换页才能结束,更合理的长期方案通常是扩充构建资源或移走构建任务。
应用运行时占用较小、生产构建峰值很高时,可以让 CI 或独立构建机完成编译,VPS 负责运行已验证的版本。迁移构建时仍要核对:
- 代码提交、锁文件、Node/包管理器版本及目标平台一致。
- 构建环境变量符合部署需求;
NEXT_PUBLIC_等构建期值不能依靠运行时临时替换。 - 原生依赖与目标操作系统、架构兼容,不直接把 macOS 产物当作 Linux 可执行产物。
- 部署完整产物和静态资源,保留上一个可用版本并验证回滚。
Next.js standalone 的复制与 Docker 部署步骤见 Next.js VPS 部署教程。不要只复制 .next/standalone 就忘记 public 与 .next/static;也不要让失败的构建覆盖当前服务的可用产物。需要独立 Runner 时可参考 GitHub Actions 自托管 Runner 部署。
使用同一个代码版本、锁文件和真实构建环境重新执行完整生产构建。检查退出码为 0、原失败阶段顺利完成、没有新增相关 OOM 事件,而且内存峰值给同机业务留下余量。
随后验证部署后的页面、API、静态资源、关键业务和回滚。区分冷构建与利用缓存的构建,避免只验证一次缓存命中就认为问题已经解决。保留原始错误、实际限额、修改项和前后数据;一次只改变关键变量,才知道是哪项措施奏效。
处理 Next.js 构建内存问题最有效的顺序是:先找真实失败边界,再选择堆调整、项目优化或更大的构建环境。这样能恢复发布流程,同时避免构建任务挤占生产服务的资源。
