Type something to search...
容器镜像优化实战:缩小 95% 镜像大小攻略

容器镜像优化实战:缩小 95% 镜像大小攻略

优化 Docker 镜像可大幅提升部署效率,以下是关键洞察:

阶段所做的事情镜像体积减少百分比
第一阶段使用 node:latest 构建标准镜像1.22GB0%
第二阶段切换至 node:slim 精简基础镜像327MB73.2%
第三阶段采用 node:alpine 极简基础镜像262MB78.5%
第四阶段实施多阶段构建与 .dockerignore 文件182MB85.1%
第五阶段使用 Distroless 镜像移除包管理器与 shell163MB86.6%
第六阶段编译为静态二进制,基于 Alpine 运行56.6MB95.4%

本文将带您体验从臃肿到精简的优化之旅,分享实用策略,助您打造高效 Docker 镜像。


【大镜像】的实际影响

镜像体积直接影响生产环境中应用的表现,以下是关键洞察:

  • 臃肿镜像拖慢部署速度。

    小型镜像通过 CI/CD 管道的速度更快。每日部署数十个服务时,每次部署节省几分钟可显著累积效率提升。

  • 精简镜像降低存储成本。

    精简镜像占用更少存储空间。团队优化镜像后,容器注册表成本可节省高达 60%。

  • 小型镜像提升安全性。

    减少软件包意味着减少攻击向量。某案例显示,超三分之二的漏洞与不必要的依赖相关。

镜像体积直接影响应用启动时间,进而影响自动扩展、恢复时间和用户体验。


示例应用

本文以典型的 Express 应用为例,展示生产环境中常见的 Node.js 微服务,以下是关键步骤:

  • 构建包含核心依赖的 Express 微服务。

    该应用包括 Express 用于路由、Moment 用于时间格式化、Mongoose 用于 MongoDB 交互,以及 CORSHelmet 中间件处理跨域请求和提升安全性。实际微服务可能包含日志、指标或认证中间件,此处为简化仅保留基础功能。

  • 初始化 Node.js 项目并安装依赖。

    创建项目并安装依赖。(请查看附件)

  • 创建 index.js 实现基本功能

    该代码实现基本路由、健康检查和错误处理。(请查看附件)


第一阶段:标准基线方法

大多数开发者会采用以下简单的 Dockerfile 作为起点,以下是关键步骤:

  • 基于 node:latest 的标准 Dockerfile 构建镜像。

    直接从教程或 Stack Overflow 复制的 Dockerfile,使用 node:latest 作为基础镜像:


  FROM node:latest

  WORKDIR /app

  COPY package*.json ./
  RUN npm install

  COPY . .

  EXPOSE 3000
  CMD ["node", "index.js"]
  • 通过命令构建镜像:

$ docker build -t node-app:phase1 .

$ docker images | grep node-app
node-app    phase1    5ff5a6469537    24 minutes ago    1.22GB

🚀 初始镜像体积高达 1.22GB。尽管应用代码仅占几兆字节,镜像却包含完整 Linux 发行版、Node.js 运行时及众多不必要依赖。


第二阶段:选择合适的基础镜像

切换到更精简的基础镜像带来显著优化,以下是关键步骤与成果:

  • 使用 node:slim 基础镜像消除冗余。

    采用 node:slim 镜像,移除运行时不必要的软件包和工具,简化容器内容。

  • 精简镜像降低安全风险。

    减少不必要组件显著缩小容器攻击面,提升安全性。

  • 更小镜像加速构建与拉取。

    精简基础镜像大幅缩短部署过程中的构建和拉取时间,提高效率。

更新后的 Dockerfile 如下:


FROM node:slim
...
  • 构建并测量镜像体积:

$ docker build -t node-app:phase2 .

$ docker images | grep node-app
node-app    phase2    e604e921cd98    2 seconds ago     327M
node-app    phase1    5ff5a6469537    24 minutes ago    1.22GB

🚀 通过仅更改一个词(node:latest 到 node:slim),镜像体积减少 73.2%,仅 327MB,基础镜像选择是影响容器体积的最关键决策。


第三阶段:使用 Alpine 基础镜像

进一步优化采用 Alpine,这是一个以超小体积和安全性著称的极简 Linux 发行版,以下是关键步骤与成果:

  • 采用 node:alpine 基础镜像大幅缩小体积。

    使用 node:alpine 镜像,不仅显著减少镜像体积,还大幅降低容器攻击面。

更新后的 Dockerfile 如下:


FROM node:alpine
...
  • 构建并测量镜像体积:

$ docker build -t node-app:phase3 .

$ docker images | grep node-app
node-app    phase3    8a88077b04a0    4 seconds ago     262MB
node-app    phase2    e604e921cd98    5 minutes ago     327M
node-app    phase1    5ff5a6469537    40 minutes ago    1.22GB

