在这一节,我们将深入探索 Docker 的核心概念之一:镜像(Image)。学完本部分,你将透彻理解什么是 Docker 镜像,如何对它进行基本操作,以及它背后神奇的工作原理。

你可以把 Docker 镜像想象成一个被打包好的“软件集装箱”。这个集装箱里装了一款应用运行所需的一切,包括应用本身的代码、依赖的各种库、一个迷你的操作系统环境,以及一些配置信息。 最关键的是,这个“集装箱”是只读的,一旦打包好,内部的东西就不会被意外改变。
如果你熟悉虚拟机(VMware),可以把镜像类比为虚拟机的“模板”。模板本身是静态的,但你可以用它快速创建出很多个一模一样的运行中的虚拟机。 同样,Docker 镜像本身也是一个静态的包,但你可以用这一个镜像,启动一个甚至成百上千个完全相同的“容器”(Container)。容器就是运行起来的镜像实例。
对于开发者来说,这个概念可能更好理解。镜像就像是面向对象编程里的“类”(Class),而容器则是根据这个类创建出来的“对象”(Object)。你可以用一个类,实例化出许多功能相同的对象。
那么,这些“集装箱”从哪里来呢?通常,我们从一个叫做“镜像仓库”(Registry)的地方获取它们。最著名的镜像仓库就是 Docker Hub,它像一个巨大的应用商店,里面有各式各样官方或社区打包好的镜像。
我们通过一个 pull(拉取)的动作,把这些镜像下载到自己的 Docker 环境中,然后就可以随时用它们来运行容器了。
简单来说,镜像是静态的、只读的软件包,是启动容器的“蓝图”或“模板”。容器则是动态的、运行中的实例。这是 Docker 中最基础也最重要的区别之一。
我们已经知道,镜像是静态的,容器是动态的。实际上,你甚至可以把一个正在运行的容器停掉,然后将它的当前状态打包成一个新的镜像。从这个角度看,我们通常认为镜像是“构建时”的产物,而容器是“运行时”的产物。
更有趣的是,Docker 镜像并非一个单一的文件,它更像一个“千层饼”或者“洋葱”,由许多层(Layers)堆叠而成。每一层都只包含一部分文件变更。 想象一下你正在制作一个披萨:
最终,你得到的这个完整的披萨,就是所有这些层叠加在一起的结果。Docker 镜像也是如此,它将所有层合并,对外呈现为一个完整、统一的文件系统。
这种分层结构带来了巨大的好处。比如,当多个镜像都基于同一个基础镜像(例如 Ubuntu 操作系统)构建时,它们可以共享这个相同的底层。这样一来,你的电脑上就不需要存储很多份重复的文件,大大节省了磁盘空间。 当你拉取一个新镜像时,如果 Docker 发现某些层本地已经存在了,它就只会下载那些你还没有的、新的层,从而极大地提高了下载速度。
这种机制被称为“写时复制”(Copy-on-Write)。当容器启动时,它在镜像的只读层之上,增加了一个可写的“容器层”。所有对容器的修改,比如新建文件、修改配置等,都发生在这个可写层,而不会影响到底层的只读镜像。这就保证了镜像本身的纯净和可复用性。
现在我们知道了镜像是什么,也了解了它的分层结构。那么,当我们要使用一个镜像时,该如何准确地找到并识别它呢?这就需要了解镜像的命名规则和存储方式。

