一、镜像概念与基础
什么是镜像
镜像,说白了就是一个「静态的、只读的文件包」。它打包了运行某个应用所需的一切——代码、运行时、系统库、环境变量、配置文件等等。
我经常跟新人这么解释:镜像就是应用的「快递包裹」。你收到包裹,拆开就能用,不用自己去买零件组装。镜像也一样,拉下来就能跑,不用操心依赖环境。
举个例子,一个 Nginx 镜像里有什么?
- 一个精简的 Linux 文件系统(比如 Alpine)
- Nginx 二进制文件
- 默认的配置文件
- 日志目录、PID 文件等运行时需要的结构
你运行这个镜像,它就变成一个 Nginx 容器。嗯,就这么简单。
镜像与容器的关系
很多人搞不清镜像和容器的区别。我习惯用一个类比:镜像 = 类,容器 = 实例。
你想想看,在面向对象编程里,类定义了属性和方法,但真正干活的是实例。镜像定义了文件系统和启动参数,但真正运行的是容器。
具体来说:
- 镜像:静态的、分层的、只读的模板
- 容器:动态的、可写的、运行中的实例
一个镜像可以启动多个容器,每个容器互不干扰。我在项目中经常这么用:同一个应用镜像,启动三个容器做负载均衡。它们共享镜像层,但各自有独立的可写层。
docker image ls 看镜像列表,用 docker ps 看运行中的容器。镜像占磁盘,容器占内存和 CPU。
镜像的层级结构(Layer)
这是 Docker 最巧妙的设计之一。镜像不是一个大文件,而是由多个只读层堆叠而成。
每一层对应 Dockerfile 里的一条指令。比如:
FROM ubuntu:22.04 # 第1层:基础层
RUN apt-get update # 第2层:更新包列表
RUN apt-get install -y nginx # 第3层:安装 Nginx
COPY index.html /var/www/ # 第4层:复制文件
CMD ["nginx", "-g", "daemon off;"] # 元数据,不算层
这个镜像就有 4 层。每一层都是只读的,并且带有唯一的哈希值。Docker 用这个哈希值做缓存——如果某层没变,直接复用。
我曾经踩过一个坑:Dockerfile 里把 COPY 写在了 RUN apt-get 前面。结果每次改代码,整个 apt-get 层都要重跑。后来我把不变的层放前面,变的放后面,构建速度快了 3 倍。
UnionFS(联合文件系统)原理
UnionFS 是镜像分层技术的底层支撑。它的核心思想是:把多个目录「联合」挂载成一个虚拟文件系统。
你想想看,镜像的每一层都是一个目录。UnionFS 把它们叠在一起,对外呈现为一个完整的文件系统。如果上层有文件,就覆盖下层的同名文件——这叫「写时复制」。
Docker 默认用的是 OverlayFS,它是 UnionFS 的一种现代实现。结构大概是这样的:
/var/lib/docker/overlay2/
├── l/ # 符号链接层
├── 层1哈希/ # 基础层
├── 层2哈希/ # 上层
├── 层3哈希/ # 再上层
└── merged/ # 联合挂载后的视图
当容器运行时,Docker 会在所有只读层之上再加一个可写层。容器里所有的写操作——创建文件、修改配置、写日志——都发生在这个可写层。
这就是为什么容器停止后,数据会丢失。可写层随着容器销毁而消失。我建议你把持久化数据挂载到宿主机目录或 volume 里,别指望容器自己的可写层。
核心逻辑图
下面这张 SVG 图展示了镜像分层和 UnionFS 的核心逻辑:
这张图左边是镜像的 4 个只读层,右边是容器运行时的视图——可写层在最上面,下面叠着镜像的所有只读层。UnionFS 把它们合并成一个完整的文件系统,容器里的进程根本感知不到分层的存在。
docker history 镜像名 可以查看镜像的每一层和对应指令。这是排查镜像体积问题的利器——哪层最大,一目了然。