从 Layer 到 Registry,理解镜像的构建、存储与分发
本文是对 Docker Image 机制的一次系统学习总结。
不只是介绍
docker images、docker pull这些命令,而是从一个实际问题出发,逐步理解 Docker Image 为什么存在、为什么要分层、Layer 为什么可以共享、Digest 和 Manifest 又分别解决什么问题,以及 Image 最终是如何被构建、存储和分发的。
一、先从一个问题开始:换一台 Linux,怎么运行同一个项目?
假设我们现在有一个 Go 项目。
在自己的电脑上运行没有问题。
但是如果换一台 Linux 服务器呢?
可能需要重新:
上传代码 ↓ 安装 Go ↓ 安装项目依赖 ↓ 修改配置 ↓ 启动项目如果服务器环境和开发环境又存在差异,就可能出现:
“在我电脑上明明可以运行,为什么换台机器就不行了?”
Docker 解决这个问题的核心思路之一,就是:
把运行应用所需要的内容打包起来。
而这个“打包后的东西”,就是我们今天要重点讨论的:
Docker Image
二、Docker Image 到底是什么?
可以先把 Image 理解成:
一个用于创建容器的模板,其中包含运行应用所需要的文件和配置。
简单来说:
应用程序 + 运行环境 + 依赖 + 配置 ↓ Docker Image例如一个 Go 应用,可以构建成一个 Image。
然后把这个 Image 拿到另一台 Linux:
Docker Image ↓ 另一台 Linux ↓ 运行应用这样就不需要每次都重新从头配置整个运行环境。
不过这里马上会产生一个问题:
Image 是模板,那真正运行起来的东西是什么?
这就要引出 Docker 中另外一个非常重要的概念:
Container
三、Image 和 Container 到底有什么区别?
两者可以简单理解为:
Image │ │ docker run ↓ Container1. Image:镜像
Image 可以理解成一个只读模板。
里面包含:
应用 依赖 文件系统 配置它本身并不是一个正在运行的程序。
2. Container:容器
Container 是基于 Image 创建出来的运行实例。
可以简单理解成:
Container = Image 内容 + Writable Layer + 运行时状态 / 配置所以:
Image 是“模板”,Container 是“运行起来的实例”。
例如:
docker run nginx可以理解成:
nginx Image ↓ 创建 ↓ nginx Container3. 一个 Image 可以创建多个 Container
例如:
nginx Image / | \ ↓ ↓ ↓ Container Container Container所以如果启动 10 个 nginx Container:
并不意味着需要准备 10 份完全独立的 nginx Image。
多个 Container 可以共享 Image 的只读内容。
这就产生了一个新的问题:
如果多个 Container 可以共享 Image,那么 Image 本身到底是怎么保存的?
四、Image 为什么要分层?
如果我们只修改其中一个文件:
修改一个配置文件 ↓ 整个 Image 都重新处理?显然会比较浪费。
所以 Docker 使用了:
Layer(镜像层)
一个 Image 不再简单理解成一个整体,而是由多个 Layer 组成:
五、Layer 到底是什么?
Layer 可以理解成:
一组文件系统变化。
例如:
Layer 1 → 基础文件 Layer 2 → 新增 / 修改文件 Layer 3 → 添加配置 Layer 4 → 添加应用文件这里需要特别注意:
Layer 本质上不是“应用层”“依赖层”“运行时层”。
这些只是为了方便理解而进行的示意。
更准确地说:
Layer ↓ 文件系统变化 ↓ 新增 / 修改 / 删除文件也就是说,每一层描述的是:
在原来的文件系统基础上,又发生了什么变化。
六、Layer 为什么可以共享?
既然 Image 是由 Layer 组成的,那么就会产生一个非常有意思的问题:
如果两个 Image 中存在相同的 Layer,需要保存两份吗?
例如:
两个 Image 都使用了:
Layer A1 Layer A2那么本地并不需要简单地理解成:
保存 A1 保存 A2 再保存一份 A1 再保存一份 A2而是可以对相同 Layer 进行复用。
这就是 Docker Image 分层非常重要的一个价值:
Layer 可以共享和复用
带来的好处包括:
减少本地存储
减少重复下载
提高镜像分发效率
支持 Docker Build Cache
例如:
那么:
Layer 1 Layer 2可以被两个 Image 共同使用。
但是问题又来了:
Docker 怎么知道两个 Layer 是不是相同的?
七、Digest:给内容一个“指纹”
Docker 使用 Digest 来帮助进行内容寻址。
可以简单理解成:
Layer 内容 ↓ SHA256 ↓ Digest例如:
Layer A ↓ sha256:abc123...另一个 Image 中如果存在相同内容:
Layer B ↓ sha256:abc123...那么就可以通过 Digest 判断它们对应的是相同内容。
简单理解:
内容相同 ↓ Digest 相同 ↓ 可以识别 / 复用如果内容发生变化:
原始内容 ↓ sha256:abc123 内容发生变化 ↓ sha256:def456Digest 也会发生变化。
因此可以把 Digest 理解成:
内容的“指纹”。
八、Tag 和 Digest 有什么区别?
我们平时经常看到:
nginx:latest这里的:
latest就是 Tag。
而另外一种引用形式是:
nginx@sha256:xxxx这里使用的是 Digest。
两者的核心区别可以简单理解为:
| Tag | Digest | |
|---|---|---|
| 本质 | 名字 / 引用 | 内容摘要 |
| 是否可以变化 | 可以 | 内容变化时改变 |
| 示例 | nginx:latest | sha256:xxxx |
例如:
nginx:latest今天可能指向 Image A:
latest → Image A以后 Registry 中的latest可能指向新的 Image B:
latest → Image B所以 Tag 更像:
“这个镜像现在叫什么。”
而 Digest 更接近:
“这个具体内容是谁。”
九、一个 Image 到底由哪些东西组成?
到这里,我们已经知道:
Image ↓ 很多 Layer同时又知道:
Layer ↓ Digest但是又出现了一个问题:
Docker 怎么知道一个 Image 到底应该使用哪些 Layer?
这时候就需要理解:
Manifest
可以把 Manifest 理解成:
Image 的“组成清单”。
例如:
Manifest 会描述 / 引用:
Config
Layer
Digest
Size
Media Type
因此:
Manifest 不是用来保存文件内容的,而是用来描述 Image 由什么组成。
十、现在回头看,一个 Image 到底是什么?
把前面的知识串起来
再展开:
所以可以形成一个比较完整的认识:
Tag / Digest ↓ 找到 Image ↓ Manifest ↓ 知道有哪些 Config + Layers ↓ Config + Layer 内容一句话总结:
引用找到镜像,Manifest 描述组成,Layers 保存文件系统内容。
十一、这些 Layer 是怎么来的?
前面解决了:
Image 是什么?
又解决了:
Image 为什么分 Layer?
接下来就要问:
这些 Layer 到底是谁创建出来的?
答案就是:
Dockerfile + Docker Build
基本过程:
Dockerfile ↓ docker build ↓ Layers + Config ↓ Manifest ↓ Image例如一个简单的 Go Dockerfile:
FROM golang:1.25 WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o app其中一些指令会产生文件系统变化。
例如:
RUN / COPY / ADD ↓ 文件系统变化 ↓ Layer而一些指令主要影响 Image 的配置或元数据,例如:
ENV CMD EXPOSE ↓ Config / Metadata因此可以建立一个非常重要的认识:
Dockerfile 描述构建过程,Build 最终产生 Image 所需要的 Layers 和 Config。
十二、为什么 Dockerfile 要分开写?
既然 Layer 可以复用,那么它不仅可以帮助节省存储。
还可以帮助:
Docker Build Cache
例如:
COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o app假设我们只是修改了业务代码:
go.mod 没变 ↓ 依赖没有变化 ↓ 之前的依赖相关结果可以复用但是:
源代码变化 ↓ COPY . . 发生变化 ↓ 后面的步骤需要重新执行所以:
Layer ↓ 缓存 ↓ 复用构建结果 ↓ 减少重复构建这也是为什么 Dockerfile 中经常会看到:
COPY go.mod go.sum ./ RUN go mod download COPY . .而不是一开始就:
COPY . . RUN go mod download前一种写法更有利于利用缓存。
十三、Multi-stage Build:构建环境和运行环境分开
再思考一个问题:
如果一个 Go 程序已经编译好了,运行的时候还需要 Go 编译器吗?
答案显然是不需要。
如果把所有东西都放进最终 Image:
最终 Image Go Compiler Source Code Dependencies Build Tools Application最终镜像可能包含很多运行时根本不需要的内容。
因此可以使用:
Multi-stage Build
基本思路:
Build Stage ┌──────────────────┐ │ Compiler │ │ Source Code │ │ Dependencies │ └────────┬─────────┘ │ Build ↓ Application Binary │ ↓ Runtime Stage ┌──────────────────┐ │ Minimal Runtime │ │ Application │ │ Binary │ └──────────────────┘也就是:
Build Stage 负责构建,Runtime Stage 负责运行。
这样可以:
减小最终 Image
不携带编译器
不携带源码
减少运行环境中的无关内容
十四、一个 Tag,为什么可以支持不同平台?
现在又有一个新的问题:
如果我的电脑是 amd64,另一台机器是 arm64,能不能使用同一个镜像 Tag?
这就涉及:
Multi-platform Image
例如:
nginx:latest ↓ Image Index ┌──┴──┐ ↓ ↓ amd64 arm64 ↓ ↓ Manifest Manifest ↓ ↓ Config Config +Layers +Layers当执行:
docker pull nginxDocker 会根据当前平台选择对应的镜像变体。
可以简单理解成:
当前平台 ↓ Image Index ↓ 选择对应 Manifest ↓ 下载对应 Config + Layers这里需要区分:
| Multi-stage | Multi-platform | |
|---|---|---|
| 解决 | 构建问题 | 平台问题 |
| 结构 | Build → Runtime | amd64 → arm64 |
| 关注 | 怎么构建 | 给谁运行 |
一句话:
Multi-stage 解决“怎么构建”,Multi-platform 解决“给谁运行”。
十五、Image 做好了,怎么把它带到另一台机器?
现在回到文章最开始的问题:
换一台 Linux,怎么运行同一个项目?
前面我们已经有了:
Dockerfile ↓ Build ↓ Image那么接下来就是:
怎么把 Image 从一台机器带到另一台机器?
这时候就需要:
Registry
可以把 Registry 理解成:
Docker Image 的存储和分发中心。
基本流程:
我的电脑 │ │ docker push ↓ Registry │ │ docker pull ↓ 另一台 Linux十六、Docker Image Reference 怎么看?
例如:
docker.io / library / nginx : latest可以拆成:
docker.io ↓ Registry library ↓ Namespace nginx ↓ Repository latest ↓ Tag所以:
Registry / Namespace / Repository / Tag例如:
Repository: nginx Tags: ├── latest ├── 1.29 └── 1.28这里要注意:
Repository 是
nginx,不同的 Tag 可以指向不同版本的镜像内容。
十七、Docker CLI 拉取规则
最常见:
docker pull nginx可以理解成默认使用:
docker.io/library/nginx:latest如果指定 Tag:
docker pull nginx:1.29表示拉取:
nginx └── Tag: 1.29如果指定 Digest:
docker pull nginx@sha256:xxxx则是根据确定的 Digest 指定内容。
因此可以记住三个简单规则:
不写 Registry → 使用默认 Registry 不写 Tag → 默认 latest 使用 @Digest → 指定确定内容十八、一次docker pull到底发生了什么?
执行:
docker pull nginx背后并不是简单地:
服务器 ↓ 把一个 Image 文件下载下来而是一个逐步解析和获取的过程。
可以简化为:
docker pull nginx ↓ 解析 Image Reference ↓ 请求 Registry ↓ 获取 Image Index / Manifest ↓ 选择当前平台 ↓ 找到 Config + Layers ↓ 检查本地 Layer ┌────┴────┐ 已存在 不存在 ↓ ↓ 复用 下载 └────┬────┘ ↓ Local Image这里非常重要的一点就是:
Pull 不一定会重新下载所有 Layer。
如果本地已经存在相同的 Layer:
Layer Digest ↓ 本地已经存在 ↓ 直接复用这就是前面讲的:
Layer 共享 + Digest 内容寻址
最终这些内容组成了本地 Image。
然后:
Local Image ↓ docker run ↓ Container十九、Image 用完了,怎么清理?
随着学习 Docker,我们很容易遇到:
docker images看到:
REPOSITORY TAG IMAGE ID nginx latest abc123 <none> <none> def456这时候可能会看到很多:
<none>但需要注意:
<none>不一定等于 Dangling Image。
可以专门查看 Dangling Image:
docker images -f dangling=true清理 Dangling Image
使用:
docker image prune默认主要清理:
Dangling Images
更彻底的清理
docker image prune -a它的范围更大:
删除没有被任何 Container 使用的 Image,包括有 Tag 但没有被使用的 Image。
所以:
docker image prune ↓ 范围较小 docker image prune -a ↓ 范围更大执行prune -a前要确认是否真的不再需要这些 Image。
二十、最后把整个 Image 机制串起来
学习到这里,可以把 Docker Image 的完整链路串起来:
Dockerfile ↓ Build ↓ Layers + Config ↓ Manifest ↓ Image ↓ push ↓ Registry ↓ pull ↓ Local Image ↓ docker run ↓ Container ↓ Writable Layer而 Image 内部又可以理解为:
Image Reference (Tag / Digest) ↓ Image Index(多平台时) ↓ Image Manifest ↓ ┌──────────────────┐ │ Config │ │ │ │ Layer 1 │ │ Layer 2 │ │ Layer 3 │ └──────────────────┘其中:
Image
用于创建 Container 的只读模板。
Layer
文件系统变化的集合,也是镜像共享和构建缓存的重要基础。
Digest
基于内容计算得到的摘要,可用于内容寻址和识别相同内容。
Manifest
描述 Image 所使用的 Config 和 Layers。
Registry
Image 的存储和分发中心。
Container
Image 创建出来的运行实例,并拥有自己的可写层。
二十一、总结
如果只记住这几个关键词,可以记成:
Image ↓ Layer ↓ 共享 ↓ Digest ↓ Manifest ↓ Build ↓ Registry ↓ Pull ↓ Container更完整地说:
Docker Image 并不是一个简单的大文件,而是由 Config 和多个 Layer 组成,并通过 Manifest 描述它们之间的关系。
Layer 带来了:
内容复用
镜像共享
存储优化
构建缓存
增量分发
Digest 则帮助 Docker 根据内容进行识别和寻址。
而 Registry 则负责让这些 Image 可以在不同机器之间进行存储和分发。
最终形成:
Dockerfile ↓ Build ↓ Layers + Config ↓ Manifest ↓ Image ↓ Registry ↓ Pull ↓ Local Image ↓ Container理解 Docker Image 的关键,就是理解“分层、内容寻址、Manifest 描述、Registry 分发”这四件事。