镜像(Image)vs 容器(Container):只读层 vs 可写层
在 Docker 的体系中,“镜像”与“容器”是两个最基本、也最容易被混淆的概念。许多人认为容器是镜像的“运行实例”,这种说法没错,但过于笼统。要真正理解两者的关系,我们必须深入文件系统的结构,看清 Docker 如何通过分层机制实现高效、可复用的容器化运行时。
核心结论只有一句:
镜像是由多个只读层组成的静态文件系统快照;容器是在这些只读层之上,叠加一个可写层的运行时实体。
一、镜像:分层的只读快照
Docker 镜像不是单个文件,而是由一系列只读层(read-only layers) 构成的堆栈。每一层对应 Dockerfile 中的一条指令,并且遵循“内容寻址”原则——每一层的 ID 由其内容的哈希值决定,内容不变,层就不变。
例如,一个典型的 Node.js 应用镜像可能包含以下层次:
FROM node:18-alpine
→ 基础镜像层,包含 Alpine Linux 和 Node.js 运行时。COPY package.json .
→ 新增一层,仅包含package.json文件。RUN npm ci
→ 执行安装命令,生成node_modules目录,形成新的一层。COPY . .
→ 将应用代码复制进来,形成最终的应用层。
这些层通过联合文件系统(如 overlay2)自底向上叠加,形成一个统一的文件系统视图。由于每一层都是只读的,它们可以被多个镜像或容器安全地共享。
例如,所有基于 node:18-alpine 的镜像,都可以共享同一个基础层,无需重复存储和下载。
二、容器:在只读层之上添加可写层
当你执行 docker run 时,Docker 并不会修改镜像本身。相反,它会在镜像的所有只读层之上,创建一个全新的可写层(writable layer),也称为“容器层(container layer)”。
这个可写层是容器独有的,它的生命周期与容器绑定:容器启动时创建,容器删除时销毁。
所有在容器运行时发生的文件变更——例如:
- 写入日志文件:
echo "request received" >> /var/log/app.log - 创建临时文件:
touch /tmp/session.txt - 修改配置:
sed -i 's/debug=false/debug=true/' config.ini
——都会被记录在这个可写层中。底层的镜像层保持不变。
这种设计带来了几个关键优势:
- 轻量启动:不需要复制整个镜像,只需创建一个空的可写层。
- 资源复用:多个容器可以共享同一个镜像的只读层,极大节省磁盘空间。
- 一致性保障:镜像一旦构建完成,就不可变,确保每次运行的环境一致。
三、联合文件系统(UnionFS)如何工作
Docker 默认使用的存储驱动(如 overlay2)实现了联合挂载(union mount),它将多个文件系统层合并为一个逻辑视图。
以 overlay2 为例,它通过三个目录实现:
lowerdir:指向镜像的只读层(可以是多层)。upperdir:容器的可写层,记录所有修改。merged:对外暴露的统一视图,进程看到的根文件系统。
当容器内进程读取一个文件时:
- 如果文件位于只读层,直接从
lowerdir返回。 - 如果文件在可写层被修改过,返回
upperdir中的副本。 - 如果文件被删除,
upperdir会记录一个“whiteout”文件,表示该文件已被移除。
当进程写入一个文件时:
- 如果文件原本在只读层,Docker 采用“写时复制”(Copy-on-Write, CoW)机制:先将文件从只读层复制到可写层,再进行修改。
- 如果是新文件,则直接在可写层创建。
这种机制确保了只读层的完整性,同时实现了运行时的灵活性。
四、docker run 到底发生了什么?
执行 docker run myapp:latest 时,Docker 引擎会完成以下关键步骤:
- 解析镜像:加载
myapp:latest的所有只读层,确定其分层结构。 - 创建容器元数据:生成容器 ID、配置网络、挂载卷、设置环境变量。
- 创建可写层:在存储驱动(如
overlay2)中为该容器创建upperdir。 - 联合挂载文件系统:将镜像的只读层(
lowerdir)与容器的可写层(upperdir)合并,形成merged视图。 - 启动进程:在独立的命名空间和 cgroup 中,运行镜像中定义的命令(如
npm start)。
至此,容器启动完成。你看到的文件系统,是镜像层与可写层的联合视图;你运行的进程,被限制在特定的资源和网络环境中。
五、理解分层结构的实践意义
1. 为什么修改容器内的文件,退出后还能看到?
因为修改发生在可写层,只要容器未被删除,可写层就一直存在。
2. 为什么 docker rm 后数据丢失?
docker rm 会删除容器及其可写层。如果未使用 volume 或 bind mount,所有运行时产生的数据将永久丢失。
3. 为什么镜像构建要遵循“高变更频率在后”原则?
因为 Docker 的层缓存机制是“一旦某一层变化,其后的所有层都失效”。所以应将不常变的内容(如基础依赖)放在前面,将经常变化的应用代码放在最后。
4. 如何将容器的修改保存为新镜像?
使用 docker commit 命令,它会将容器的可写层“固化”为一个新的只读镜像层。
docker commit <container-id> myapp:modified但这种做法违背了不可变基础设施原则,推荐通过 Dockerfile 重新构建。
六、总结
- 镜像 = 只读层的堆栈,是静态的、可复用的文件系统快照。
- 容器 = 镜像 + 可写层,是动态的、可修改的运行时实例。
docker run的本质,是启动一个进程,并为其挂载一个由只读镜像层和独立可写层组成的联合文件系统。
理解这一分层模型,是掌握 Docker 构建优化、数据管理、镜像瘦身和 CI/CD 自动化的前提。当你开始思考“哪一层应该缓存”“哪些文件不该进入镜像”“如何最小化可写层开销”时,你已经超越了“会用 Docker”的层面,真正进入了“掌控 Docker”的领域。