← 全部笔记

Docker · 7 底层原理

镜像层只读,容器改动写在上层

OverlayFS 把只读层与可写层合成一个目录视图。容器经由这个视图读写,原镜像仍可供其他容器复用。

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

lowerdir、upperdir、workdir 和 merged 的关系 底部多个只读 lowerdir 与上方可写 upperdir 合成 merged。容器经 merged 读写。workdir 仅供内核工作,和 upperdir 在同一文件系统,不加入可见文件列表。 容器进程:读写 /app/config 经由视图读写 merged · 合并挂载点 同名普通文件:上层优先 提供改动 upperdir · 可写 每个容器各自一份 workdir 内核工作目录 辅助 upperdir 与 workdir 在同一文件系统 提供只读内容 lowerdir · 镜像只读层(自下而上)
图 1 merged 是合并视图;workdir 只辅助内核操作,不是又一层可见内容。[1][3]

视图 · 约 2 分钟

四目录各有分工

OverlayFS 是 Linux 内核提供的联合文件系统:把多个目录叠起来,呈现统一的文件路径。你已学过镜像与容器的关系;这里解释它们如何共享只读文件。[1]

“只读层”描述的是这个挂载怎样使用目录。并非宿主机上的每个底层目录都天然不可写。Docker 管理这些目录,你不应直接编辑它们。[1]

lowerdir 里有 /app/config = base,upperdir 里有同名文件 = custom。容器读到哪个?workdir 里偶然有同名文件会参与查找吗?

读到 upperdir 的 custom。workdir 不参与可见文件的合并。同名普通文件按层优先级选择,不会把两份文字拼接起来。

深入:lowerdir 列表怎么排序?目录也直接覆盖吗?

挂载参数形如 lowerdir=/L2:/L1:/L0 时,最左侧 /L2 优先级最高。这个写法仅示意,不在本页执行宿主机挂载。目录同名时一般合并子项,再对各子项应用优先级;后面提到的 opaque 标记会改变目录合并行为。[3]

图中只解释容器根文件系统。volume、bind mount、tmpfs 会在指定路径提供独立挂载;那里读写的内容不按此图进入容器 upperdir。[4]

写入 · 约 3 分钟

首次写入先复制

写时复制(copy-on-write,CoW)让未改动的文件继续共享。首次改写下层已有文件时,先把它复制到 upperdir,这一步叫 copy_up;再修改上层副本。后续写入直接修改这份副本。[4]

六步追踪同一个文件的读取和修改 下层 config 为 base。先读取 base,首次写触发 copy_up,上层副本改为 custom,再次写成为 final。同镜像新容器仍读取 base。每一行按步骤出现,最终保留全部因果链。 文件:/app/config · 示意值 共享 lowerdir:base 始终保留 1 只读:merged → lowerdir 读到 base;upperdir 无此文件 2 应用请求首次改写 目标仍在下层 → 需要上层副本 3 copy_up:lowerdir → upperdir 先复制 base;workdir 辅助操作 4 写上层副本:base → custom merged 现在读到 custom 5 再次写入:custom → final 直接写 upperdir,不再 copy_up 6 同镜像新容器:读取 base 它有自己的 upperdir,不见 final
  1. 只读文件时,上层没有就从下层读取,不需要为它创建上层副本。先猜:要把 base 改成 custom,会直接改下层吗?
  2. 应用通过 merged 请求写入。内核发现目标只在下层,需要准备上层文件。
  3. copy_up 把下层文件复制到上层;workdir 用于内核内部操作,下层内容保持不变。
  4. 在上层副本上写成 custom;合并视图优先展示上层文件。先猜:再次修改这个文件,还要从下层复制一次吗?
  5. 再次写入直接改上层副本,这里把 custom 改为 final。
  6. 新容器共享原镜像只读层,拥有独立可写层,所以仍能读到 base。
图 2 copy_up 只为尚未进入上层的文件准备副本;改动不会回写到镜像。[4]

普通文件内容写入的复制粒度是文件。只改大文件的一小部分,首次写入也可能带来整文件复制开销;新建文件则直接落在 upperdir。这里不展示虚构的耗时或大小测量。[1]

