← 全部笔记

Docker · 8 安全与生产实践

镜像体积与漏洞扫描

只交付运行所需的文件,再扫描它们的已知漏洞。体积和扫描结果要分别验证。

约 15 分钟 · 截至 2026-11-03,Docker Engine 29.x · 实验未实测 · 工具口径:Docker Scout CLI v1.29.0

多阶段构建只复制运行所需文件 左边构建阶段包含编译器、缓存和产物。只复制产物到右边的运行基础镜像;最终镜像仍需要扫描。方块表示文件组成,不按体积比例绘制。 构建阶段 编译器、下载缓存 运行产物 构建基础镜像 只复制产物 最终镜像 → 再扫描 COPY --from=build 运行基础镜像
图 1 不复制构建工具可减少交付内容;剩下的依赖仍要维护和扫描。图示不按字节数绘制。手机上可在图内横向滚动。

前置回顾:镜像、容器、仓库。本页关注交付镜像,容器运行时产生的文件不计入镜像体积。

机制 · 约 3 分钟

留下的层

镜像体积来自保留的文件层。基础镜像本身的文件也在其中,不只是你的代码。构建残留指运行时用不着的编译器、缓存、测试数据等。

层保存一次文件系统改动。新层能把旧文件隐藏,却不能改写已经保存的旧层。下面用 /tmp/cache 代表一份下载缓存;用方块表示文件是否存在,不表示大小。

跨 RUN 删除和同一 RUN 清理的区别 左侧先保存含缓存的层,再增加删除标记,旧层缓存仍保留。右侧在同一次 RUN 结束前删除缓存,保存的新增层只含程序。镜像层从下往上堆叠。 分开 RUN 同一 RUN 内清理 FROM:基础镜像(底层) FROM:基础镜像(底层) RUN:下载、安装后保存 程序 + /tmp/cache 下一条 RUN:删除缓存 只新增「隐藏旧文件」标记 缓存不可见,但旧层仍占空间 RUN 进行中,尚未保存 程序 + /tmp/cache 同一 RUN:先删除缓存 只剩程序,等待保存 RUN 结束,保存新增层 只包含程序的文件改动 缓存从未进入这个新增层
  1. 左侧第一条 RUN 把程序和缓存一起保存为层。猜猜:下一条 RUN 删除缓存,能让已经保存的层缩小吗?
  2. 第二条 RUN 保存删除标记,不会回头修改第一层。
  3. 容器里看不到缓存了,镜像仍带着第一层的缓存数据。
  4. 右侧把下载、安装和清理放在同一条 RUN。此刻缓存只是构建中的临时文件。
  5. 在这条 RUN 结束前清理本次生成的缓存;尚未保存新增层。
  6. 最终只保存程序的文件改动;左侧的多余缓存依然留在旧层里。
图 2 在生成缓存的同一条 RUN 内清理,才能避免把它保存进新增层。右侧不适用于基础镜像原本已有的文件。

先选满足运行需要的基础镜像,再排除多余文件:不要安装运行时用不着的包;用 .dockerignore 排除构建上下文里的无关文件;多阶段构建只把产物复制进最终阶段。参见 Docker 构建建议与多阶段构建。

深入一点:构建缓存和交付镜像是两个对象

多阶段构建的中间结果可以留在构建机缓存里,却不必进入最终镜像的层链。清理构建机缓存可能腾出本机空间,但不会重写你已经推送的镜像。要让交付镜像变小,应改变最终阶段保留的内容并重新构建。

如果最终阶段用 FROM build 继承整个构建阶段,构建工具也会被继承。图 1 假设最终阶段从单独的运行基础镜像开始。

动手 · 约 4 分钟

同口径比较

先固定平台和测量列,再谈谁更小。对比 debian:bookworm、debian:bookworm-slim 和 alpine:3.23,记录标签当时指向的摘要(digest,内容的哈希标识)。同一标签以后可能指向新内容。

未实测

编写环境没有 Docker CLI 和守护进程。以下是可复制实验及预期行为,没有实测大小和扫描结果。表格留待维护者补录,不能据空表计算排名或节省比例。

在宿主机的 Bash 中运行。需要 Docker Engine 29.x、已启动的 Linux 容器环境,以及拉取镜像的网络。下面选择 linux/amd64;ARM 机器请统一改为 linux/arm64,避免依赖跨架构模拟。

platform=linux/amd64
images=(debian:bookworm debian:bookworm-slim alpine:3.23)
date -u +%F
docker version
docker info --format '{{.Driver}} {{json .DriverStatus}}'
for image in "${images[@]}"; do
  docker pull --platform "$platform" "$image"
  docker image inspect --platform "$platform" \
    --format '{{json .RepoDigests}} {{.Os}}/{{.Architecture}}' "$image"
  docker image ls "$image"
  docker run --rm --network none --platform "$platform" "$image" \
    sh -c 'printf "runtime-ok\n"'
done

预期:每次 inspect 显示摘要和所选平台;每次 ls 有该镜像的一行。容器里的 printf 指定标准输出为 runtime-ok,共执行三次;这是预期输出,未实测。它只验证 shell 能运行,不证明你的应用兼容。