我们存放和分发镜像的地方,叫做镜像仓库(Registry)。它是一个中心化的存储服务,你可以把它想象成代码界的 GitHub,只不过它存放的是打包好的镜像。 最常用、也是 Docker 默认的仓库就是 Docker Hub。此外,还有很多第三方的或者企业自己搭建的私有仓库,用来存放敏感或内部使用的镜像。
Docker Hub 上有官方仓库(Official Repositories)和非官方仓库之分。官方仓库里的镜像都经过了严格的审查和验证,由软件的开发者或 Docker 公司亲自维护,保证了其质量、安全性和时效性。
通常,这些官方镜像的名字都很简洁,比如 nginx、redis、mongo。
而非官方仓库则是由个人或普通组织创建的,就像 GitHub 上的个人项目一样,质量参差不齐。使用它们时需要多加小心,因为你无法保证它们是否安全、是否是最新版本。这些镜像的名称通常会带有用户名或组织名作为前缀,例如 nigelpoulton/tu-demo。
请始终对来源不明的镜像保持警惕!即使是官方镜像,也建议在使用前检查其文档和更新历史。安全第一!
一个完整的镜像名称,通常由两部分组成:仓库名(Repository)和标签(Tag),中间用冒号 : 隔开。
格式如下:<repository>:<tag>
ubuntu 或 nginx。如果是来自非官方仓库,还会包含用户名,如 nigelpoulton/tu-demo。18.04、latest。例如,mongo:4.2.24 指的是官方 mongo 仓库中,标签为 4.2.24 的那个镜像。
一个非常特殊的标签是 latest。如果你在拉取镜像时不指定标签,Docker 会默认使用 latest。
|# 拉取 alpine 仓库中标签为 latest 的镜像 $ docker pull alpine # 这等同于 $ docker pull alpine:latest
但请注意,latest 标签并没有任何魔法。它不一定代表“最新”的版本。它只是一个开发者可以任意设置的标签。
完全有可能一个仓库里,v2 镜像是最新的,但 latest 标签却指向了 v1 版本的镜像。因此,在生产环境中使用镜像时,最佳实践是明确指定一个具体的版本号标签,而不是依赖模糊的 latest。
标签是可变的,可能会导致混淆。为了更精确地锁定一个镜像,Docker 为每个镜像内容都生成了一个唯一的、不可变的哈希值,称为摘要(Digest)。这个摘要是根据镜像内容通过加密算法计算出来的,所以任何对镜像的微小改动都会导致摘要的改变。 通过摘要来拉取镜像,可以保证你获取到的是百分之百正确的、未经改动的镜像版本,这对于保证环境的一致性和安全性至关重要。
理论说了很多,现在让我们动手实践一下。下面是一些最常用的 Docker 镜像管理命令。
pull)docker pull 命令用于从镜像仓库下载镜像到本地。
|# 从 Docker Hub 拉取最新的 Redis 镜像 $ docker pull redis:latest latest: Pulling from library/redis b5d25b35c1db: Pull complete ... Status: Downloaded newer image for redis:latest docker.io/library/redis:latest
在输出中,你可以看到 Pull complete 的信息,每一行都代表正在下载镜像的一个“层”。
images)docker images 命令可以列出你本地已经下载的所有镜像。
|$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE redis latest 2334573cc576 2 weeks ago 111MB alpine latest 44dd6f223004 9 days ago 7.73MB
这个列表显示了镜像的仓库名、标签、唯一的镜像ID(IMAGE ID)、创建时间以及大小。
inspect)如果你想了解一个镜像的更多细节,比如它的所有分层信息、构建历史、环境变量等,可以使用 docker inspect 命令。它会返回一个详细的 JSON 格式的信息。
|# 查看 alpine:latest 镜像的详细信息 $ docker inspect alpine:latest [ { "Id": "sha256:44dd6f2230041eede4ee5e792728313e43921b3e46c1809399391535c0c0183b", "RepoTags": [ "alpine:latest" ], "RootFS": { "Type": "layers", "Layers": [ "sha256:94dd7d531fa5695c0c033dcb69f213c2b4c3b5a3ae6e497252ba88da87169c3f" ] }, ... } ]
在 RootFS.Layers 字段中,你可以看到组成这个镜像的所有层的哈希值。
search)如果你不确定想要找的镜像全名是什么,可以用 docker search 命令在 Docker Hub 上进行搜索。
|# 搜索名称中包含 "alpine" 的镜像 $ docker search alpine NAME DESCRIPTION STARS OFFICIAL AUTOMATED alpine A minimal Docker image based on Alpine... 9962 [OK] rancher/alpine-git 1 ...
这个命令会返回一个列表,其中 OFFICIAL 列标记了是否为官方镜像,STARS 则表示受欢迎程度,可以作为选择的参考。
rmi)当一个镜像不再需要时,你可以使用 docker rmi 命令(remove image)来删除它,以释放磁盘空间。
|# 通过镜像名和标签删除 $ docker rmi alpine:latest # 也可以通过镜像ID删除 $ docker rmi 44dd6f223004
在删除镜像之前,必须先停止并删除所有基于该镜像创建的容器。否则,Docker 会提示错误,阻止你删除一个仍被占用的镜像。
随着技术的发展,我们的软件需要运行在各种各样的硬件上,比如个人电脑的 x86 架构、服务器的 ARM 架构,还有 Windows 和 Linux 等不同的操作系统。如果每种环境都需要一个不同名字的镜像,那管理起来将是一场噩梦。
幸运的是,Docker 提供了多架构镜像(Multi-architecture images) 的支持。这意味着,一个像 golang:latest 这样的单一镜像标签,可以同时指向多个针对不同平台(操作系统 + CPU架构)构建的镜像版本。
当你执行 docker pull golang:latest 时,Docker 客户端会足够智能,它会与 Docker Hub “沟通”,根据你当前系统的平台信息(例如,你是在一台 ARM 架构的 Linux 机器上,还是一台 x64 架构的 Windows 机器上),自动拉取那个最适合你的镜像版本。
这个神奇的功能是通过一个叫做 manifest list 的清单文件实现的。这个清单列出了该镜像标签下所有可用的平台版本及其对应的具体镜像。Docker 正是靠它来完成“按需分配”的。
|# 在任何支持的平台上运行此命令 # Docker 会自动选择正确的镜像版本来执行 $ docker run --rm golang go version # 在 Linux on arm64 上的可能输出 go version go1.20.4 linux/arm64 # 在 Windows on amd64 上的可能输出 go version go1.20.4 windows/amd64
这个设计极大地简化了跨平台开发和部署的流程,让开发者可以专注于代码,而不用过多担心底层硬件的差异。
在本部分我们详细地学习了 Docker 镜像。我们知道了镜像是一个包含了应用运行所需一切的只读包,是启动容器的模板。我们还探讨了它底层的分层结构,这使得镜像的存储和分发变得高效。
我们学习了如何通过 docker pull 从 Docker Hub 获取镜像,如何通过命名和标签来识别它们,以及如何使用摘要来确保镜像的不可变性。我们还实践了 docker images, docker inspect, docker rmi 等一系列常用命令。
最后,我们了解了多架构镜像是如何帮助我们轻松应对跨平台挑战的。