两个容器共享同一镜像。A 第一次修改镜像里的 /app/config,B 没写过它。哪一项成立?

共享下层保持只读。想想第一次写入前的 copy_up。

只读文件无需复制;workdir 也不承载容器的完整文件视图。

A 有自己的 upperdir,修改的是 copy_up 后的副本。B 的上层没有这个改动,所以继续从共享的 lowerdir 读取原内容。

删除 · 约 2 分钟

删除用标记遮住下层

删除只在下层存在的文件时,内核在上层记录删除标记(whiteout)。合并视图遇到它,就隐藏同路径的下层文件,也不展示标记本身。下层原文件仍然存在。[3]

删除前

merged · 应用所见/app/obsolete.txt 可见
upperdir没有此路径
lowerdir · 只读/app/obsolete.txt

视图向下查找,找到镜像文件。

在 A 中删除后

merged · 应用所见此路径不存在
upperdirwhiteout:遮住同路径
lowerdir · 只读/app/obsolete.txt 仍在

标记拦住查找;不会退回下层。

同镜像新容器 B

merged · 应用所见/app/obsolete.txt 可见
B 的 upperdir没有 A 的 whiteout
共享 lowerdir · 只读/app/obsolete.txt

A 的删除标记不属于 B。

图 3 whiteout 改变一个容器的可见结果,下层文件及其占用空间仍保留。

为什么删掉 lowerdir 文件后,不能只让 upperdir 保持“没有这个文件”?请用查找顺序解释。

如果上层只有“没有”,查找会继续到下层,原文件就又可见了。whiteout 明确记录“这个路径已删除”,让查找停下来。

  • 说出上层缺失时会继续查下层。
  • 说出 whiteout 隐藏下层同名文件,下层本体没变。
深入:whiteout 是 .wh.文件名吗?

要区分挂载中的表示和镜像归档中的表示。Linux OverlayFS 可用设备号 0/0 的字符设备,或带 trusted.overlay.whiteout 扩展属性的零长度普通文件表示 whiteout。扩展属性是附在文件上的额外元数据。它不是供应用读取的普通空文件。[3]

开放容器倡议(Open Container Initiative,OCI)的镜像规范,在层的 tar 归档中使用 .wh.<原文件名> 记录删除。这是分发格式,不能据此断言宿主机 upperdir 一定有同名 .wh. 文件。[8]

目录还有 opaque(不透明)标记,可阻止合并同名下层目录的子项。本页实验只删普通文件,不把目录删除的所有情况混在一起。[3]

辨识 · 约 2 分钟

先认后端再读字段

overlay2 是 Docker 经典存储驱动名;overlayfs snapshotter 是 containerd 使用的快照后端名。containerd 是 Docker 使用的容器运行时,快照后端负责准备和管理容器的文件系统层。二者都能使用 Linux OverlayFS,管理方式和暴露字段不同。[2]

截至 2026-11-03,Docker Engine 29.x 的新安装默认使用 containerd 镜像存储,默认 snapshotter 是 overlayfs。从旧版升级通常保留经典驱动,直到主动切换。不要单凭版本猜后端。[2]

docker version
docker info
docker info --format '{{ .DriverStatus }}'

在连接 Docker 的终端运行。记录 Server 版本、Storage Driver 和 DriverStatus。下图是字段判读指南,不是本机输出。官方 containerd 文档给出的 driver-type 值是 io.containerd.snapshotter.v1。[2]

经典 overlay2

  1. Storage Driver 为 overlay2。
  2. 查看容器 GraphDriver。
  3. Data 中识别 LowerDir、UpperDir、WorkDir、MergedDir。

containerd / overlayfs

  1. DriverStatus 的 driver-type 指向 containerd snapshotter。
  2. 先查看容器完整 inspect JSON。
  3. 新 API 查 Storage.RootFS.Snapshot;不假定有四目录路径。
图 4 相同的分层模型,不保证相同的 inspect 字段或宿主机路径。[2][7]

