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),运行时也更轻量。

相关推荐
qq_4260039612 分钟前
UI自动化元素定位不到解决方案
前端·pycharm·自动化
学长毕业设计15 分钟前
基于SpringBoot的社区鲜奶订购系统的设计与实现(源码+文档+讲解视频)
java·spring boot·后端
EthanChou202019 分钟前
AES67协议笔记
前端·笔记·es6
马优晨20 分钟前
pom.xml 中 jquery webjars 依赖解释
xml·前端·jquery·jquery webjars·webjars依赖
呆呆敲代码的小Y20 分钟前
【游戏开发】C# 中的迭代器
java·开发语言·c#·迭代器
TinyMemory21 分钟前
Java 面向对象核心入门(六):this 关键字完全解析
java·笔记·面向对象·java新手·this关键字
萧瑟余晖23 分钟前
Java深入解析篇九之JavaStream API详解
java
一只QAQ28 分钟前
c++项目
java·c++·算法
小蒜学长35 分钟前
基于SpringBoot + Vue的智能健身房管理系统的设计与实现(代码+数据库+LW)
java·数据库·vue.js·spring boot·后端