Ubuntu容器说明
回顾一下,容器只不过是一个正在运行的进程,它还应用了一些附加的封装功能,以使其与主机和其他容器保持隔离。而镜像包含运行应用程序所需的所有内容——代码或二进制文件、运行时、依赖项以及所需的任何其他文件系统对象。
实际上在实际使用中,我们使用镜像创建容器,非常像使用一个新的系统镜像(这里的镜像包含了运行程序的所有内容)来安装系统(创建容器)
有镜像才能创建容器,这是根本前提
那么这些容器的关系是什么情况,如下图

推荐 Ubuntu,因为 CentOS 太大了
貌似现在你在 Ubuntu 装 docker,就会自带一个 Ubuntu 的镜像上的 Ubuntu 容器
Docker镜像加载原理
镜像基本
镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时、库、环境变量和配置文件,这个打包好的运行环境就是 image 镜像文件。只有通过这个镜像文件才能生成 Docker 的镜像实例。
以我们的pull为例,在下载的过程中我们可以看到docker的镜像好像是在一层一层的在下载

发现镜像是一层一层的在下载的,镜像是分层的,这与docker的镜像加载原理有关。
UnionFS 联合文件系统
Union文件系统是一种分层、轻量级并且高性能的文件系统,它的核心功能是将多个独立的文件系统(目录)“联合” 挂载到同一个目录下,形成一个逻辑上的单一文件系统。用户操作这个挂载点时,会感觉访问的是一个完整的文件系统,而实际数据可能分布在多个底层目录中。
Union文件系统是 Docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
而且它一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。
核心特性
- 分层存储:支持将多个目录(称为 “层”)按顺序叠加,每层可以是只读或可写的。
- 写时复制(Copy-on-Write,CoW):当修改只读层中的文件时,系统会先将该文件复制到可写层再进行修改,避免直接修改底层只读数据,节省空间并保证底层数据的安全性。
- 透明性:用户无需关心数据实际存储在哪个层,操作逻辑与单一文件系统一致。
我们怎么理解其中的工作原理呢
假设存在三个层,从上到下依次为:
- 可写层(Writable Layer)
- 只读层 1(Read-only Layer 1)
- 只读层 2(Read-only Layer 2)
当用户访问或修改文件时:
- 读取文件:系统从顶层开始向下查找,找到第一个包含该文件的层并返回内容。
- 修改文件:若文件在只读层,系统先将其复制到可写层,再在可写层中修改;若文件已在可写层,则直接修改。
- 删除文件:并非真正删除底层文件,而是在可写层中标记 “删除”,后续查找时会忽略底层的该文件。
Docker镜像分层的特点
Docker 通过 UnionFS(或其变种,如 AUFS、OverlayFS、Device Mapper 等)实现镜像层的联合挂载,使多个只读层和容器可写层看起来像一个完整的文件系统。
因为 Docker 镜像的设计直接依赖 UnionFS 的分层特性,其核心思想是将镜像拆分为多个只读层,通过复用和叠加实现高效存储与分发。
镜像分层的构成
基础层:通常是操作系统的基础镜像(如
ubuntu:20.04
),包含内核、系统工具等基础文件。中间层:通过
Dockerfile
指令(如RUN
、COPY
、ADD
)生成的层,每个指令对应一个只读层。例如:1
2
3FROM ubuntu:20.04 # 基础层
RUN apt-get update # 中间层1
COPY app.py /app/ # 中间层2顶层:镜像本身没有可写层,只有当容器启动时,Docker 才会在镜像的所有只读层之上添加一个可写层(容器层),容器内的所有修改(如新建、删除、修改文件)都只发生在这一层。
关键特性
- 只读性:镜像的所有层均为只读,确保镜像的一致性和可复用性。多个容器可以共享同一镜像的底层,节省磁盘空间。
- 层复用:不同镜像若包含相同的中间层(如都执行过
apt-get update
),Docker 会只存储一份该层,避免重复存储。 - 与容器的关系:容器启动时,Docker 在镜像层之上添加可写层,容器的修改仅保存在可写层。当容器删除时,可写层被清理,镜像层不受影响。
所有的 Docker镜像都起始于一个基础镜像层,进行修改或增加新的内容时,就会在当前的镜像层之上,创建新的镜像层。
假设有一个基于 ubuntu:20.04
的镜像,包含 3
个只读层:
- 基础层(ubuntu 系统文件)
- 层 1(
RUN apt-get install python3
) - 层 2(
COPY app.py /app/
)
- 当启动容器时,添加可写层(层 4)。
- 若容器内执行
echo "hello" >> /app/app.py
,系统会将层 2 中的app.py
复制到层 4,再修改层 4 中的文件。 - 若容器内删除
/app/app.py
,系统仅在层 4 中标记该文件为 “删除”,层 2 中的文件实际仍存在。
镜像加载原理和镜像结构
docker 的镜像实际上由一层一层的文件系统组成,上面说过了这种层级的文件系统就是UnionFS,这个系统决定了 Docker 镜像的分层特性。
bootfs(boot file system)
主要包含
bootloader
和 kernel
,bootloader
主要是引导加载 kernel
,Linux 刚启动时会加载
bootfs
文件系统,在 Docker
镜像的最底层是引导文件系统 bootfs 。这一层与我们典型的 Linux /
Unix 系统是一样的,包含 boot 加载器和内核。当 boot
加载完成之后,整个内核就都在内存中了,此时内存的使用权已由
bootfs
转交给内核,此时系统也会卸载
bootfs
。
rootfs(root file system)
,在 bootfs 之上,包含的就是典型
Linux 系统中的 /dev,/proc,/bin,/etc
等标准目录和文件。
rootfs 就是各种不同的操作系统发行版,比如 Ubuntu,Centos等等。
这两个文件系统是 linux 内核的核心,这也是为什么 docker 镜像加载 Ubuntu才不到80兆,就是因为只有linux的核心内容,只包含最基本的命令、工具和程序库。
而镜像分层最大的一个好处就是资源共享,这样就很好,大部分基础镜像做好了的内容,其他镜像也可以由相同的 base 镜像构建而来,docker 只需要在磁盘上保留一份 base 镜像,再在内存中保留一份镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享。
而 Docker 镜像是制度的,是因为 docker 镜像得到每一层都是只读的,叫只读层,而容器层是可写的,容器层会在容器启动的时候,以可写层的方式加载到镜像的顶部。容器层之下都叫镜像层。
Docker
镜像并非一个单一文件,而是由多个只读层(Layer)
和元数据(Metadata) 组成,存储在 Docker
的本地仓库(通常位于 /var/lib/docker/
目录下)。
镜像层(Layer)
- 每个镜像层对应
Dockerfile
中的一条指令(如RUN
、COPY
、ADD
等),是一组文件和目录的集合,以tar 包形式存储(解压后为具体文件)。 - 每层都有一个唯一的 SHA256 哈希值作为标识,例如
a1b2c3...
,存储路径通常为/var/lib/docker/overlay2/[哈希值]/diff/
(不同存储驱动路径略有差异)。 - 层与层之间通过父层指针关联,形成一个链式结构(如层 3 的父层是层 2,层 2 的父层是层 1,以此类推)。
元数据(Metadata)
- 镜像配置文件(Config):记录镜像的基础信息,如层的顺序、默认启动命令(
CMD
)、环境变量、入口点(ENTRYPOINT
)等,以 JSON 格式存储,路径通常为/var/lib/docker/image/overlay2/imagedb/content/sha256/[镜像ID]
。 - 镜像索引(Index):用于关联不同平台的镜像(如 amd64、arm64 架构),确保拉取时匹配当前系统架构。
- 层元数据:记录层的父层哈希、创建时间、大小等信息,帮助 Docker 识别层的依赖关系。
镜像加载的流程可以按照如下理解,当通过
docker run [镜像名]
启动容器时,Docker
会按以下步骤加载镜像并准备文件系统:
解析镜像元数据
- Docker 首先根据镜像名(或 ID)查找本地仓库中的镜像配置文件,确认镜像包含的所有层及其顺序(从基础层到顶层)。
- 检查所有层是否已本地存在:若缺失,会从远程仓库拉取(通过哈希值匹配,仅拉取缺失层)。
准备联合挂载的目录结构
- 确定 lowerdir:将镜像的所有层按顺序组合为 OverlayFS
的
lowerdir
(例如,基础层在最底部,后续层依次叠加)。 - 创建 upperdir 和
workdir:在容器专属目录下生成可写层(
upperdir
)和工作目录(workdir
),路径通常为/var/lib/docker/overlay2/[容器ID]/upper/
和/work/
。 - 创建
mergedir:生成联合挂载的入口目录(
mergedir
),路径通常为/var/lib/docker/overlay2/[容器ID]/merged/
,容器内的/
根目录即映射到此处。
- 确定 lowerdir:将镜像的所有层按顺序组合为 OverlayFS
的
执行联合挂载
通过 OverlayFS 的挂载命令(如
mount -t overlay overlay -o lowerdir=...,upperdir=...,workdir=... mergedir
)将lowerdir
(镜像层)、upperdir
(可写层)联合挂载到mergedir
。此时,容器通过
mergedir
访问的文件系统,是所有镜像层和可写层的逻辑合并,用户无需关心文件实际存储在哪个层。
处理文件操作(基于 CoW 机制)
容器运行时对文件的操作(读、写、删)会触发 OverlayFS 的写时复制机制:
读取文件:从
mergedir
访问文件时,OverlayFS 会从lowerdir
顶层开始向下查找,找到第一个包含该文件的层并返回内容。修改文件:若文件在
lowerdir
(镜像层)中,OverlayFS 会先将文件复制到upperdir
(可写层),再修改upperdir
中的副本,原镜像层文件不变。删除文件:若删除
lowerdir
中的文件,OverlayFS 会在upperdir
中创建一个特殊的 “删除标记”(whiteout 文件),后续查找时会忽略lowerdir
中的该文件,实际镜像层文件未被删除。
Docker 镜像加载的核心是通过联合文件系统(如 OverlayFS)将多个只读镜像层联合挂载为一个逻辑文件系统,并通过写时复制(CoW)机制确保容器修改仅作用于顶层可写层,不影响底层镜像。
Docker 镜像的命名与标签机制
在实际使用 Docker
镜像时,我们经常会看到类似ubuntu:20.04
、nginx:latest
这样的镜像标识,这其实包含了
Docker 镜像的命名规范与标签机制,这是镜像分发和版本管理的核心基础。
镜像的完整命名格式为[仓库地址/][用户名/][镜像名]:[标签]
,各部分含义如下:
- 仓库地址:默认指向 Docker
Hub(
docker.io
),若使用私有仓库需显式指定(如harbor.example.com/project/nginx
)。 - 用户名:用于区分不同用户 /
组织的同名镜像(如
library/ubuntu
中library
是 Docker 官方仓库的默认用户名)。 - 镜像名:标识镜像的核心名称(如
ubuntu
、nginx
)。 - 标签(Tag):用于区分同一镜像的不同版本,默认标签为
latest
(表示最新版本,但并非严格意义上的 “最新”,需由镜像维护者定义)。
标签的核心作用是版本管理,例如python:3.9
和python:3.10
分别对应
Python
的不同版本。避免过度依赖latest
标签,因为其指向的版本可能随时间变化,生产环境中应指定具体标签(如ubuntu:20.04
而非ubuntu:latest
)以保证环境一致性。
镜像存储驱动的差异
Docker 通过 UnionFS 实现分层存储,但在不同操作系统和内核版本中,实际使用的存储驱动存在差异。常见的存储驱动包括 Overlay2、AUFS、Device Mapper、Btrfs 等,其中Overlay2是目前 Docker 推荐的默认驱动(要求 Linux 内核 3.18+)。
各驱动的核心差异如下:
- Overlay2:基于 Linux 内核的 OverlayFS,仅需两层(lowerdir 和 upperdir)即可实现联合挂载,性能优异且空间占用低,是目前最广泛使用的驱动。
- AUFS:最早的 Docker 存储驱动之一,支持多层叠加,但在 Linux 内核中并非原生支持(需额外补丁),目前逐渐被 Overlay2 替代。
- Device Mapper:基于块设备的驱动,通过快照机制实现分层,适用于不支持 OverlayFS 的内核,但性能和空间效率略逊。
- Btrfs/ZFS:依赖底层文件系统的快照功能,支持高效的写时复制和压缩,适合对存储性能要求较高的场景。
存储驱动的选择会影响镜像层的存储方式和容器 IO 性能,实际部署时需根据内核版本、底层文件系统和业务需求选择(例如 Ubuntu 20.04 + 和 CentOS 7 + 均默认支持 Overlay2)。
镜像与容器的生命周期关联
镜像和容器的生命周期紧密关联但又相互独立,理解这种关系有助于更好地管理 Docker 资源:
镜像的不可变性:镜像一旦构建完成,其所有层均为只读,无法修改。任何对镜像内容的变更都需要通过
docker commit
(不推荐)或重新构建 Dockerfile 生成新镜像。容器对镜像的依赖:容器启动时必须依赖一个基础镜像,且会在镜像层之上添加可写层。当多个容器基于同一镜像启动时,它们会共享底层的镜像层,仅各自维护独立的可写层。例如:
1
2docker run -d --name c1 ubuntu:20.04 sleep 3600
docker run -d --name c2 ubuntu:20.04 sleep 3600上述两个容器共享
ubuntu:20.04
的所有镜像层,仅c1
和c2
的可写层各自独立,大幅节省磁盘空间。容器消亡对镜像的影响:删除容器(
docker rm
)只会清理其可写层,底层镜像层不受影响;只有当镜像没有被任何容器依赖且未被标记为 “dangling”(悬空镜像,通常是构建过程中遗留的中间层)时,才能通过docker rmi
删除镜像。
镜像体积优化实践
基于镜像的分层特性,我们可以通过以下方式优化镜像体积,提升分发效率:
合并冗余层:Dockerfile 中多条
RUN
指令会生成多个中间层,可通过&&
合并命令并清理缓存,减少层数量。例如:1
2
3
4
5
6
7
8# 优化前(生成2层)
RUN apt-get update
RUN apt-get install -y python3
# 优化后(生成1层,且清理apt缓存)
RUN apt-get update && \
apt-get install -y python3 && \
rm -rf /var/lib/apt/lists/*多阶段构建:通过
FROM ... AS
创建临时构建层,仅将最终产物复制到目标镜像,剥离构建依赖。例如 Go 应用构建:1
2
3
4
5
6
7
8
9
10# 构建阶段(临时镜像,包含编译器等工具)
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o app .
# 运行阶段(仅包含运行时依赖)
FROM alpine:3.18
COPY --from=builder /app/app /usr/local/bin/
CMD ["app"]最终镜像仅包含 Alpine 基础层和编译后的二进制文件,体积可从数百 MB 缩减至几 MB。
使用精简基础镜像:优先选择
alpine
(约 5MB)、slim
等精简版本替代完整版操作系统镜像(如ubuntu:20.04
约 72MB,ubuntu:20.04-slim
约 28MB)。
镜像的导出与导入
在无网络或私有环境中,可通过导出 / 导入镜像文件实现离线分发,这一过程与镜像的分层结构直接相关:
导出镜像(docker save):将镜像的所有层(tar 包)和元数据打包为一个 tar 文件,保留层的依赖关系。例如:
1
docker save -o ubuntu20.04.tar ubuntu:20.04
导出的
ubuntu20.04.tar
包含该镜像的所有只读层和配置文件,可拷贝至其他环境。导入镜像(docker load):从 tar 文件恢复镜像,Docker 会自动解析层结构并重建镜像元数据:
1
docker load -i ubuntu20.04.tar
注意:导出的是完整镜像(包含所有层),而docker export
用于导出容器的可写层(不包含镜像层),生成的是容器文件系统的快照,与镜像导出有本质区别。