🚀 新镜像体积仅 262MB,相比初始 1.22GB 减少 78.5%,展现 Alpine 的高效精简能力。

🧐 Alpine 高效的原因在于极简设计。Alpine 使用 musl libc 替代 glibc、BusyBox 替代 GNU Core Utilities,并提供小型软件包仓库,整个发行版仅 5MB。

⚠️ Alpine 需谨慎验证本地依赖兼容性。在某客户项目中,开发和测试环境运行正常,但生产环境出现随机崩溃。调查发现,某本地依赖在 musl libc 下的行为与 glibc 不同。因此,使用 Alpine 时需彻底验证构建,尤其涉及编译的本地模块。


第四阶段:多阶段构建与 .dockerignore 文件

多阶段构建是一种强大的优化技术,可显著缩小镜像体积,以下是关键步骤与成果:

  • 多阶段构建分离构建与运行环境。

    多阶段构建使用完整 Node 镜像进行编译、测试或打包,然后仅将必要产物复制到基于 Alpine 的精简运行时镜像中,剔除开发依赖和不必要文件。

  • .dockerignore 文件优化构建内容。

    .dockerignore 类似 .gitignore,限制镜像打包内容,提升构建清洁度和安全性。在构建前,添加 .dockerignore 文件排除不必要文件 (请查看附件)

以下是多阶段构建的 Dockerfile:

# 构建阶段

FROM node:alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# 运行时阶段
FROM node:alpine

WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY --from=builder /app/*.js ./

EXPOSE 3000
CMD ["node", "index.js"]
  • 构建并测量镜像体积:

$ docker build -t node-app:phase4 .

$ docker images | grep node-app
node-app    phase4    d0009cbf6598    3 seconds ago      182MB
node-app    phase3    8a88077b04a0    7 minutes ago      262MB
node-app    phase2    e604e921cd98    13 minutes ago     327M
node-app    phase1    5ff5a6469537    48 minutes ago     1.22GB

🚀 相比初始 1.22GB 减少 85.1%,多阶段构建有效剔除构建工具和中间文件。

🚀 多阶段构建特别适合 TypeScript 项目。在某些大项目中,TypeScript 编译和测试生成超 300MB 产物。单阶段构建会包含这些文件,而多阶段构建仅保留编译后的 JavaScript,显著精简镜像。


第五阶段:采用 Distroless 镜像

谷歌的 Distroless 镜像将极简主义推向极致,仅包含应用运行所需的最少运行时依赖,以下是关键步骤与成果:

  • Distroless 镜像移除包管理器与 shell。

    Distroless 镜像剔除操作系统包管理器(如 apk、apt)和 shell(如 /bin/sh),防止攻击者在入侵后安装恶意软件,大幅降低攻击面。

  • Distroless 镜像极小且不可变。

    镜像仅包含应用及其运行时依赖,通常比 Alpine 镜像更小,适合追求可重现性、体积和安全性的生产环境。

以下是使用 Distroless 的 Dockerfile:


# 构建阶段
FROM node:22-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install --only=production

COPY . .

# 运行时阶段
FROM gcr.io/distroless/nodejs22

WORKDIR /app

COPY --from=builder /app /app

CMD ["index.js"]
  • 构建并测量镜像体积:

$ docker build -t node-app:phase5 .

$ docker images | grep node-app
node-app    phase5    165ff003b03f   4 seconds ago    163MB
node-app    phase4    d0009cbf6598   6 minutes ago    182MB
node-app    phase3    8a88077b04a0   14 minutes ago   262MB
node-app    phase2    e604e921cd98   19 minutes ago   327MB
node-app    phase1    5ff5a6469537   55 minutes ago   1.22GB

🚀 新镜像体积仅 163MB,相比初始 1.22GB 减少 86.6%,在不牺牲功能或安全性的情况下实现惊人优化。

🔐 Distroless 镜像无 shell 增强安全性。Distroless 镜像仅包含应用及其运行时依赖,无包管理器、无 shell、无多余组件,不仅节省空间,还极大增强安全性。


第六阶段:静态二进制文件

通过将 Node.js 应用编译为独立二进制文件,可实现极致的体积优化,以下是关键步骤与成果:

  • 编译 Node.js 应用为静态二进制文件。

    使用 pkgnexe 等工具,将应用及其 Node.js 运行时打包为单一可执行文件,降低攻击面和依赖复杂性。

  • 静态二进制支持超小镜像。

    编译后的二进制文件无需 Node.js 运行时,可基于 scratch 或 Distroless 镜像运行,镜像体积通常低于 20MB,显著提升冷启动速度。

以下是使用静态二进制的 Dockerfile:

# 构建阶段
FROM node:alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production
RUN npm install -g pkg

COPY . .
RUN pkg --targets node16-alpine-x64 index.js -o app

# 运行时阶段
FROM alpine:latest

WORKDIR /app
COPY --from=builder /app/app .

EXPOSE 3000
CMD ["./app"]
  • 构建并测量镜像体积:

$ docker build -t node-app:phase6 .

$ docker images | grep node-app
node-app    phase6    73c6db9a49f6   4 seconds ago    56.6MB
node-app    phase5    165ff003b03f   4 minutes ago    163MB
node-app    phase4    d0009cbf6598   11 minutes ago   182MB
node-app    phase3    8a88077b04a0   19 minutes ago   262MB
node-app    phase2    e604e921cd98   24 minutes ago   327MB
node-app    phase1    5ff5a6469537   59 minutes ago   1.22GB

🚀 最终镜像体积仅 56.6MB,相比初始 1.22GB 减少 95.4%,剔除不必要层、软件包和依赖,实现巨大优化。

⚠️ 指定 linux/amd64 平台确保兼容性。使用 —platform=linux/amd64 参数确保二进制文件在 x86_64 架构上运行,避免 ARM 架构(如 Mac)上的兼容性问题。

⚠️ 静态编译适合简单应用,但在复杂依赖场景下可能出现问题。例如,某应用在开发环境正常,但在生产环境因动态模块加载失败,需仔细验证。


使用 Slim(原名 DockerSlim)优化镜像

Slim 是一款强大的开源工具,可自动化精简容器镜像,以下是关键步骤与成果:

  • Slim 自动精简镜像大幅减小体积。

    Slim(原 DockerSlim)通过运行时分析镜像,识别应用所需组件,移除不必要内容,体积可减少 30 倍以上,同时提升安全性。

  • 无需修改 Dockerfile 或应用代码。

    Slim 无需重写 Dockerfile 或调整应用结构,即可生成精简镜像,简化优化流程。

  • Slim 通过沙箱分析生成优化镜像。

    Slim 在沙箱环境中运行容器,跟踪系统调用,仅保留必要运行时组件,生成以下产物:

    • 精简镜像(slim.<原始镜像名>)
    • Seccomp 配置文件,增强容器安全性
    • 报告目录,包含文件、软件包和网络使用详情

以下是安装与使用 Slim 的步骤,针对第一阶段未优化的 Node.js 镜像(node-app:phase1):

  • 在 macOS 上通过 Homebrew 安装 Slim:

brew install docker-slim
slim --version
  • 对 node-app:phase1 镜像执行优化命令:

$ slim build --target node-app:phase1 --tag node-app:slim --http-probe=false --include-path /app

$ docker images | grep node-app
node-app    slim      8b423f794ee3   28 seconds ago      177MB
node-app    phase6    73c6db9a49f6   About an hour ago   56.6MB
node-app    phase5    165ff003b03f   About an hour ago   163MB
node-app    phase4    d0009cbf6598   About an hour ago   182MB
node-app    phase3    8a88077b04a0   About an hour ago   262MB
node-app    phase2    e604e921cd98   About an hour ago   327MB
node-app    phase1    5ff5a6469537   2 hours ago         1.22GB

🚀 Slim 通过单条命令将 1.22GB 镜像精简至 177MB,减少约 85.5%,效果显著。

⚠️ 谨慎使用 Slim 避免遗漏关键文件。在某项目中,Slim 移除仅在罕见错误处理路径使用的文件,导致生产环境偶发故障。需深入了解应用运行时行为,指导优化过程。Slim 虽强大,但非万能,结合应用特性可实现最佳优化效果。


为什么不直接使用 Slim?

看到 Slim 的惊人优化效果后,你可能好奇为何还要费力进行手动优化。以下是手动优化的关键理由:

  • 手动优化便于故障排查。

    当 Slim 误删关键文件时,理解 Docker 优化原理可帮助快速诊断和修复问题,自动化工具难以完全应对。

  • 手动优化提供精确控制与可预测性。

    手动优化确保镜像仅包含必要组件,构建结果更可预测,避免意外包含冗余内容。

  • 手动优化便于安全验证与合规。

    安全团队需全面了解容器内容,手动优化的镜像便于记录和验证组件,确保符合安全策略。

  • 手动优化知识广泛适用。

    容器优化原理可跨语言和框架应用,手动优化培养的技能适用于各类项目。

  • 手动优化支持定制与灵活性。

    手动优化可根据特定需求调整镜像,如优化构建流水线或满足存储限制。

  • 手动优化加深构建过程理解。

    手动优化帮助深入理解 Docker 层交互和 Dockerfile 结构,最大化构建效率。

手动优化不仅是技术基础,还为自动化工具(如 Slim)提供必要补充,确保更高效、安全的容器化流程。


优化过程中可能遇到的问题

优化显著提升了镜像效率,但也带来了一些需要解决的挑战,以下是关键点:

  • Alpine 兼容性问题需额外调整。

    Alpine 环境中,本地依赖可能表现异常,导致兼容性问题。某些库或工具可能无法正常运行,需额外配置或调整。

  • Distroless 容器调试难度增加。

    Distroless 容器极简高效,但因缺乏调试工具,问题定位和解决变得更复杂耗时。

  • 多阶段构建增加 Dockerfile 复杂性。

    管理多阶段构建使 Dockerfile 更复杂。部分团队成员难以理解构建流程,确保正确包含和排除层成为挑战。

  • 团队采纳优化实践需培训。

    推动团队全面采用优化最佳实践需时间和教育。并非所有成员熟悉这些实践,需通过培训和持续学习获得全員支持。

优化需根据具体应用权衡体积、功能和安全性,找到最佳平衡点。


其他优化技术

以下是一些行之有效的优化技术,可进一步精简 Docker 镜像:

  • 优化层结构减少镜像层数。

Docker 镜像由层组成,Dockerfile 每条指令生成一层。合并相关命令可减少层数,显著缩小镜像体积。错误示例:


RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2

🚀 正确示例


RUN apt-get update && \
    apt-get install -y package1 package2 && \
    rm -rf /var/lib/apt/lists/*
  • 利用 BuildKit 缓存挂载加速构建。

对于 Node.js 应用,npm 缓存可显著提升构建速度,同时避免缓存进入最终镜像:

# syntax=docker/dockerfile:1.4
FROM node:alpine

WORKDIR /app

COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

COPY . .

EXPOSE 3000
CMD ["node", "index.js"]

BuildKit 缓存挂载技术保持 npm 缓存外部化,优化构建效率。

  • 使用辅助工具深入分析与优化镜像。

除了手动优化和 Slim 自动化,以下工具可进一步提升镜像分析与优化:

  • dive:命令行工具,视觉化探索 Docker 镜像层,识别冗余文件和低效层顺序,优化镜像体积。
  • Docker Scout:Docker 官方镜像分析与安全工具,提供漏洞、过期依赖和体积优化的洞察,基于镜像元数据和 SBOM(软件物料清单)。
  • Buildpacks:CNCF 项目,无需 Dockerfile 即可从源码构建容器镜像,遵循最佳实践,生成精简的生产就绪镜像。 这些工具帮助深入了解容器内容,超越基础 Dockerfile 优化。

总结思考

1.22GB56.6MB 的优化历程不仅关乎节省磁盘空间,更体现了效率、安全和工程卓越的理念,以下是关键洞察:

  • 精简容器带来多重长期收益。

    优化技术随时间累积显著优势,包括:

    • 更快部署速度
    • 更低基础设施成本
    • 更佳安全态势
    • 更高资源利用率
    • 更快速水平扩展
  • 优化是一个渐进过程而非终极目标。

    即使无法实施所有技术,每一步优化都带来收益。例如,将镜像从 1GB 减至 200MB 已是巨大成功。

从适合项目的优化技术入手,逐步引入高级方法。容器效率的提升是迭代过程,但回报值得努力。


附件

  • 创建项目

mkdir docker-demo
cd docker-demo
npm init -y
npm install express moment mongoose cors helmet
npm install --save-dev nodemon jest supertest
  • index.js
const express = require("express");
const moment = require("moment");
const cors = require("cors");
const helmet = require("helmet");

// 初始化 Express
const app = express();
const PORT = process.env.PORT || 3000;

// 中间件
app.use(cors());
app.use(helmet());
app.use(express.json());

// 路由
app.get("/", (req, res) => {
  res.json({
    message: "Hello from a slim Docker container!",
    timestamp: moment().format("MMMM Do YYYY, h:mm:ss a"),
    uptime: process.uptime(),
  });
});

app.get("/health", (req, res) => {
  res.json({ status: "UP", memory: process.memoryUsage() });
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: "Something went wrong!" });
});

// 启动服务器
if (process.env.NODE_ENV !== "test") {
  app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
  });
}

module.exports = app; // 用于测试
  • .dockerignore

  - node_modules
  - npm-debug.log
  - tests
  - coverage
  - Dockerfile
  - .dockerignore
  - .env
  - \*.md

联系我们

有任何云成本管理的需求或问题?欢迎通过以下方式联系我们!

公众号

Mofcloud 微信公众号二维码

企业微信客服

Mofcloud 企业微信客服二维码

业务咨询

contact@mofcloud.com

技术社区

mofcloud/issuer

地址

北京市海淀区自主创新大厦 5层

推荐阅读