Engine 29 的容器 inspect 没有 GraphDriver,却有 Storage.RootFS.Snapshot。能据此认定容器没有可写层吗?你接下来查什么?

不能。先结合 docker info 判断存储后端。containerd 使用 snapshotter,Engine 29 在 API 1.52 及以上用 Storage 描述它;旧 API 兼容响应可能保留 GraphDriver 名称,却不提供经典驱动的路径。字段不同不能推翻分层机制。[7]

深入:这些路径属于哪台机器?

inspect 返回的是 Docker 服务端的内部信息。远程 context(连接配置)的路径属于远程主机;Docker Desktop 的 Linux 容器数据通常在它的 Linux 虚拟机内,不能直接套到 macOS 或 Windows 本地目录。[9]

若后端是 fuse-overlayfs、btrfs、zfs 或其他实现,只把本页当作 OverlayFS 模型;不要强行找四目录。本页不要求改 daemon 配置来凑出路径,切换后端还会影响已有镜像和容器的可见性。[2]

验证 · 约 5 分钟

实验对照原文件

未实测

编写环境没有 Docker。以下是待运行实验;预期现象按文件内容与官方机制推导,尚待维护者补充实测输出。没有展示虚构的版本、ID、大小或 inspect 路径。

在能访问 Docker Engine 的 Bash 等终端中执行,服务端须运行 Linux 容器。需要拉取 alpine:3.21;该标签不固定镜像摘要,实验不依赖其大小。实验会创建一个临时构建目录、本地镜像、构建缓存和一个无网络容器;不使用绑定挂载、端口发布或特权模式。需要本机 Docker 使用权限。

1. 准备可控的只读层

构建一个在 /opt/overlay-lab 放入两个普通文件的镜像。这个路径避开 Docker 单独挂载的 /etc/hosts 等文件。

overlay_lab_dir=$(mktemp -d)
cat > "$overlay_lab_dir/Dockerfile" <<'DOCKERFILE'
FROM alpine:3.21
RUN mkdir -p /opt/overlay-lab \
 && printf 'base\n' > /opt/overlay-lab/config.txt \
 && printf 'keep\n' > /opt/overlay-lab/obsolete.txt
DOCKERFILE
docker build -t overlayfs-lab:issue32 "$overlay_lab_dir"

预期镜像构建成功;输出随基础镜像和构建器变化,本页不预填构建日志。如果已有同名实验镜像或容器,请先换一个专用名称,并同步替换后面的命令。

2. 留一个容器供 inspect

docker run -d --network none --name overlayfs-lab-32 \
  overlayfs-lab:issue32 sleep 3600
docker inspect --type container overlayfs-lab-32

先按上一节记录 docker info。完整 JSON 是所有后端的观察入口。仅当确认为经典 overlay2 且存在 GraphDriver,再执行下面的格式化命令。[5]

docker inspect --type container \
  --format '{{json .GraphDriver}}' overlayfs-lab-32
经典 overlay2 的 GraphDriver.Data 字段判读(未实测;不预填路径)
字段对应关系你要核对的内容
LowerDirlowerdir通常是冒号分隔的目录列表,可能包含 Docker 准备的初始化层。
UpperDirupperdir当前容器的可写目录。
WorkDirworkdir内核工作目录,与上层在同一文件系统。
MergedDirmerged容器运行时的合并挂载点。

这些键名可在 Moby 的 overlay2 驱动源码核对。不要编辑返回路径下的文件。[10]

如果使用 containerd,跳过上面的 GraphDriver 命令。若完整 JSON 中有 Storage.RootFS.Snapshot,可执行:

docker inspect --type container \
  --format '{{json .Storage.RootFS.Snapshot}}' overlayfs-lab-32

它描述 snapshotter,不保证提供四个路径。若客户端协商的是旧 API,保留完整 JSON 和 docker info 作为记录即可,不必强行读取不存在的字段。[7]

经典 overlay2 的 JSON 里,哪个 Data 键对应内核工作目录?

GraphDriver.Data.

WorkDir 对应 workdir;MergedDir 对应合并挂载点。只在实际输出支持这组字段时使用它们。

3. 在同一容器里改写和删除

