← 全部笔记

Docker · 2 镜像与 Dockerfile

镜像只存变化,容器修改先复制

把只读层自下而上叠起来,再给每个容器加一层自己的可写空间。

约 15 分钟 · 截至 2026-11-03,Docker Engine 29.x · 本页实验未实测

上层记录差异,下面的层保持原样。

层(layer)记录新增、修改或删除文件的变化。四个文件层合起来,让程序看到一套完整目录。

大小取自 官方示例 acme/my-final-image:1.0。这是文档中的历史样本,不是 Docker 29.x 的本机实测,也不是如今 Alpine 的固定大小。

图 1 四个文件层自下而上叠加;CMD 在层栈之外,继承的其他配置记录已省略。
学完你能做到
  • 能区分 Dockerfile 的历史记录与文件系统层。
  • 能从 docker history 读出来源、顺序与每层大小。
  • 能解释修改、删除文件时,可写层发生了什么。
  • 能用实验验证:两个镜像共享底层,容器的修改各自隔离。

先分清两件事

分层只存变化

镜像的文件层一旦生成就保持只读。上一页的镜像与容器关系,在这里细化成了“共享的只读层 + 各自的可写层”。

Dockerfile 是描述如何构建镜像的文本文件。本页只用它辨认变化来源;具体语法留给下一课。

读图 1 前,只需认识这些指令
指令本页含义对层的影响
FROM选择起点镜像继承它已有的层,不把它们压成一层
RUN构建时执行命令把命令产生的文件变化记入新层
COPY / ADD把文件加入镜像;此处 ADD 导入基础文件记录加入的文件
CMD / ENV设置默认启动命令 / 环境变量只改元数据,即文件以外的配置

“每条指令一层”是便于入门的说法。更准确地说:改变文件系统的构建步骤产生文件层;历史中的一行不一定对应一个文件层。 图 1 中的 CMD 没有新增文件。

在图 1 的镜像后加一条 ENV MODE=demo,再加一条 COPY 新文件的指令。它们会各增加一个文件层吗?先写理由。

ENV 只记录环境变量配置,不增加文件层。COPY 加入新文件,会产生文件变化层。因此不能用指令行数直接数出文件层数。

同一个路径在多层里都存在,会看到哪份?

容器看到的是合并后的目录视图:同一路径通常由更上层的版本覆盖显示。底层旧文件仍保留。新增文件会出现;删除标记则会隐藏下层的同名文件。

构建时的 RUN 也遵守这条规则。下一条 RUN 删除上一层加入的大文件,通常不能收回上一层已占的镜像空间。如何安排构建步骤,留到后面的镜像优化课程。

把层栈和终端对上

历史倒着读

docker history 是 docker image history 的别名。默认从最新记录往最旧记录显示,正好对应图 1 从上往下读。

下面原样节选官方文档的输出:只保留表头和 COPY 那一行,其余行省略。镜像必须先存在于本地才能运行此命令;这不是本页实验生成的镜像。

docker image history acme/my-final-image:1.0
IMAGE CREATED CREATED BY SIZE COMMENT
<missing> 3 minutes ago COPY . /app # buildkit 222B buildkit.dockerfile.v0

遇到截断的命令,用 docker history --no-trunc 镜像名。需要和层栈核对时先看来源,再看大小,最后辨认哪些只是配置记录。

只数图 1 选出的五条记录(四个文件变化和顶部 CMD),其中有 个文件系统层。

四条记录写入了文件变化;CMD 只设置默认启动命令。原文还保留了基础镜像继承的 CMD 等记录,图 1 省略了它们。0B 是配置记录的显示值,不表示整个镜像不占空间。

看到上面的 COPY 行,你能得出哪个结论?

missing 在 IMAGE 列;BuildKit 不必为每步保留一个中间镜像 ID。

这是某一步的 SIZE。图 1 下面还有基础文件和 bash 等层。

history 展示各步记录。COPY 的 222B 只描述本步差异;是否有中间镜像 ID 与文件层是否存在是两件事。

容器改变自己的视图

修改先复制

写时复制(copy-on-write,CoW)让容器先共享只读文件,首次修改时才为自己保存变化。下面按 OverlayFS(Linux 上合并显示多层目录的文件系统)的文件级复制过程示意:先复制文件,再改副本;镜像原件保持不变。

