Docker 容器与镜像的区别及关联方式

Docker 容器与镜像的关系常被混淆。本文从定义、生命周期、存储结构等维度剖析二者区别,并展示如何通过构建、运行、提交等操作将镜像与容器关联起来,形成高效的开发部署闭环。

Docker 容器与镜像的区别及关联方式
封面图:ZuCDN · ZuCDN 原创

理解 Docker 容器与镜像的区别及关联方式,是掌握容器化技术的关键一步。很多人误以为容器就是镜像的另一个名字,或者镜像就是一个精简的虚拟机。实际上,镜像是一个只读的模板,而容器是它的运行实例。要判断一个对象是镜像还是容器,最简单的路径是:看它是否有可写层。镜像没有,容器有。这个判断方法几乎适用于所有场景。

镜像:不可变的只读模板

Docker 镜像由一系列只读层组成,每一层代表一个指令(如 RUN、COPY)产生的文件系统变更。这些层被堆叠起来,形成一个完整的文件系统视图。镜像本身是不可变的,你无法直接修改一个已存在的镜像层;任何修改都会产生新的层,从而形成一个新的镜像。这种不可变性保证了镜像的稳定性和可复现性。

镜像的生命周期从构建开始。通过 Dockerfile 定义构建步骤,使用 docker build 命令生成镜像。构建完成后,镜像可以被推送到仓库、拉取到其他主机,或者用于创建容器。镜像可以同时被多个容器使用,不会相互影响。

容器:镜像的运行实例与可写层

当你使用 docker run 从镜像启动一个容器时,Docker 会在镜像的只读层之上添加一个可写层(容器层)。所有对文件系统的写入操作(如创建文件、修改配置)都发生在这个可写层。容器因此可以被看作一个轻量级的、隔离的运行环境,它包含了应用程序及其依赖,但状态是可变的。

容器的生命周期与镜像不同:容器可以被启动、停止、删除,也可以被提交为新的镜像。容器的可写层是临时的,除非你显式地将其提交或挂载持久化存储,否则容器删除后,可写层中的数据也会丢失。这与容器的设计理念一致——容器应该是无状态的,状态应保存在外部卷或数据库中。

关联方式:构建、运行与提交

镜像与容器通过一系列操作紧密关联。最常见的关联链条是:编写 Dockerfile → 构建镜像 → 运行容器 → 修改容器 → 提交新镜像。每一步都在镜像和容器之间建立或转换关系。

从镜像到容器:docker run

docker run 是创建容器的主要方式。它根据指定的镜像启动一个容器,并在可写层中执行入口命令。你可以通过参数控制容器的网络、存储、资源限制等,这些配置不会改变镜像本身,只影响容器的运行行为。

从容器到镜像:docker commit

如果你对容器进行了修改(例如安装了新的软件包),并希望将这些修改保存下来以便复用,可以使用 docker commit 将容器的当前状态提交为一个新镜像。这个新镜像会包含原镜像的所有层,以及容器可写层的当前内容。但要注意,docker commit 生成的镜像缺乏可追溯性,不推荐用于生产环境;更好的做法是修改 Dockerfile 并重新构建。

镜像与容器的存储关联:层与可写层

在存储层面,镜像层是只读的,容器可写层是唯一的可写区域。当容器读取文件时,如果文件存在于镜像层,则直接读取;如果不存在或已被修改,则从可写层读取。这种机制称为写时复制(Copy-on-Write),它使得多个容器可以共享同一镜像的只读层,从而节省磁盘空间并加快启动速度。

判断路径:如何快速区分镜像与容器

在实际操作中,你可以通过以下路径快速判断:

  • 使用 docker images 列出的是镜像,使用 docker ps 列出的是容器。
  • 镜像名称通常带有标签(如 ubuntu:22.04),容器则有一个唯一的容器 ID 和名称。
  • 镜像没有运行状态,容器有运行、停止等状态。
  • 镜像不可变,容器可变。尝试修改一个镜像的文件系统(通过 docker run 进入容器)只会影响容器,不会影响镜像。

如果你仍然不确定,可以运行 docker inspect 查看对象的类型字段,其中 “Type”: “image” 表示镜像,”Type”: “container” 表示容器。

常见误区与失败条件

一个常见的误区是将容器当作持久化存储的载体。由于容器可写层的生命周期随容器结束而终结,一旦容器被删除,所有未提交的修改都会丢失。因此,生产环境中的关键数据应使用数据卷或绑定挂载,而不是依赖容器可写层。

另一个误区是认为 docker commit 可以替代 Dockerfile。实际上,docker commit 会丢失构建历史,使得镜像难以维护和审计。失败条件包括:忘记提交导致修改丢失、在容器中直接修改配置文件而未记录、以及将容器状态提交为镜像后无法复现构建过程。

此外,镜像的不可变性也意味着你不应该试图“修复”一个正在运行的容器中的问题,而应该修复 Dockerfile 并重新构建镜像,然后重新部署容器。这是容器化应用的最佳实践,也是 Kubernetes 等编排系统所依赖的原则——Pod 中的容器应该是不可变和可替换的。

实践建议:从镜像到容器的标准流程

为了充分利用镜像与容器的关系,建议遵循以下标准流程:

  1. 使用 Dockerfile 定义应用环境和启动命令,构建镜像。
  2. 使用 docker run 启动容器,并通过 -v 挂载数据卷实现持久化。
  3. 当需要更新应用时,修改 Dockerfile 并重新构建镜像,而不是直接在容器中更改。
  4. 使用 docker commit 仅在特殊场景下(如调试和快速原型)保存容器状态,但随后应将其固化为 Dockerfile 指令。
  5. 定期清理无用的镜像和容器,避免磁盘占用。

通过这种方式,你可以将镜像视为不可变的交付物,容器视为临时的执行环境,从而构建出更可靠、更易于扩展的容器化应用。

参考资料

延伸阅读