Nuxt 项目 Docker 构建:从 pnpm+node 迁移到全 bun

项目:Nuxt 4.5.2 + Nitro 2.13.4 + bun 1.3.14

背景与目标

项目原本使用 node:26-alpine + pnpm 进行构建和运行,镜像体积约 196MB。本地开发环境已经切换到 bun,但 Dockerfile 仍停留在 node+pnpm。

目标是统一使用 bun,缩小镜像体积、加快构建速度。

尝试一:bun build --compile 生成独立二进制(失败)

思路:像 Go 一样编译成单个 ELF 二进制,省去 node_modules,实现最小化部署。

text

复制代码
bun build ./server/index.ts --compile --outfile server

初步结果:

  • 编译成功,生成约 100MB 的独立可执行文件

  • SSR 首页返回 200

  • API 路由正常工作

问题:

  • 静态资源 /_nuxt/*.js 返回 500

  • 错误:ENOENT: /$bunfs/public/_nuxt/xxx.js

根因:

Nitro 的 bun preset 通过 serveStatic('/$bunfs/public/') 服务静态资源。bun build --compile 会将代码和依赖打包进二进制,但不会嵌入 Nitro 运行时动态读取的 public 目录文件。这是 Nitro 层面的兼容缺陷,与 bun 的编译机制无关。

尝试了 7 种绕过方案(symlink、wrapper、monkey-patch fs、asset-naming 等),全部失败。

结论: 在当前 Nitro 版本下,bun build --compile 不适用于 Nuxt SSR 生产部署。

尝试二:bun run 模式(成功)

改用 bun 作为运行时执行 Nitro 产物,不编译成独立二进制:

bash

复制代码
bun run .output/server/index.mjs

SSR、静态资源、API 路由全部正常工作,启动时间约 30ms。

最终方案:全 bun Dockerfile

dockerfile

复制代码
FROM oven/bun:1-alpine AS build
WORKDIR /app

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages ./packages
COPY apps/web ./apps/web

RUN bun install

WORKDIR /app/apps/web
RUN NITRO_PRESET=bun bun run build

# 关键:在 .output/server 内扁平化安装依赖
WORKDIR /app/apps/web/.output/server
RUN bun install --production --no-cache && rm -rf /root/.bun

FROM alpine:3.21
COPY --from=oven/bun:1-alpine /usr/local/bin/bun /usr/local/bin/bun
RUN apk add --no-cache libgcc libstdc++ && chmod +x /usr/local/bin/bun
WORKDIR /app
COPY --from=build /app/apps/web/.output .output
EXPOSE 3200
ENV NITRO_PORT=3200
CMD ["bun", "run", ".output/server/index.mjs"]

关键设计点

1. build 阶段全 bun

bun install 替代 pnpm,速度从约 30s 降至约 9s。bun run build 替代 pnpm build

2. NITRO_PRESET=bun

让 Nitro 生成 bun 原生 SSR 产物。

3. .output/server 二次安装依赖

pnpm 的 workspace 结构使用符号链接,bun 无法完全解析。在 .output/server 目录内执行 bun install --production 可以将依赖扁平化,解决 Cannot find module '@vue/shared' 类问题。

4. 清理 bun 缓存

bun install 默认在 /root/.bun 缓存下载的包(约 155MB)。用 --no-cache 配合 rm -rf /root/.bun 清理,避免镜像膨胀。

5. 运行阶段最小化

oven/bun:1-alpine 只复制 bun 二进制(约 74.5MB),运行在 alpine:3.21(约 8.5MB)之上,不依赖完整镜像。

镜像分层

大小
alpine 基础 8.5MB
bun 二进制 74.5MB
libgcc/libstdc++ 3.0MB
.output(含 node_modules) 48.2MB
合计 180MB

效果对比

指标 旧方案(node+pnpm) 新方案(全 bun)
镜像体积 196MB 180MB
构建工具链 node + pnpm bun
运行时体积 node(141MB) bun(74.5MB)
依赖安装速度 ~30s ~9s
SSR
静态资源
API 路由

验证通过:docker run 后访问首页和 favicon 均返回 200,日志显示 Listening on http://localhost:3200/

踩坑记录

坑 1:bun install 缓存导致镜像膨胀

bun install 默认在 /root/.bun 缓存下载的包,约 155MB。多阶段构建中即使不保留该目录,若未在构建阶段清理,缓存层仍会被带入最终镜像。

解决: --no-cache + rm -rf /root/.bun

坑 2:pnpm 符号链接与 bun 不兼容

pnpm 的 node_modules 使用符号链接结构,bun 运行时无法完全解析,导致 Cannot find module 错误。

解决:.output/server 内重新执行 bun install --production,将依赖扁平化(实化符号链接)。

坑 3:Nitro bun preset 静态资源路径

Nitro 的 bun preset 使用 /$bunfs/public/ 虚拟路径服务静态资源。bun build --compile 不会将这些文件嵌入二进制,导致静态资源 500。

解决: 放弃 bun build --compile,改用 bun run 模式运行 .output 产物。

经验总结

  1. bun run 是目前 Nuxt SSR 项目中最可靠的 bun 部署方式bun build --compile 适合纯 CLI 工具或无静态资源的 API 服务,不适合需要 serveStatic 的 SSR 应用。

  2. pnpm workspace 迁移到 bun 时需注意符号链接处理。在产物目录内二次安装依赖是有效的缓解手段。

  3. bun 的缓存机制在 Docker 构建中需要主动清理,否则会显著增加镜像体积。

  4. 总镜像体积从 196MB 降到 180MB,虽然减少幅度不大,但构建速度提升明显(30s → 9s),运行时也更轻量。

相关推荐
鹿角片ljp3 小时前
LeetCode 64:最小路径和复盘|二维 DP 与 ACM 模式完整写法
java·数据结构·算法
+VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue蛋糕店管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·课程设计
泡海椒4 小时前
PDF 表格样式优化:jquick-pdf 边框、圆角、背景色
java·开发语言·pdf
Nicander5 小时前
我给 Excalidraw 做了一个本地工作区:DrawSpace
前端·开源
景熙55235 小时前
15.Java 8 Stream 流入门到实战
java·开发语言·数据结构
wno7046 小时前
Spring Security权限控制
java·python·spring
杨运交6 小时前
[071][验证码模块]基于Spring拦截器的验证码认证设计思想
java·后端·spring
Geek-Chow6 小时前
CountDownLatch in Java
java
linux_cfan6 小时前
17 · 引擎适配器全景:HLS/DASH/Vimeo/Mux/Cast
前端·javascript·音视频