Docker 镜像瘦身实战:从 1.2GB 到 180MB 的六个手段
Docker 镜像瘦身实战:从 1.2GB 到 180MB 的六个手段
摘要:镜像太大不仅拖慢构建和拉取速度,还会扩大攻击面。本文以一个真实的 Node.js 服务为例,演示六种行之有效的瘦身手段,最终把镜像从 1.2GB 压到 180MB,每一步都附上可复制的 Dockerfile 片段。
一、为什么镜像会膨胀到 1.2GB
先看一个典型的"反面教材"Dockerfile,很多项目上线多年一直是这个写法:
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
这个写法的问题非常典型:
- 基础镜像选错:
node:20默认基于完整版 Debian,自带编译工具链、文档、大量系统库,光基础层就超过 1GB; - COPY . . 把不该带的全带上:
.git目录、本地测试截图、node_modules(如果有本地开发缓存)、日志文件统统进了镜像; - npm install 而非 npm ci:前者不严格按 lockfile 安装,还可能在镜像里留下 npm 缓存;
- 没有分层优化意识:源码一改,依赖层也要重新构建。
用 docker images 一看,1.2GB 就这么来的。下面逐条治理。
二、手段一:换掉臃肿的基础镜像
同一版本号的 Node 官方镜像有好几个 tag,体积差距巨大:
| 镜像 tag | 压缩后大小 | 说明 |
|---|---|---|
| node:20 | 约 350MB | 完整版 Debian |
| node:20-slim | 约 75MB | 精简 Debian |
| node:20-alpine | 约 50MB | Alpine,最小 |
仅把基础镜像换成 node:20-alpine,就有立竿见影的效果:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
注意 Alpine 用的是 musl 而非 glibc,个别依赖原生模块(如 sharp、bcrypt 老版本)需要额外处理,遇到编译报错时可以退回 -slim,同样能省下大部分体积。
三、手段二:.dockerignore 拦住垃圾文件
和 .gitignore 同理,构建上下文里不该进镜像的都写进 .dockerignore:
.git
node_modules
dist
*.log
test
docs
README.md
Dockerfile
.dockerignore
别小看这一步:.git 目录在大项目里轻松超过几百 MB,而且它们会被打进 COPY 层,永远躺在镜像里。构建上下文变小后,docker build 的"Sending build context to daemon"阶段也会明显变快。
四、手段三:多阶段构建,把编译工具留在 builder
前端项目、带原生编译的 Node 项目,构建期需要的工具(gcc、make、webpack)运行期完全用不到。多阶段构建让"脏活"留在第一阶段,最终镜像只拷贝产物:
# ---- 构建阶段 ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- 运行阶段 ----
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
三个细节值得强调:
package*.json先拷贝、再npm ci,最后才拷源码——这样源码变更不会击穿依赖层的缓存,日常构建从几分钟降到几秒;npm ci --omit=dev不安装 devDependencies,webpack、测试框架这些统统不进运行镜像;npm cache clean --force清掉 npm 缓存,这一步必须和npm ci写在同一个RUN里(分开写的话缓存已经落层,删了也不减重)。
五、手段四:合并 RUN、清理同层垃圾
Docker 的每一层都是只读增量,RUN apt-get update && apt-get install 之后跟一个独立的 RUN rm -rf /var/lib/apt/lists/* 是无效的——文件在上层"消失"了,但下层还在,镜像不减一分。正确写法是把安装和清理放进同一条命令:
RUN apk add --no-cache --virtual .build-deps \
gcc make python3 \
&& npm rebuild \
&& apk del .build-deps
--virtual .build-deps 把编译依赖打成一个虚拟包,用完即删,这是 Alpine 下的标准姿势。同理,curl、wget 这类只在构建期用的工具,尽量不留进最终镜像。
六、手段五:压箱底的两个细节
1. 固定 tag + 定期重建。用 node:20-alpine 而不是 node:alpine,避免某天拉到一个大版本变化的镜像导致体积和行为突变。
2. 用 dive 审计镜像层:
dive myapp:latest
它会逐层展示每个 RUN 增加了多少体积、哪些文件被浪费地塞进了镜像,瘦身时对着它的分析结果做减法,效率远高于盲猜。
七、最终效果对比
| 阶段 | 镜像体积 |
|---|---|
| 原始 Dockerfile | 1.2GB |
| 换 alpine 基础镜像 | 约 400MB |
| + .dockerignore | 约 320MB |
| + 多阶段构建 | 约 190MB |
| + 依赖清理与合并层 | 约 180MB |
整体瘦身 85%,CI 拉取和部署时间从分钟级降到十几秒,滚动发布时对带宽的冲击也小得多。
镜像瘦身没有黑魔法,本质就三句话:基础镜像选小、构建产物和运行环境分离、每一层只留必需品。把这三条变成团队的 Dockerfile 规范,新服务天生就是轻的。
- 点赞
- 收藏
- 关注作者
评论(0)