整体说明
- TLDR:镜像的本质是静态的只读模板
- 镜像(Image)是一个静态的、只读的二进制文件集合 ,包含运行应用所需的代码、 runtime、库、环境变量、配置文件等所有依赖
- 镜像的核心作用是:
- 作为容器的”模板”:容器是镜像的运行实例(镜像 + 可写层)
- 保证环境一致性:无论在哪个宿主机器上,基于同一镜像创建的容器都能运行相同的应用(”一次构建,到处运行”)
- 镜像的核心特性包括
- 只读性 :所有层不可修改,保证安全性和可复用性
- 分层存储 :基于 UnionFS,层可共享,减少冗余
- 轻量高效 :仅包含应用依赖,体积远小于虚拟机镜像
- 可移植性 :镜像内容与宿主环境无关,实现”一次构建,到处运行”
镜像的分层结构:UnionFS 与 Copy-on-Write
- Docker 镜像最核心的设计是分层存储 ,基于 Union File System(联合文件系统) 实现
- 这种分层结构让镜像具备了”可复用、轻量、高效”的特性
分层的本质
- 每个镜像由多个只读层(Layer) 叠加而成,每层对应镜像构建过程中的一个操作(如
RUN、COPY等 Dockerfile 指令) - 层与层之间通过哈希值唯一标识(如
sha256:a1b2c3...),相同的层会被不同镜像共享(避免重复存储) - 例如,一个
nginx镜像可能包含以下层:- 基础层:
ubuntu:20.04的底层文件系统(如/bin、/etc等) - 依赖层:安装
nginx所需的库(如libpcre3等) - 应用层:
nginx二进制文件和配置文件(如/usr/sbin/nginx、/etc/nginx/)
- 基础层:
联合挂载(Union Mount)
- 当镜像被用于创建容器时,Docker 会将所有只读层联合挂载为一个统一的文件系统,对容器来说,这些层看起来是一个完整的目录(透明化分层细节)
Copy-on-Write(写时复制)机制
- Copy-on-Write(写时复制)机制是镜像分层与容器交互的核心机制
- 镜像的所有层都是只读的,容器启动时,Docker 会在镜像顶层添加一个可写层(Writable Layer)
- 当容器需要修改文件时:
- 若文件在底层(镜像层),会先将文件复制到可写层 ,再修改可写层的副本(底层文件不变)
- 若文件是新创建的,直接写入可写层
- 这种机制保证了:
- 镜像层不会被容器修改(只读),可安全复用
- 容器的修改仅保存在自己的可写层,不影响其他容器或镜像
Dockerfile 构建镜像与分层
镜像的构建通常通过
Dockerfile定义(而非docker commit,后者不推荐)Dockerfile中的每一条指令都会生成一个新的只读层 ,指令与层的对应关系是理解镜像体积和优化的关键下面是一个 Dockerfile 与分层对应的示例:
1
2
3
4
5
6FROM ubuntu:20.04 # 基础层(复用 ubuntu:20.04 的所有层)
RUN apt-get update # 层 1:执行 update 后的文件变化
RUN apt-get install -y nginx # 层 2:安装 nginx 后的变化
COPY nginx.conf /etc/nginx/ # 层 3:复制配置文件的变化
EXPOSE 80 # 元数据(不生成层,仅记录信息)
CMD ["nginx", "-g", "daemon off;"] # 元数据(容器启动命令)EXPOSE、CMD、ENV等指令仅修改镜像的元数据(保存在镜像的配置层),不生成新的文件层多条指令会生成多个层,层越多,镜像体积可能越大(需优化)
附录:镜像 ID 与 Digest的区别
- 镜像 ID是镜像的唯一标识符(64 位哈希,通常显示前 12 位),由镜像的所有层和元数据共同计算得出
f9c8f87e172b(完整 ID 为f9c8f87e172b2a4e41a73e2d685c8f...)
- Digest(摘要) :镜像内容的哈希值(基于所有层的内容计算),用于验证镜像的完整性(避免篡改)
- 拉去影响完成时,给出的
Digest: sha256:abc123...就是摘要,相同摘要的镜像内容一定相同
- 拉去影响完成时,给出的
附录:Storage Driver
- Docker 通过存储驱动管理镜像层和容器可写层的存储与联合挂载
- 不同的存储驱动实现方式不同,主流的是 overlay2(Linux 推荐,性能最优)
- overlay2 驱动的核心结构如下:
- 镜像层 :存储在
/var/lib/docker/overlay2/下,每层对应一个目录(以层哈希命名) - 可写层 :容器启动时,overlay2 会创建一个新目录作为可写层,并通过”上下层”关系关联镜像层
- 合并视图 :通过
overlay2的联合挂载,将所有层合并为容器内看到的统一文件系统
- 镜像层 :存储在
附录:镜像的体积优化
镜像体积过大会导致存储、传输和启动效率下降,优化核心是减少层数、删除冗余文件 :
合并指令(减少层数) :多条
RUN指令可合并为一条(用&&连接),并清理缓存(如apt-get clean):1
2
3
4
5
6
7
8优化前(2 层)
RUN apt-get update
RUN apt-get install -y nginx
优化后(1 层,且清理缓存)
RUN apt-get update && \
apt-get install -y nginx && \
rm -rf /var/lib/apt/lists/* # 删除 apt 缓存多阶段构建(丢弃无用层) :用于编译型应用(如 Go、Java),仅保留运行时所需文件(下面的代码最终镜像仅包含 alpine 基础层 + 二进制文件,体积大幅减小):
1
2
3
4
5
6
7
8
9
10阶段 1:编译(包含编译器等冗余工具)
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .
阶段 2:运行(仅保留编译产物)
FROM alpine:3.18
COPY --from=builder /app/myapp /myapp # 仅复制编译好的二进制文件
CMD ["/myapp"]优先选择
alpine(几 MB)、slim版本,而非完整版(如ubuntu完整版约 200MB,alpine约 5MB)删除临时文件、日志、包管理缓存(如
yum clean all、npm cache clean)
附录:执行 docker pull 时在发生什么?
- 当执行
docker pull [镜像名]时,终端会显示的多行输出,这是 Docker 拉取镜像过程的详细日志,每一行对应镜像的一个 层(Layer) 的下载或处理状态,包含层的信息 - 如果本地已经有的层,不会再下载,且不同镜像是可以共享同一个层的(通过 ID 唯一识别)
- 层 ID :每个层的唯一标识符(如
a1b2c3d4...) - 操作类型 :
Pull complete:该层已成功下载并解压Already exists:本地已存在该层,无需重复下载Downloading:正在下载该层,会显示进度(如50% [=====>])Verifying Checksum:验证文件完整性Extracting:解压下载的层文件
- 为什么会有这么多层?
- Docker 镜像是由多个 只读层(Layer) 叠加而成的
- 每个层对应镜像构建过程中的一个操作(如
RUN、COPY等指令) - 层具有 可复用性 :不同镜像可能共享相同的层,避免重复存储和下载
- 层的设计让镜像更新更高效(只需更新变化的层)
附录:镜像与容器的关系:动态 vs 静态
- 镜像 :静态、只读、多分层,是容器的”模板”
- 容器 :动态、可写,是镜像的”运行实例”(= 镜像所有只读层 + 容器独有的可写层)
- 可以理解为:容器 = 镜像(只读层) + 可写层(容器私有) + 容器元数据(如网络配置、环境变量等)
- 当容器被删除时,其可写层和元数据会被清理 ,但镜像的只读层不受影响(可继续用于创建新容器)