实验会下载镜像、占用本地磁盘并短暂创建容器。--rm 在命令退出后删除容器;--network none 让实验容器没有外部网络。镜像会保留,没有端口发布、宿主目录挂载或特权操作。

别把两列混起来

Docker 29 的人类可读列表在表头前有 WARNING: This output is designed for human readability... 提示(CLI 29.1.3 源码)。表头是 IMAGE ID DISK USAGE CONTENT SIZE EXTRA。这里只描述格式,不伪造一段实测终端。官方教程也展示了双列格式。

若本地还保存了同一标签的其他平台,顶层行可能合计多个平台。用 docker image ls --tree "$image" 核对,读取目标平台子行,或在只保存目标平台的独立实验环境中比较;不要为本实验删除已有镜像。

待补录记录 · 平台 linux/amd64 · 大小、摘要、实测日期均未实测
镜像比较目的DISK USAGECONTENT SIZE
debian:bookworm完整变体,作为对照未实测未实测
debian:bookworm-slim同发行版的精简变体未实测未实测
alpine:3.23另一发行版;需验证兼容性未实测未实测

三者都是操作系统基础镜像,不包含你的完整应用。选型还要补上运行库、证书和应用依赖,再测最终镜像。空白的扫描结果也不能当作“零漏洞”。

深入一点:为什么换机器可能测出不同值?

平台、标签指向、CLI 版本和存储后端都会影响记录。Docker Engine 29.0 起,全新安装默认用 containerd 镜像存储;从旧版升级可能仍用原来的后端。containerd 会保留压缩内容和解压后的快照,所以本地占用不能当作下载流量。参见存储说明。

若列表不符合本节表头,先记录实际客户端版本、存储后端和自定义格式;不要硬套本表。这里只测量,不要求修改 Docker 配置。

要定位哪条指令贡献体积,再读历史:

docker image history --platform "$platform" --no-trunc \
  --format 'table {{.CreatedBy}}\t{{.Size}}' debian:bookworm-slim

预期是指令与大小的表格,最新历史在上方;这不是 ls 的双列格式。历史里的元数据指令可能不新增文件,不能只数行数判断体积。参见history 参考。

已经记录了相同平台和日期。要比较镜像内容大小,你应对三行都读取哪一列?

列名:

三行都读 CONTENT SIZE,并保留单位和摘要。DISK USAGE 也可作为另一组比较,但不能把一个镜像的 CONTENT SIZE 与另一个的 DISK USAGE 比。

你把“下载缓存”和“删除缓存”拆成两条 RUN,容器里已经找不到缓存。该怎样判断减重是否成功?

容器显示叠加后的文件视图。文件被隐藏,不表示旧层被改写。

后续层只能记录新的改动,不能把不可变的旧层改小。

需要让缓存在生成它的 RUN 结束前被删除,再重新构建。对照相同平台、同一大小列,才能判断新镜像是否减少了交付内容。

读报告 · 约 4 分钟

扫描读什么

扫描器从镜像里识别软件包和版本,再匹配已公开的漏洞公告。包清单叫软件物料清单(Software Bill of Materials,SBOM);漏洞编号常用公共漏洞与暴露(Common Vulnerabilities and Exposures,CVE)。

扫描匹配软件版本和漏洞公告 最终镜像被识别为包清单,包名和版本与公告匹配,产生待处理的发现。公告更新后,同一镜像的报告也可能变化。 最终镜像 系统包 + 应用 识别 SBOM 包清单 包名、版本、来源 漏洞公告(会更新) 匹配 待处理的发现 CVE、等级、修复
图 3 报告取决于镜像内容和当时的漏洞公告;字节数不能替代这次匹配。

先保存全量,再筛重点

本页使用 Docker Scout CLI v1.29.0(截至 2026-11-03 的固定工具版本,未实测)。Docker Desktop 预装 Scout;单独的 Engine 需要按官方说明安装此版本。先运行版本命令核对;需要 Docker 账号登录和访问分析服务的网络。

docker scout version
docker login
scan_image=debian:bookworm-slim
docker image inspect --platform "$platform" \
  --format '{{json .RepoDigests}}' "$scan_image"
docker scout cves --platform "$platform" "local://$scan_image" \
  --output scout-full.txt
docker scout cves --platform "$platform" "local://$scan_image" \
  --only-severity critical,high --only-fixed

接着上一节的 Bash 会话运行;local:// 强制使用本地镜像。预期第一遍写入全量报告 scout-full.txt,第二遍只显示“有修复版本的严重或高危”发现。本页未实测,不能预报条数。筛选后没有发现,可能只是其他等级或未修复的发现被隐藏了。参数见cves 参考。

复查时一并保存 UTC 日期、镜像摘要、平台、Scout 实际版本、命令及报告。固定工具版本不能固定服务端漏洞公告;同一摘要隔天复扫也可能不同。

从包追到修复版本

下面逐行摘自 Docker 官方教程的模拟终端,省略定位和其他漏洞。它用于读字段,非本页实测,也不表示这个包现在只有这一条漏洞。