从读原件到修改副本的六步过程 下方镜像层有 note 等于 base。容器 A 读取它;写入时把它复制到自己的可写层,再修改成 changed。容器 B 仍读取 base。 容器 A · 自己的可写层 共享镜像层 · 只读 /note = base 原件始终保留 尚无 /note 副本 读:使用下层原件 写请求:/note ← changed先查找下层已有文件 /note = base副本已在 A 的可写层 复制文件(copy-up) /note = changedA 后续读取上层副本 B 读原件:base
  1. 创建 A:镜像层保持只读,A 获得自己的可写层。
  2. A 只读 /note:直接使用下层文件,不为这次读取复制它。先猜:A 要修改它时,可以直接覆盖镜像原件吗?
  3. A 请求写入:找到下层的 /note。为了保留原件,需要先准备副本。
  4. 把 /note 复制到 A 的可写层。这一步叫 copy-up;此时副本内容仍是 base。
  5. 把上层副本改成 changed。A 随后读到副本,底层原件仍是 base。先猜:刚从同一镜像启动的 B 会读到哪一份?
  6. B 仍读到 base。A 的副本、B 的视图和镜像原件彼此没有回写。
图 2 写时复制把修改留在 A 的可写层,B 继续使用未变的镜像原件。

新建文件直接写入本容器的可写层。再次修改已经复制上来的文件,继续改这个副本。这里讨论的是容器自身的文件系统;卷和绑定挂载是另一条存储路径,后面的存储模块再讲。

如果 A 执行 rm /note,下层的原件会被删掉吗?随后从同一镜像新建的 B 还能读到吗?

下层原件不会改变。A 的可写层记录删除,把下层路径从 A 的合并视图中隐藏;新建 B 仍能看到原件。OverlayFS 常用 whiteout(删除标记)表达这种隐藏。

Docker 29.x 不一定使用 overlay2

Docker Engine 29.0 起,全新安装默认使用 containerd 镜像存储;它由 snapshotter(快照管理后端)管理层。升级安装可能继续使用原来的存储驱动。

上图是 OverlayFS 的文件级 copy-up 模型;不同后端的复制粒度与实现可能不同。不要据此推断所有后端都按整文件复制,也不要假设本机层目录一定在 /var/lib/docker/overlay2。

从一个镜像扩展另一个

底层可共享

多个镜像可以引用同一组底层。图 1 的官方示例中,基础镜像含基础文件与 bash;应用镜像在它上面追加 COPY 和改权限的层,两个镜像仍共用前两层。

两个镜像引用同一组底层左侧基础镜像与右侧应用镜像都引用下方基础文件层和 bash 层;只有应用镜像还引用 COPY 和 chmod 两层。 基础镜像引用下方两层 应用镜像 chmod · 39B COPY · 222B 共同引用 安装 bash · 2.15MB 基础文件 · 5.6MB
图 3 共用的是同一组底层;应用镜像只追加自己的文件变化。

共享发生在内容相同、层链可复用的情况下。同一镜像存储中,无需为每个引用者再复制一份相同底层。 两个镜像的显示大小相加,会把共享部分重复计算。

这也不等于“两个容器共同修改一个目录”。镜像层只读;每个容器仍在自己的可写层记录变化。

用两句话解释:为什么两个镜像能省空间,却不会让容器 A 修改的 /note 自动出现在 B 里?

相同底层可以复用,镜像只需引用它们。运行时的修改写到各容器自己的可写层,不回写共享底层,所以 B 看不到 A 的修改。

  • 提到共享的是只读底层
  • 提到修改属于各容器自己的可写层
history 的大小为什么不等于磁盘实际占用?

层差异大小、下载的压缩内容、展开后的快照和实际磁盘占用是不同口径。containerd 镜像存储会同时保留压缩和解压形式;再加上共享、元数据等因素,不能把 history 各行直接加成宿主机用量。

本页因此只展示有来源的层大小,不给实验镜像编一个总大小。观察实际占用可用 docker system df -v,并按输出区分共享与独占部分。

在自己的 Docker 上验证

亲手验证

未实测

本编写环境没有 Docker。以下是可复制的实验和预期行为,不是实测终端记录。镜像 ID、层哈希、大小和版本输出留给维护者或你本机采集。需要 Linux 容器环境和可用的 Docker 守护进程。

先回顾第一次 docker run。下面会创建一个临时工作目录、拉取 Alpine 并构建两个本地镜像。--rm 在容器退出后删除容器及其可写层;--network none 禁用实验容器的外部网络。命令不挂载宿主目录、不发布端口、不修改宿主网络配置。

1. 做一个含 /note 的镜像

