共计 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 中充分测试。
📚 本文总结
核心优化技巧
- 基础镜像:Alpine > 完整镜像(减少 70%+)
- 多阶段构建:分离构建和运行(减少 40%+)
- 依赖优化:生产依赖 + node-prune(减少 30%+)
- .dockerignore:排除非必要文件(减少 10%+)
记住这个公式
最终镜像 = 最小基础镜像 + 运行时依赖 + 编译产物
不需要的都不要放进去!
下一步
明天我们将学习:Day 13 – .dockerignore 文件详解
深入学习 .dockerignore 的语法规则和最佳实践。
🐳 加入 Docker 学习群
扫码加入 Docker 学习交流群,和大家一起讨论实践:

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