|
|
🔍 镜像优化实战:从1.2GB到80MB的优化之路
今天,我们通过一个真实案例,完整演示镜像优化的全过程。
你将看到如何把一个 1.2GB 的镜像,一步步优化到 80MB,减少 93%!
━━━━━━━━━━━━━━━━━━━━
本文你将学到
✅ 完整的镜像优化流程
✅ 10+ 个实用优化技巧
✅ 每个优化步骤的效果对比
✅ 建立镜像优化检查清单
阅读时间:约 15 分钟
实操时间:约 30 分钟
难度等级:⭐⭐⭐⭐☆
━━━━━━━━━━━━━━━━━━━━
案例背景
项目情况
一个 Node.js + TypeScript 的 API 服务:
Express.js 框架
TypeScript 编译
若干 npm 依赖
开发团队反馈:镜像太大,部署太慢
原始 Dockerfile
- FROM node:20
- WORKDIR /app
- COPY . .
- RUN npm install
- RUN npm run build
- EXPOSE 3000
- CMD ["npm", "start"]
复制代码
原始镜像分析
- docker build -t myapi:v0 .
- docker images myapi:v0
复制代码
结果:- REPOSITORY TAG SIZE
- myapi v0 1.21GB
复制代码
问题分析:
使用完整 node 镜像(约 1GB)
包含开发依赖
包含 TypeScript 源码
包含 node_modules 中的测试文件
━━━━━━━━━━━━━━━━━━━━
优化步骤
第1步:分析镜像层
首先,了解镜像大小的构成:
- # 查看各层大小
- docker history myapi:v0
- # 使用 dive 工具深入分析
- docker run --rm -it \
- -v /var/run/docker.sock:/var/run/docker.sock \
- wagoodman/dive myapi:v0
复制代码
发现的问题:
| 层 | 大小 | 问题 |
|-----|------|------|
| node:20 基础镜像 | 1.0GB | 太大 |
| npm install | 150MB | 包含 devDependencies |
| COPY . . | 50MB | 包含不必要文件 |
━━━━━━━━━━━━━━━━━━━━
第2步:使用 Alpine 基础镜像
- # 从 node:20 改为 node:20-alpine
- FROM node:20-alpine
- WORKDIR /app
- COPY . .
- RUN npm install
- RUN npm run build
- EXPOSE 3000
- CMD ["npm", "start"]
复制代码- docker build -t myapi:v1 .
- docker images myapi:v1
复制代码
效果:
| 版本 | 大小 | 减少 |
|------|------|------|
| v0 | 1.21GB | - |
| v1 | 350MB | 71% |
✅ 仅换基础镜像就减少了 71%!
━━━━━━━━━━━━━━━━━━━━
第3步:添加 .dockerignore
创建文件:
- # .dockerignore
- node_modules
- npm-debug.log
- .git
- .gitignore
- README.md
- .env
- .env.*
- coverage
- .nyc_output
- dist
- *.md
- .vscode
- .idea
- tests
- __tests__
- *.test.ts
- *.spec.ts
- Dockerfile
- docker-compose*.yml
- .dockerignore
复制代码
效果:
| 版本 | 大小 | 减少 |
|------|------|------|
| v1 | 350MB | - |
| v2 | 320MB | 9% |
━━━━━━━━━━━━━━━━━━━━
第4步:分离依赖安装和代码复制
优化层缓存:
- FROM node:20-alpine
- WORKDIR /app
- # 先复制依赖文件
- COPY package*.json ./
- # 安装依赖(这层可缓存)
- RUN npm install
- # 再复制源码
- COPY . .
- RUN npm run build
- EXPOSE 3000
- CMD ["npm", "start"]
复制代码
效果:构建速度提升,代码变更时无需重新安装依赖
━━━━━━━━━━━━━━━━━━━━
第5步:只安装生产依赖
- FROM node:20-alpine
- WORKDIR /app
- COPY package*.json ./
- # 只安装生产依赖
- RUN npm ci --only=production
- COPY . .
- RUN npm run build
- EXPOSE 3000
- CMD ["npm", "start"]
复制代码
❌ 问题:TypeScript 编译需要 devDependencies!
解决:使用多阶段构建
━━━━━━━━━━━━━━━━━━━━
第6步:多阶段构建
- # ======== 阶段1:构建 ========
- FROM node:20-alpine AS builder
- WORKDIR /app
- # 复制依赖文件
- COPY package*.json ./
- # 安装所有依赖(包括 devDependencies)
- RUN npm ci
- # 复制源码并构建
- COPY . .
- RUN npm run build
- # 清理开发依赖
- RUN npm prune --production
- # ======== 阶段2:运行 ========
- FROM node:20-alpine
- WORKDIR /app
- # 只复制必要文件
- COPY --from=builder /app/dist ./dist
- COPY --from=builder /app/node_modules ./node_modules
- COPY --from=builder /app/package*.json ./
- EXPOSE 3000
- CMD ["node", "dist/index.js"]
复制代码- docker build -t myapi:v3 .
- docker images myapi:v3
复制代码
效果:
| 版本 | 大小 | 减少 |
|------|------|------|
| v2 | 320MB | - |
| v3 | 180MB | 44% |
━━━━━━━━━━━━━━━━━━━━
第7步:优化 npm 安装
- FROM node:20-alpine AS builder
- WORKDIR /app
- COPY package*.json ./
- # 使用 npm ci 并清理缓存
- RUN npm ci && npm cache clean --force
- COPY . .
- RUN npm run build
- RUN npm prune --production
- FROM node:20-alpine
- WORKDIR /app
- # 清理 apk 缓存
- RUN apk add --no-cache tini
- COPY --from=builder /app/dist ./dist
- COPY --from=builder /app/node_modules ./node_modules
- COPY --from=builder /app/package*.json ./
- # 使用 tini 作为 init 进程
- ENTRYPOINT ["/sbin/tini", "--"]
- CMD ["node", "dist/index.js"]
复制代码
效果:
| 版本 | 大小 | 减少 |
|------|------|------|
| v3 | 180MB | - |
| v4 | 165MB | 8% |
━━━━━━━━━━━━━━━━━━━━
第8步:压缩 node_modules
使用清理无用文件:
- FROM node:20-alpine AS builder
- WORKDIR /app
- # 安装 node-prune
- RUN wget -qO- https://gobinaries.com/tj/node-prune | sh
- COPY package*.json ./
- RUN npm ci
- COPY . .
- RUN npm run build
- RUN npm prune --production
- # 清理 node_modules 中的无用文件
- RUN node-prune
- FROM node:20-alpine
- WORKDIR /app
- RUN apk add --no-cache tini
- COPY --from=builder /app/dist ./dist
- COPY --from=builder /app/node_modules ./node_modules
- COPY --from=builder /app/package*.json ./
- ENTRYPOINT ["/sbin/tini", "--"]
- CMD ["node", "dist/index.js"]
复制代码
node-prune 清理的内容:
文件
类型定义源文件
目录
目录
目录
效果:
| 版本 | 大小 | 减少 |
|------|------|------|
| v4 | 165MB | - |
| v5 | 120MB | 27% |
━━━━━━━━━━━━━━━━━━━━
第9步:合并 RUN 指令
减少镜像层数:
- FROM node:20-alpine AS builder
- WORKDIR /app
- RUN wget -qO- https://gobinaries.com/tj/node-prune | sh
- COPY package*.json ./
- RUN npm ci
- COPY . .
- # 合并多个 RUN 命令
- RUN npm run build && \
- npm prune --production && \
- node-prune
- FROM node:20-alpine
- WORKDIR /app
- # 合并 apk 安装
- RUN apk add --no-cache tini && \
- addgroup -S appgroup && \
- adduser -S appuser -G appgroup
- COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
- COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
- COPY --from=builder --chown=appuser:appgroup /app/package*.json ./
- USER appuser
- EXPOSE 3000
- ENTRYPOINT ["/sbin/tini", "--"]
- CMD ["node", "dist/index.js"]
复制代码
效果:
| 版本 | 大小 | 减少 |
|------|------|------|
| v5 | 120MB | - |
| v6 | 115MB | 4% |
━━━━━━━━━━━━━━━━━━━━
第10步:选择更小的基础镜像(可选)
如果不需要完整的 Node 环境:
- FROM node:20-alpine AS builder
- # ... 构建步骤同上 ...
- # 使用 distroless
- FROM gcr.io/distroless/nodejs20-debian11
- WORKDIR /app
- COPY --from=builder /app/dist ./dist
- COPY --from=builder /app/node_modules ./node_modules
- COPY --from=builder /app/package*.json ./
- EXPOSE 3000
- CMD ["dist/index.js"]
复制代码
效果:
| 版本 | 大小 | 减少 |
|------|------|------|
| v6 (alpine) | 115MB | - |
| v7 (distroless) | 80MB | 30% |
━━━━━━━━━━━━━━━━━━━━
优化成果总结
完整优化历程
| 步骤 | 优化措施 | 大小 | 减少 |
|------|----------|------|------|
| v0 | 原始版本 | 1.21GB | - |
| v1 | Alpine 基础镜像 | 350MB | 71% |
| v2 | .dockerignore | 320MB | 9% |
| v3 | 多阶段构建 | 180MB | 44% |
| v4 | 清理 npm 缓存 | 165MB | 8% |
| v5 | node-prune | 120MB | 27% |
| v6 | 合并指令+安全 | 115MB | 4% |
| v7 | Distroless | 80MB | 30% |
最终结果:从 1.21GB → 80MB,减少 93%!
时间收益
| 指标 | 优化前 | 优化后 | 提升 |
|------|--------|--------|------|
| 镜像拉取 | 2-5分钟 | 10-20秒 | 10x |
| 构建时间 | 3分钟 | 1分钟 | 3x |
| 存储成本 | 1.21GB | 80MB | 15x |
━━━━━━━━━━━━━━━━━━━━
镜像优化检查清单
基础镜像
[ ] 使用 Alpine 或 slim 变体
[ ] 使用固定版本标签
[ ] 考虑 Distroless(生产环境)
构建优化
[ ] 使用多阶段构建
[ ] 分离依赖安装和代码复制
[ ] 合并 RUN 指令减少层数
[ ] 清理包管理器缓存
依赖优化
[ ] 只安装生产依赖
[ ] 使用 npm ci 而非 npm install
[ ] 清理无用的测试/文档文件
文件优化
[ ] 添加 .dockerignore
[ ] 不复制开发文件
[ ] 不复制 .git 目录
安全加固
[ ] 使用非 root 用户
[ ] 设置正确的文件权限
[ ] 使用 tini 作为 init 进程
━━━━━━━━━━━━━━━━━━━━
最终优化版 Dockerfile
- # ==========================================
- # 阶段1:构建
- # ==========================================
- FROM node:20-alpine AS builder
- WORKDIR /app
- # 安装 node-prune
- RUN wget -qO- https://gobinaries.com/tj/node-prune | sh
- # 复制依赖文件
- COPY package*.json ./
- # 安装依赖
- RUN npm ci
- # 复制源码
- COPY . .
- # 构建 + 清理
- RUN npm run build && \
- npm prune --production && \
- node-prune
- # ==========================================
- # 阶段2:运行
- # ==========================================
- FROM node:20-alpine
- WORKDIR /app
- # 安装 tini 并创建用户
- RUN apk add --no-cache tini && \
- addgroup -S appgroup && \
- adduser -S appuser -G appgroup
- # 复制构建产物
- COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
- COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
- COPY --from=builder --chown=appuser:appgroup /app/package*.json ./
- # 安全设置
- USER appuser
- # 健康检查
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
- CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
- EXPOSE 3000
- ENTRYPOINT ["/sbin/tini", "--"]
- CMD ["node", "dist/index.js"]
复制代码
━━━━━━━━━━━━━━━━━━━━
🤔 常见问题
Q1:优化后功能会受影响吗?
A:不会。所有优化都是去除不必要的内容,核心功能完全保留。
Q2:Distroless 无法调试怎么办?
A:开发环境用 Alpine,生产环境用 Distroless:- # 开发
- docker build --target builder -t myapi:dev .
- # 生产
- docker build -t myapi:prod .
复制代码
Q3:node-prune 安全吗?
A:是的,它只删除文档、测试等非运行时文件。但建议在 CI 中充分测试。
━━━━━━━━━━━━━━━━━━━━
📚 本文总结
核心优化技巧
基础镜像:Alpine > 完整镜像(减少 70%+)
多阶段构建:分离构建和运行(减少 40%+)
依赖优化:生产依赖 + node-prune(减少 30%+)
.dockerignore:排除非必要文件(减少 10%+)
记住这个公式
- 最终镜像 = 最小基础镜像 + 运行时依赖 + 编译产物
复制代码
不需要的都不要放进去!
━━━━━━━━━━━━━━━━━━━━
下一步
明天我们将学习:Day 13 - .dockerignore 文件详解
深入学习 .dockerignore 的语法规则和最佳实践。
━━━━━━━━━━━━━━━━━━━━
🐳 加入 Docker 学习群
扫码加入 Docker 学习交流群,和大家一起讨论实践:

🔔 关注公众号,不错过每一篇干货! |
|