上周五下午,运维找我:"你们的镜像怎么这么大?2.1GB,拉取要10分钟,能不能优化一下?"
我看了下,确实大得离谱。然后花了一天时间,把镜像从 2GB 优化到 200MB。
问题在哪
先看原来的 Dockerfile:
FROM node:18 WORKDIR /app COPY . . RUN npm install RUN npm run build CMD ["node", "dist/server.js"]看起来没问题,对吧?问题大了。
我用 docker history 查看各层大小:
IMAGE SIZE node:18 1.1GB # 基础镜像 COPY . . 500MB # 源码 + node_modules + .git npm install 300MB # 又装了一遍依赖 npm run build 200MB # 构建产物 + 缓存总共 2.1GB。
优化过程
第一步:换基础镜像
node:18 是完整的 Debian 系统,1.1GB。我们只需要运行 Node.js,不需要这么多东西。
换成 Alpine:
FROM node:18-alpineAlpine 只有 170MB,直接省了 1GB。
第二步:.dockerignore
原来的 COPY . . 把所有文件都复制进去了,包括:
node_modules/(300MB).git/(150MB)*.log.env
创建 .dockerignore:
node_modules .git *.log .env .DS_Store coverage .vscodeCOPY 的体积从 500MB 降到 50MB。
第三步:多阶段构建
我们需要 npm install 来构建,但运行时不需要 devDependencies。
改成多阶段:
# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build # 运行阶段 FROM node:18-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY package*.json ./ CMD ["node", "dist/server.js"]最终镜像里只有:
- Alpine 基础镜像 (170MB)
- 生产依赖 (20MB)
- 构建产物 (10MB)
总共 200MB。
第四步:利用缓存
注意到 COPY package*.json 在 COPY . . 之前。
这样,如果 package.json 没变,npm ci 这层就会用缓存,不用重新下载依赖。
改代码不会触发 npm install,构建速度快很多。
第五步:精简依赖
我顺便检查了 package.json,发现很多没用的包:
lodash:只用了2个方法,可以自己写moment:可以用原生Date或date-fnsaxios:Node.js 18 有原生fetch
删掉这些包,又省了 10MB。
最终结果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 镜像大小 | 2.1GB | 190MB |
| 拉取时间 | 10分钟 | 30秒 |
| 构建时间 | 5分钟 | 1分钟 |
| 存储成本 | $50/月 | $5/月 |
缩小了 91%。
其他技巧
1. 合并 RUN 命令
每个 RUN 都会创建一层。如果有多个 RUN,可以合并:
# 不好 RUN apt-get update RUN apt-get install -y curl RUN apt-get clean # 好 RUN apt-get update && \ apt-get install -y curl && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*2. 使用 distroless
如果是 Go、Java 这种编译型语言,可以用 gcr.io/distroless/static。
这个镜像只有 2MB,连 shell 都没有,非常安全。
3. 压缩构建产物
对于前端项目,可以在构建时开启 gzip/brotli 压缩。
RUN npm run build && \ find dist -type f \( -name '*.js' -o -name '*.css' \) \ -exec gzip -k {} \;总结
优化 Docker 镜像的核心原则:
- 选对基础镜像:能用 Alpine 就别用完整版
- 减少复制内容:用
.dockerignore - 多阶段构建:构建和运行分离
- 利用缓存:把不常变的层放前面
- 精简依赖:定期清理无用的包
这些都是小改动,但效果立竿见影。
你的镜像有多大?有没有优化的空间?
参考资料:Docker 最佳实践
评论区