找回密码
 立即注册

QQ登录

只需一步,快速开始

peUfSYR.png
查看: 53|回复: 0

Day 12 镜像优化实战:从1.2GB到80MB的优化之路

[复制链接]

876

主题

13

回帖

2808

积分

管理员

积分
2808
发表于 2026-3-29 16:17:09 | 显示全部楼层 |阅读模式
🔍 镜像优化实战:从1.2GB到80MB的优化之路
Docker 30天实战系列 · Day 12

今天,我们通过一个真实案例,完整演示镜像优化的全过程。

你将看到如何把一个 1.2GB 的镜像,一步步优化到 80MB,减少 93%

━━━━━━━━━━━━━━━━━━━━

本文你将学到

  • ✅ 完整的镜像优化流程
  • ✅ 10+ 个实用优化技巧
  • ✅ 每个优化步骤的效果对比
  • ✅ 建立镜像优化检查清单

    阅读时间:约 15 分钟
    实操时间:约 30 分钟
    难度等级:⭐⭐⭐⭐☆

    ━━━━━━━━━━━━━━━━━━━━

    案例背景

    项目情况

    一个 Node.js + TypeScript 的 API 服务:
  • Express.js 框架
  • TypeScript 编译
  • 若干 npm 依赖
  • 开发团队反馈:镜像太大,部署太慢

    原始 Dockerfile
    1. FROM node:20
    2. WORKDIR /app
    3. COPY . .
    4. RUN npm install
    5. RUN npm run build
    6. EXPOSE 3000
    7. CMD ["npm", "start"]
    复制代码

    原始镜像分析
    1. docker build -t myapi:v0 .
    2. docker images myapi:v0
    复制代码

    结果
    1. REPOSITORY   TAG   SIZE
    2. myapi        v0    1.21GB
    复制代码

    问题分析
  • 使用完整 node 镜像(约 1GB)
  • 包含开发依赖
  • 包含 TypeScript 源码
  • 包含 node_modules 中的测试文件

    ━━━━━━━━━━━━━━━━━━━━

    优化步骤

    第1步:分析镜像层

    首先,了解镜像大小的构成:
    1. # 查看各层大小
    2. docker history myapi:v0
    3. # 使用 dive 工具深入分析
    4. docker run --rm -it \
    5.   -v /var/run/docker.sock:/var/run/docker.sock \
    6.   wagoodman/dive myapi:v0
    复制代码

    发现的问题
    | 层 | 大小 | 问题 |
    |-----|------|------|
    | node:20 基础镜像 | 1.0GB | 太大 |
    | npm install | 150MB | 包含 devDependencies |
    | COPY . . | 50MB | 包含不必要文件 |

    ━━━━━━━━━━━━━━━━━━━━

    第2步:使用 Alpine 基础镜像
    1. # 从 node:20 改为 node:20-alpine
    2. FROM node:20-alpine
    3. WORKDIR /app
    4. COPY . .
    5. RUN npm install
    6. RUN npm run build
    7. EXPOSE 3000
    8. CMD ["npm", "start"]
    复制代码
    1. docker build -t myapi:v1 .
    2. docker images myapi:v1
    复制代码

    效果

    | 版本 | 大小 | 减少 |
    |------|------|------|
    | v0 | 1.21GB | - |
    | v1 | 350MB | 71% |

    仅换基础镜像就减少了 71%!

    ━━━━━━━━━━━━━━━━━━━━

    第3步:添加 .dockerignore

    创建
    1. .dockerignore
    复制代码
    文件:
    1. # .dockerignore
    2. node_modules
    3. npm-debug.log
    4. .git
    5. .gitignore
    6. README.md
    7. .env
    8. .env.*
    9. coverage
    10. .nyc_output
    11. dist
    12. *.md
    13. .vscode
    14. .idea
    15. tests
    16. __tests__
    17. *.test.ts
    18. *.spec.ts
    19. Dockerfile
    20. docker-compose*.yml
    21. .dockerignore
    复制代码

    效果

    | 版本 | 大小 | 减少 |
    |------|------|------|
    | v1 | 350MB | - |
    | v2 | 320MB | 9% |

    ━━━━━━━━━━━━━━━━━━━━

    第4步:分离依赖安装和代码复制

    优化层缓存:
    1. FROM node:20-alpine
    2. WORKDIR /app
    3. # 先复制依赖文件
    4. COPY package*.json ./
    5. # 安装依赖(这层可缓存)
    6. RUN npm install
    7. # 再复制源码
    8. COPY . .
    9. RUN npm run build
    10. EXPOSE 3000
    11. CMD ["npm", "start"]
    复制代码

    效果:构建速度提升,代码变更时无需重新安装依赖

    ━━━━━━━━━━━━━━━━━━━━

    第5步:只安装生产依赖
    1. FROM node:20-alpine
    2. WORKDIR /app
    3. COPY package*.json ./
    4. # 只安装生产依赖
    5. RUN npm ci --only=production
    6. COPY . .
    7. RUN npm run build
    8. EXPOSE 3000
    9. CMD ["npm", "start"]
    复制代码

    问题:TypeScript 编译需要 devDependencies!

    解决:使用多阶段构建

    ━━━━━━━━━━━━━━━━━━━━

    第6步:多阶段构建
    1. # ======== 阶段1:构建 ========
    2. FROM node:20-alpine AS builder
    3. WORKDIR /app
    4. # 复制依赖文件
    5. COPY package*.json ./
    6. # 安装所有依赖(包括 devDependencies)
    7. RUN npm ci
    8. # 复制源码并构建
    9. COPY . .
    10. RUN npm run build
    11. # 清理开发依赖
    12. RUN npm prune --production
    13. # ======== 阶段2:运行 ========
    14. FROM node:20-alpine
    15. WORKDIR /app
    16. # 只复制必要文件
    17. COPY --from=builder /app/dist ./dist
    18. COPY --from=builder /app/node_modules ./node_modules
    19. COPY --from=builder /app/package*.json ./
    20. EXPOSE 3000
    21. CMD ["node", "dist/index.js"]
    复制代码
    1. docker build -t myapi:v3 .
    2. docker images myapi:v3
    复制代码

    效果

    | 版本 | 大小 | 减少 |
    |------|------|------|
    | v2 | 320MB | - |
    | v3 | 180MB | 44% |

    ━━━━━━━━━━━━━━━━━━━━

    第7步:优化 npm 安装
    1. FROM node:20-alpine AS builder
    2. WORKDIR /app
    3. COPY package*.json ./
    4. # 使用 npm ci 并清理缓存
    5. RUN npm ci && npm cache clean --force
    6. COPY . .
    7. RUN npm run build
    8. RUN npm prune --production
    9. FROM node:20-alpine
    10. WORKDIR /app
    11. # 清理 apk 缓存
    12. RUN apk add --no-cache tini
    13. COPY --from=builder /app/dist ./dist
    14. COPY --from=builder /app/node_modules ./node_modules
    15. COPY --from=builder /app/package*.json ./
    16. # 使用 tini 作为 init 进程
    17. ENTRYPOINT ["/sbin/tini", "--"]
    18. CMD ["node", "dist/index.js"]
    复制代码

    效果

    | 版本 | 大小 | 减少 |
    |------|------|------|
    | v3 | 180MB | - |
    | v4 | 165MB | 8% |

    ━━━━━━━━━━━━━━━━━━━━

    第8步:压缩 node_modules

    使用
    1. node-prune
    复制代码
    清理无用文件:
    1. FROM node:20-alpine AS builder
    2. WORKDIR /app
    3. # 安装 node-prune
    4. RUN wget -qO- https://gobinaries.com/tj/node-prune | sh
    5. COPY package*.json ./
    6. RUN npm ci
    7. COPY . .
    8. RUN npm run build
    9. RUN npm prune --production
    10. # 清理 node_modules 中的无用文件
    11. RUN node-prune
    12. FROM node:20-alpine
    13. WORKDIR /app
    14. RUN apk add --no-cache tini
    15. COPY --from=builder /app/dist ./dist
    16. COPY --from=builder /app/node_modules ./node_modules
    17. COPY --from=builder /app/package*.json ./
    18. ENTRYPOINT ["/sbin/tini", "--"]
    19. CMD ["node", "dist/index.js"]
    复制代码

    node-prune 清理的内容
    1. *.md
    复制代码
    文件
    1. *.ts
    复制代码
    类型定义源文件
    1. tests/
    复制代码
    目录
    1. examples/
    复制代码
    目录
    1. .github/
    复制代码
    目录

    效果

    | 版本 | 大小 | 减少 |
    |------|------|------|
    | v4 | 165MB | - |
    | v5 | 120MB | 27% |

    ━━━━━━━━━━━━━━━━━━━━

    第9步:合并 RUN 指令

    减少镜像层数:
    1. FROM node:20-alpine AS builder
    2. WORKDIR /app
    3. RUN wget -qO- https://gobinaries.com/tj/node-prune | sh
    4. COPY package*.json ./
    5. RUN npm ci
    6. COPY . .
    7. # 合并多个 RUN 命令
    8. RUN npm run build && \
    9.     npm prune --production && \
    10.     node-prune
    11. FROM node:20-alpine
    12. WORKDIR /app
    13. # 合并 apk 安装
    14. RUN apk add --no-cache tini && \
    15.     addgroup -S appgroup && \
    16.     adduser -S appuser -G appgroup
    17. COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
    18. COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
    19. COPY --from=builder --chown=appuser:appgroup /app/package*.json ./
    20. USER appuser
    21. EXPOSE 3000
    22. ENTRYPOINT ["/sbin/tini", "--"]
    23. CMD ["node", "dist/index.js"]
    复制代码

    效果

    | 版本 | 大小 | 减少 |
    |------|------|------|
    | v5 | 120MB | - |
    | v6 | 115MB | 4% |

    ━━━━━━━━━━━━━━━━━━━━

    第10步:选择更小的基础镜像(可选)

    如果不需要完整的 Node 环境:
    1. FROM node:20-alpine AS builder
    2. # ... 构建步骤同上 ...
    3. # 使用 distroless
    4. FROM gcr.io/distroless/nodejs20-debian11
    5. WORKDIR /app
    6. COPY --from=builder /app/dist ./dist
    7. COPY --from=builder /app/node_modules ./node_modules
    8. COPY --from=builder /app/package*.json ./
    9. EXPOSE 3000
    10. 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. # ==========================================
    2. # 阶段1:构建
    3. # ==========================================
    4. FROM node:20-alpine AS builder
    5. WORKDIR /app
    6. # 安装 node-prune
    7. RUN wget -qO- https://gobinaries.com/tj/node-prune | sh
    8. # 复制依赖文件
    9. COPY package*.json ./
    10. # 安装依赖
    11. RUN npm ci
    12. # 复制源码
    13. COPY . .
    14. # 构建 + 清理
    15. RUN npm run build && \
    16.     npm prune --production && \
    17.     node-prune
    18. # ==========================================
    19. # 阶段2:运行
    20. # ==========================================
    21. FROM node:20-alpine
    22. WORKDIR /app
    23. # 安装 tini 并创建用户
    24. RUN apk add --no-cache tini && \
    25.     addgroup -S appgroup && \
    26.     adduser -S appuser -G appgroup
    27. # 复制构建产物
    28. COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
    29. COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
    30. COPY --from=builder --chown=appuser:appgroup /app/package*.json ./
    31. # 安全设置
    32. USER appuser
    33. # 健康检查
    34. HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    35.   CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
    36. EXPOSE 3000
    37. ENTRYPOINT ["/sbin/tini", "--"]
    38. CMD ["node", "dist/index.js"]
    复制代码

    ━━━━━━━━━━━━━━━━━━━━

    🤔 常见问题

    Q1:优化后功能会受影响吗?

    A:不会。所有优化都是去除不必要的内容,核心功能完全保留。

    Q2:Distroless 无法调试怎么办?

    A:开发环境用 Alpine,生产环境用 Distroless:
    1. # 开发
    2. docker build --target builder -t myapi:dev .
    3. # 生产
    4. docker build -t myapi:prod .
    复制代码

    Q3:node-prune 安全吗?

    A:是的,它只删除文档、测试等非运行时文件。但建议在 CI 中充分测试。

    ━━━━━━━━━━━━━━━━━━━━

    📚 本文总结

    核心优化技巧

  • 基础镜像:Alpine > 完整镜像(减少 70%+)
  • 多阶段构建:分离构建和运行(减少 40%+)
  • 依赖优化:生产依赖 + node-prune(减少 30%+)
  • .dockerignore:排除非必要文件(减少 10%+)

    记住这个公式
    1. 最终镜像 = 最小基础镜像 + 运行时依赖 + 编译产物
    复制代码

    不需要的都不要放进去!

    ━━━━━━━━━━━━━━━━━━━━

    下一步

    明天我们将学习:Day 13 - .dockerignore 文件详解

    深入学习 .dockerignore 的语法规则和最佳实践。

    ━━━━━━━━━━━━━━━━━━━━

    🐳 加入 Docker 学习群

    扫码加入 Docker 学习交流群,和大家一起讨论实践:



    🔔 关注公众号,不错过每一篇干货!
  • 您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    扫码关注微信公众号

    Archiver|手机版|小黑屋|风叶林

    GMT+8, 2026-5-8 23:01 , Processed in 0.152422 second(s), 20 queries .

    Powered by 风叶林

    © 2001-2026 Discuz! Team.

    快速回复 返回顶部 返回列表