pkg:npm/path-to-regexp@0.1.7
✗ HIGH CVE-2024-52798 [Inefficient Regular Expression Complexity]
https://scout.docker.com/v/CVE-2024-52798?s=github&n=path-to-regexp&t=npm&vr=%3C0.1.12
Affected range : <0.1.12
Fixed version : 0.1.12
CVSS Score : 7.7
包名与已装版本
pkg:npm/path-to-regexp@0.1.7 标识 npm 生态里的这个包,版本为 0.1.7。npm 是 JavaScript 包管理生态;这里的包不是 Debian 的系统包。
漏洞编号与等级
CVE-2024-52798 指向具体公告。HIGH 是高危;概览中 C / H / M / L 对应严重 / 高危 / 中危 / 低危,U 表示未指定。包数和漏洞数不相等,一个包可以命中多条公告。
受影响范围与修复版本
Affected range 是公告认定受影响的版本范围;Fixed version 是对应修复版本。片段中 0.1.7 落在 <0.1.12 的范围内。升级越过这一条的修复边界,还要检查其余公告和应用兼容性。
分数与实际风险
CVSS 是通用漏洞评分系统(Common Vulnerability Scoring System),描述漏洞严重程度。它不证明你的运行路径一定可被利用。Scout 的等级与分数可能采用不同公告来源,应打开公告核实。参见等级来源说明。

某条 HIGH 发现的 Fixed version 显示 not fixed。加上 --only-fixed 后它不见了。你能据此宣布风险已消除吗?先写出理由。

不能。not fixed 表示当前公告没有列出修复版本,筛选器只是隐藏它。仍应保留全量报告,评估替换依赖、移除不需要的功能或限制暴露,并跟踪后续修复。字段含义见官方未修复示例。

决策 · 约 2 分钟

修复再验证

先找包从哪来,再改负责它的输入。基础镜像带来的系统包,要跟踪其维护者的更新;应用依赖则在项目依赖声明和锁定文件中更新。锁定文件记录实际解析出的依赖版本。

按来源修复,再对新的交付镜像验证 基础镜像和应用依赖分别更新,构建残留移出最终阶段。三条路径汇入重建、运行测试和全量复扫,最后记录新摘要和结果。 系统包来自基础镜像 → 更新受维护的基础版本 应用依赖来自项目 → 更新依赖与锁定文件 构建残留无需运行 → 移出最终阶段 重建 新镜像 运行测试 复扫 全量报告 新摘要 同口径大小
图 4 修复要落到重建后的交付镜像;只更新源码或在旧容器内升级,都不能证明新镜像已修复。
深入一点:重建为什么还需要关注拉取与缓存?

在你自己的项目目录里,docker build --pull -t my-app:scan-check . 会检查 FROM 引用是否有更新;如果固定了摘要,需先按维护流程更新该摘要。--no-cache 会让构建步骤重新执行,但不会代替 --pull 刷新基础镜像。构建命令未实测,依赖你的项目 Dockerfile。

更新基础镜像也不自动修复你复制进去的应用包。反过来,更新应用包也不代替系统包更新。参见Docker 重建建议。

你把完整基础镜像换成 slim,体积下降,但同一应用包仍报 HIGH。用两句话解释原因,并给出下一步。

精简基础镜像减少了一部分系统文件,却未改变复制进去的应用依赖版本。先定位该包的公告与修复版本,更新项目依赖和锁定文件,再构建、运行测试并对新摘要复扫。

  • 区分基础镜像文件和应用依赖。
  • 指出体积减少不能直接证明漏洞修复。
  • 包含重建、测试和对新镜像复扫。

回顾 · 约 2 分钟

要记住的

精简后镜像更小。全量报告里仍有一条“有修复版本的 HIGH”。哪项最能支持交付决定?

镜像可能少了无关文件,但受影响的包和版本仍然存在。

这条发现有修复版本,会被 --only-fixed 保留。筛选本身不会改变包版本。

应把报告转成可核验的改动。更新正确来源,测试新镜像的行为,并保存新摘要、工具版本、日期和全量报告,才能判断这次修复的效果。

来源

以下来源于 2026-11-03 核对。命令按 Docker Engine 29.x;Scout 选择固定版本 v1.29.0。官方教程中的模拟输出只用于字段教学。

  1. Docker Docs:Building best practices;Multi-stage builds。基础镜像、同层清理、重建与阶段边界。
  2. Docker Docs:Storage drivers;containerd image store。不可变层、共享数据、压缩内容与本地存储。
  3. Docker Learn:Shrink your image,Step 5。双体积列;image ls、inspect、history 命令参考。
  4. Docker Official Images 标签登记:Debian、Alpine。仅据此选择实验标签,没有引用或推算大小。
  5. Docker:Scout CLI v1.29.0 发布记录;安装说明。
  6. Docker Docs:Scout image analysis、scout cves。包匹配、等级来源、筛选与报告保存。
  7. Docker Learn:Find and fix vulnerabilities before you ship。本页扫描片段的原文出处,以及扫描边界。