在宿主机的 Linux shell 中执行。FROM 选起点;RUN 写入文件;CMD 设置默认读取命令。-t 给镜像起本地名字;末尾 . 是包含 Dockerfile 的当前构建目录。

lab_dir=$(mktemp -d -t docker-layers-12.XXXXXX)
cd "$lab_dir"
cat > Dockerfile <<'DOCKERFILE'
FROM alpine:3.22
RUN printf 'base\n' > /note
CMD ["cat", "/note"]
DOCKERFILE
docker build -t learning-image-layers:base .
docker history --no-trunc learning-image-layers:base

预期:history 顶部有 CMD 配置记录,再往下有写入 /note 的 RUN。具体文本、时间、大小和基础镜像层数以你的平台为准。alpine:3.22 是标签,内容可能更新;在后续实验中都使用刚构建出的本地镜像。

2. 修改 A,再新建 B

先预测第二个容器会输出什么,再执行。镜像的 CMD 会读 /note;第一条命令额外指定 sh -c,让 shell 执行引号内的修改和读取。

docker run --rm --network none learning-image-layers:base \
  sh -c 'printf "changed\n" > /note; cat /note'
docker run --rm --network none learning-image-layers:base

预期输出,未实测:第一条打印 changed,第二条打印 base,各占一行。A 的修改没有进入镜像;B 使用镜像的原件。这个结果验证视图隔离,不能单凭输出测出存储后端的物理复制粒度。

3. 删除也只影响本容器

docker run --rm --network none learning-image-layers:base \
  sh -c 'rm /note; if [ ! -e /note ]; then printf "hidden\n"; fi'
docker run --rm --network none learning-image-layers:base

预期输出,未实测:依次打印 hidden 和 base。删除发生在第一个容器自己的视图里;第二个容器仍能读取镜像中的文件。

4. 扩展镜像,比较底层

下面用 base 作为起点,再增加 /extra。-f 选择另一份 Dockerfile。inspect 读取对象信息,--format 只输出指定字段;这里把根文件系统的层列表以 JSON 数组显示出来。

cat > Dockerfile.child <<'DOCKERFILE'
FROM learning-image-layers:base
RUN printf 'extra\n' > /extra
DOCKERFILE
docker build -f Dockerfile.child -t learning-image-layers:child .
docker image inspect --format '{{json .RootFS.Layers}}' learning-image-layers:base
docker image inspect --format '{{json .RootFS.Layers}}' learning-image-layers:child
docker history --no-trunc learning-image-layers:child

预期结构,未实测:child 的层数组以 base 的完整数组开头,并多出写入 /extra 的一层。这些 sha256:… 值是文件系统变化内容的标识,不是 history 的 IMAGE 列;相同前缀说明它们引用相同的底层链。这里不预填哈希值或大小。

你从 child 镜像启动 A,在 A 里把 /note 改为 changed。随后从 base 镜像启动 B。B 应读到什么?

底层是只读的;共享底层不会共享容器的可写层。

--rm 删除的是该容器及其可写层,镜像底层继续存在。

child 继承 base 的底层,但 A 的写入只改变 A 的副本。B 从 base 的只读层开始,所以仍读取 base。

实验结束后

下面只删除本实验的两个镜像标签与临时目录中的两份 Dockerfile。先回到原工作目录;若你在实验目录添加了其他文件,rmdir 会保留该目录并报非空。

docker image rm learning-image-layers:child learning-image-layers:base
rm -- Dockerfile Dockerfile.child
cd -
rmdir -- "$lab_dir"

拉取的 Alpine 镜像和构建缓存可能仍保留。本实验不执行全局清理。

来源与边界

核对于 2026-11-03,适用 Docker Engine 29.x。官方历史示例与本机预期实验已分别标注;本环境没有 Docker,未声称实测 Engine 29.1.3 或 Compose 2.40.3。

  1. Understanding the image layers:差异层、合并视图、镜像复用。
  2. Storage drivers:图 1、图 3 的指令与全部大小、history 节选、copy-up 与删除语义。原文中的旧版 image ls 表格没有作为 Docker 29 输出引用。
  3. docker image history:别名、字段、--no-trunc 与输出格式。
  4. Dockerfile reference:FROM、RUN、COPY、ADD、ENV、CMD。
  5. OverlayFS storage driver:文件级复制与删除标记。
  6. containerd image store:29.0 起新安装的默认后端、快照与磁盘占用口径。
  7. docker container run、docker image inspect:临时容器与层列表采集。

下一课再系统学习 Dockerfile 指令。本页不要求你预先掌握构建缓存、数据卷或 OverlayFS 的挂载参数。