冷蟊初退 孤灯野澜 志起鸡鸣 墓不悲秋 技术交流 软件开发 商业合作 加Q:411239339

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

浏览:18次阅读
没有评论

共计 5877 个字符,预计需要花费 15 分钟才能阅读完成。

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

Docker 30 天实战系列 · Day 12

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

你将看到如何把一个 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 文件:

# .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

使用 node-prune 清理无用文件:

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 清理的内容

  • *.md 文件
  • *.ts 类型定义源文件
  • tests/ 目录
  • examples/ 目录
  • .github/ 目录

效果

版本 大小 减少
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 中充分测试。


📚 本文总结

核心优化技巧

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

记住这个公式

最终镜像 = 最小基础镜像 + 运行时依赖 + 编译产物

不需要的都不要放进去!


下一步

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

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


🐳 加入 Docker 学习群

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

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

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

正文完
创作不易,扫码加点动力
post-qrcode
 0
果较瘦
版权声明:本站原创文章,由 果较瘦 于2026-03-29发表,共计5877字。
转载说明:除特殊说明外本站文章皆由果较瘦原创发布,转载请注明出处。