Docker 多阶段构建实战:把 Node 镜像从 1.2GB 压到 180MB

举报
yd_222030558 发表于 2026/10/22 02:09:35 2026/10/22
【摘要】 用一个真实的 Node 项目演示多阶段构建、依赖分层缓存和 .dockerignore 的配合,把镜像体积压掉 85%。

为什么镜像会越来越大


一个典型的 Node 项目,直接 FROM node:20 加 npm install,构建出来的镜像往往超过 1GB。原因不复杂:基础镜像本身约 1GB,node_modules 里塞了 devDependencies,再加上 .git、测试用例、本地构建产物,一股脑全进了最终镜像。


镜像大带来的直接后果是:推送到镜像仓库慢、节点拉取慢、冷启动扩容慢,安全扫描还要多扫几千个无关文件。


多阶段构建:只要产物,不要工具链


核心思路是把「构建环境」和「运行环境」分开,最终镜像只从构建阶段拷贝需要的东西。


---------- 构建阶段 ----------

FROM node:20-alpine AS builder

WORKDIR /app


先只拷依赖清单,利用分层缓存

COPY package*.json ./

RUN npm ci


再拷源码并构建

COPY . .

RUN npm run build


只保留生产依赖

RUN npm ci --omit=dev && npm cache clean --force


---------- 运行阶段 ----------

FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production


用非 root 用户运行

RUN addgroup -S app && adduser -S app -G app


COPY --from=builder /app/node_modules ./node_modules

COPY --from=builder /app/dist ./dist

COPY --from=builder /app/package.json ./


USER app

EXPOSE 3000

CMD ["node", "dist/index.js"]


这个写法的收益来自三点:第一,构建工具(TypeScript、webpack、测试框架)全部留在 builder 阶段,不进最终镜像;第二,node:20-alpine 本身只有几十 MB;第三,只拷贝 dist 和 node_modules,源码和 .git 不会泄露到线上。


缓存顺序决定构建速度


COPY package*.json ./ 单独一行、放在 COPY . . 之前,是整套写法里最容易忽略的细节。Docker 的分层缓存以指令为单位,只要某一层的输入没变就直接复用。代码改动频繁、依赖改动很少,把依赖安装和源码拷贝分开,日常改代码时依赖层可以命中缓存,npm ci 不会再跑一遍。


反过来,如果写成 COPY . . 之后再 npm install,任何一次改代码都会让依赖层失效,构建时间从 10 秒变成 3 分钟。


.dockerignore 比想象中重要


多阶段构建解决「不该进最终镜像」的问题,.dockerignore 解决「不该进构建上下文」的问题。少了它,node_modules、.git、日志文件会被打包发给 Docker daemon,构建上下文动辄几百 MB。


node_modules

.git

dist

coverage

*.log

.env*

Dockerfile

docker-compose*.yml


注意 .env 一定要排除。很多人把密钥写进去,镜像推到仓库就等于公开了。


几个容易踩的坑


时区和字符集:alpine 默认没有时区数据,容器里 new Date() 出来的是 UTC。需要本地时间就装 tzdata 并设置 TZ=Asia/Shanghai。


原生模块编译失败:alpine 用 musl libc,某些依赖(比如老版本的 sharp、bcrypt)没有预编译产物,会在 npm ci 阶段编译失败。解决办法是要么换 node:20-slim,要么在 builder 阶段装 python3 make g++。


HEALTHCHECK 别省:镜像能起来不代表服务可用。加一条 HEALTHCHECK CMD wget -qO- http://127.0.0.1:3000/health || exit 1,编排系统才知道什么时候该把流量切过去。


效果对比


同一个项目,改造前后:

· 单阶段:1.18GB,冷启动拉取约 90 秒

· 多阶段 + alpine:182MB,冷启动约 15 秒


体积下降 85%,构建缓存命中后 CI 时间也少了三分之二。改造本身只涉及一个 Dockerfile 和一个 .dockerignore,投入产出比相当高。


最后提醒一句:镜像不是越小越好。为了省几十 MB 把运行时依赖删到不可调试,出问题时连 sh 都进不去,反而得不偿失。体积和安全、可维护性之间要取平衡。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。