先预测:修改 config.txt 并删除 obsolete.txt 后,A 中能读到什么?从同一镜像启动 B 又能读到什么?

A 读到 custom,且 obsolete.txt 不可见;新容器 B 仍读到 base 和 keep。A 的上层副本和删除标记不会修改镜像。

docker exec overlayfs-lab-32 sh -ec '
  cat /opt/overlay-lab/config.txt
  printf "custom\n" > /opt/overlay-lab/config.txt
  rm /opt/overlay-lab/obsolete.txt
  cat /opt/overlay-lab/config.txt
  test ! -e /opt/overlay-lab/obsolete.txt && printf "hidden\n"
'
docker diff overlayfs-lab-32
docker run --rm --network none overlayfs-lab:issue32 sh -ec '
  cat /opt/overlay-lab/config.txt
  cat /opt/overlay-lab/obsolete.txt
'
  1. 预期,未实测:exec 中三次打印依次为 base、custom、hidden,每项一行。这是脚本的确定性期望,不是终端记录。
  2. 预期,未实测:diff 中 config.txt 应标为 C(修改),obsolete.txt 应标为 D(删除)。目录本身也可能有变化项,不假定行数或顺序。[6]
  3. 预期,未实测:新容器打印 base 和 keep,各一行,然后由 --rm 自动删除。

docker diff 展示容器文件系统相对镜像的变化。它不直接展示磁盘上的 whiteout 编码。这组实验验证“当前视图已删除、镜像内容仍在”;whiteout 的具体表示以内核文档为依据。[6][3]

4. 清理本次实验

docker rm -f overlayfs-lab-32
docker image rm overlayfs-lab:issue32
rm "$overlay_lab_dir/Dockerfile"
rmdir "$overlay_lab_dir"

仅移除上述专用容器、镜像标签和临时目录。拉取的基础镜像及构建缓存可能保留;本页不要求全局清理。即使实验中途停止,也可用这段命令收尾。

迁移 · 约 1 分钟

遮住不等于回收

如果在后续 Dockerfile 层删除早先层的大文件,后续视图会隐藏它,早先层仍携带这些数据。whiteout 不能让下层文件占用的空间消失。避免把只在构建期间使用的大文件留进较早的最终镜像层。[4][8]

镜像较早层包含大文件 /archive.bin,后续层删除了它。最终容器看不到该文件,能据此认定早先层占用的空间已回收吗?

共享下层不会被后续层的 rm 改写;变化由上面的层记录。

workdir 不是回收站。whiteout 改变可见性,不会搬走下层文件。

删除标记让合并视图跳过下层路径,但不会改写已存在的镜像层。要减少这类数据,需改变构建方式,使临时文件不留在最终镜像的早期层;判断实际占用还要测量,不能只看容器内 ls。

来源

核对日期:2026-11-03。Docker 行为以官方文档为准;GraphDriver 的兼容细节另核对 Engine 29.1.3 对应的 Moby 源码,不代表本页在该版本实测过。

  1. Docker:OverlayFS storage driver,四目录、读写、copy_up 和 whiteout;该文档讲经典 overlay2。
  2. Docker:containerd image store,29.x 新安装默认值、升级行为、DriverStatus 与默认 snapshotter。
  3. Linux 内核:Overlay Filesystem,Directories、whiteouts and opaque directories、Multiple lower layers。
  4. Docker:Storage drivers,镜像共享、容器可写层、copy-on-write 与数据卷边界。
  5. Docker CLI:docker inspect,完整 JSON 与 --format。
  6. Docker CLI:docker container diff,A/C/D 的含义。
  7. Moby 29.1.3:容器 inspect 生成及API 兼容转换,snapshotter 使用 Storage,API < 1.52 恢复兼容 GraphDriver。
  8. OCI Image Spec v1.1.1:Whiteouts,镜像 tar 层中的 .wh. 格式。
  9. Docker Desktop for Mac FAQ,容器与镜像数据位于虚拟机的磁盘映像。
  10. Moby 29.1.3:overlay2 GetMetadata,LowerDir、UpperDir、WorkDir、MergedDir 的字段来源。