Docker 多阶段构建实战:把 Node.js 镜像从 1.2GB 压到 180MB
一、问题:镜像为什么越做越大
很多团队的前端项目一开始都是这样构建镜像的:用一个 node:20 基础镜像,把源码拷进去,执行 npm install 和 npm run build,然后把整份目录当作运行镜像推送到镜像仓库。这样做能跑通,但副作用很明显:编译工具链、devDependencies、源码、缓存全部留在镜像里,一个普通的中后台项目轻松做到 1GB 以上,推送到镜像仓库要几分钟,节点拉取镜像又慢,安全扫描还会报出一堆编译期依赖的漏洞。
更麻烦的是,运行阶段根本不需要 npm、node-gyp、TypeScript 编译器这些东西。运行只需要一份构建产物和一台静态服务器。
二、多阶段构建的原理
Docker 从 17.05 开始支持多阶段构建:一个 Dockerfile 里可以写多个 FROM,每个 FROM 开启一个新阶段,阶段之间用 as 命名。最终镜像只保留最后一个阶段的内容,前面的阶段可以当作“临时工作区”,通过 COPY --from=阶段名 把需要的文件复制过来。构建缓存依然按阶段生效,所以构建速度不会明显变慢,但最终镜像的体积会小一个数量级。
三、改造前:单阶段 Dockerfile
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
这份文件的问题在于:node_modules、源码、构建缓存、npm 缓存全部进入镜像;基础镜像本身还带着完整的编译工具链。最终镜像 1.2GB 是常态。
四、改造后:两阶段 Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
第一个阶段只负责安装依赖和打包,第二个阶段只拿 dist 产物,运行环境换成体积小得多的 nginx-alpine。node、npm、源码、缓存都不会出现在最终镜像里。
五、实测对比
同一个项目,同一台构建机,改造前后的数据大致如下:镜像体积从 1.2GB 降到 180MB;docker push 从约 3 分钟降到 20 秒;节点冷启动拉取镜像从 90 秒降到 12 秒;安全扫描的高危项从 40 多个降到个位数。构建耗时基本持平,因为 npm ci 和打包仍然要做,只是换了一个阶段。
六、几个容易踩的坑
第一,npm ci 需要 package-lock.json,缺锁文件会直接失败,这是好事,能保证依赖可复现。第二,一定要写 .dockerignore,把 node_modules、dist、.git 排除掉,否则 COPY . . 会把宿主机的依赖带进构建上下文,既慢又容易出现平台不一致的问题。第三,跨平台构建时要注意原生模块,苹果芯片机器上安装的依赖不能直接拿到 linux/amd64 镜像里用,最好在构建阶段指定 --platform。第四,COPY --from 的路径要写构建产物真实的位置,Vite 默认是 dist,Create React App 是 build,写错了不会报错,只会在运行时报 404。第五,静态资源如果走 CDN,记得调整 nginx 的缓存头和 gzip 配置。
七、总结
多阶段构建不是新特性,但它是投入产出比最高的镜像瘦身手段之一:改十几行 Dockerfile,换来一个更小、更快、更安全的运行镜像。建议把它作为团队 Dockerfile 的默认写法,同时在 CI 里加一条镜像体积阈值检查,防止体积悄悄回涨。
- 点赞
- 收藏
- 关注作者
评论(0)