Reading
Docker 镜像从 1.2GB 瘦到 183MB:多阶段构建实战
镜像大的原因几乎都是把「构建现场」原封不动打进了「运行现场」。用多阶段构建、.dockerignore 和更小的基础镜像,把一个 Next.js 镜像从 1.2GB 压到 183MB。

适用场景:Node.js 或其它编译型项目的 Docker 镜像动辄 1GB 起步,构建慢、上传慢、服务器磁盘也吃紧。这篇记录我把一个 Next.js 项目镜像从 1.2GB 压到 180MB 的完整过程,思路对其它语言同样适用。
背景和思路
给博客做部署脚本的时候,我发现每次发布都要往服务器传一个 1.2GB 的镜像包,家里的上行带宽要传七八分钟。
镜像大的原因几乎都是同一类:把「构建现场」原封不动打进了「运行现场」。源码、devDependencies、编译缓存、包管理器缓存,这些只有构建时需要的东西全被带进了最终镜像。
瘦身的思路按收益排序:
- 多阶段构建,只把产物拷进运行镜像;
- 换更小的基础镜像;
- 用
.dockerignore挡住不该进构建上下文的东西; - 合理安排层顺序,让缓存尽量命中。
第一步:先搞清楚空间去哪了
不要盲目改 Dockerfile,先看数据:
docker history --no-trunc qingsong-notes:latest
docker history 按层列出每条指令产生的体积。我这里一眼看到两个大头:npm ci 装出来的 800MB node_modules,以及 COPY . . 带进来的 300MB 杂物。
想更细致地逐层浏览文件,可以用开源工具 dive(https://github.com/wagoodman/dive):
dive qingsong-notes:latest
第二步:多阶段构建
核心改动就是把 Dockerfile 拆成 builder 和 runner 两个阶段:
# ---- 构建阶段 ----
FROM node:22-bookworm AS builder
WORKDIR /app
# 先只拷贝依赖清单,让这一层的缓存尽量长寿
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- 运行阶段 ----
FROM node:22-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["npm", "start"]
关键点:
- 运行阶段从零开始,只
COPY --from=builder拿构建产物,源码和 devDependencies 都留在了 builder 阶段,不会进最终镜像。 npm ci --omit=dev只装生产依赖;npm cache clean --force清掉同一层里的下载缓存——清理必须和产生缓存的命令在同一个 RUN 里,分开写的话上一层已经固化,删了也不会变小。- Next.js 用户还可以在
next.config.ts开output: 'standalone',运行阶段连node_modules都不用装,直接拷.next/standalone,能再砍一大截。
第三步:.dockerignore
COPY . . 会把构建上下文里的一切都送进去,必须有一份 .dockerignore:
node_modules
.next
.git
*.db
media
test-results
playwright-report
*.log
.env*
这一步有双重收益:镜像更小,而且 .env 这类敏感文件不会被意外打进镜像层里。镜像层是可以被 docker history 和导出翻出来的,删除文件的后一层并不能抹掉前一层的内容。
第四步:基础镜像的取舍
| 基础镜像 | 大小量级 | 说明 |
|---|---|---|
| node:22-bookworm | ~1GB | 全量 Debian,带编译工具链 |
| node:22-bookworm-slim | ~200MB | 去掉了大部分系统包,够跑纯 JS |
| node:22-alpine | ~130MB | 最小,但是 musl libc |
我最后选了 slim 而不是 alpine:alpine 用 musl,遇到 sharp 这类带原生二进制的依赖偶尔会踩兼容坑,为了再省 70MB 不值得。slim 是收益和风险平衡得最好的默认选项。
验证
docker build -t qingsong-notes:slim .
docker images | grep qingsong-notes
docker run --rm -p 3000:3000 qingsong-notes:slim
对比结果:
qingsong-notes latest 1.21GB
qingsong-notes slim 183MB
再用 curl -I http://127.0.0.1:3000 确认服务正常响应,别只看体积不看能不能跑。
收尾检查
- 清理命令和产生垃圾的命令写在同一个 RUN 里。
-
.dockerignore里挡住了.git、.env、数据库文件。 - 依赖清单的 COPY 在源码 COPY 之前,改代码不会击穿
npm ci的缓存层。 - 最终镜像实际跑过一次,接口能正常响应。


