理解镜像层
解释
正如你在什么是镜像?中学到的那样,容器镜像由多层组成。每一层一旦创建,就是不可变的。但是,这究竟意味着什么?这些层是如何用来创建容器可以使用的文件系统的呢?
镜像层
镜像中的每一层都包含一组文件系统更改——添加、删除或修改。让我们来看一个理论上的镜像。
- 第一层添加基本命令和包管理器,例如 apt。
- 第二层安装 Python 运行时和 pip 用于依赖项管理。
- 第三层复制应用程序特定的 requirements.txt 文件。
- 第四层安装应用程序特定的依赖项。
- 第五层复制应用程序的实际源代码。
这个例子可能看起来像这样


这很有益,因为它允许在镜像之间重用层。例如,假设你想要创建另一个 Python 应用程序。由于分层,你可以利用相同的基础 Python。这将使构建速度更快,并减少分发镜像所需的存储空间和带宽。镜像分层可能类似于以下内容


层允许你通过重用其他人的基础层来扩展镜像,只添加你的应用程序所需的数据。
层叠
分层是由内容寻址存储和联合文件系统实现的。虽然这会变得技术性很强,但以下是它的工作原理:
- 下载每一层后,它都会解压缩到主机文件系统上的自身目录中。
- 当你从镜像运行容器时,会创建一个联合文件系统,其中各层叠加在一起,创建一个新的统一视图。
- 容器启动时,其根目录设置为该统一目录的位置,使用 `chroot`。
创建联合文件系统时,除了镜像层之外,还会为正在运行的容器创建一个目录。这允许容器进行文件系统更改,同时允许原始镜像层保持不变。这使你可以从相同的底层镜像运行多个容器。
试试看
在这个实践指南中,你将使用 docker container commit
命令手动创建新的镜像层。请注意,你很少会以这种方式创建镜像,因为你通常会使用 Dockerfile。但是,它使理解这一切如何工作更容易。
创建一个基础镜像
在第一步中,你将创建你自己的基础镜像,然后在接下来的步骤中使用它。
下载并安装 Docker Desktop。
在终端中,运行以下命令以启动一个新的容器
$ docker run --name=base-container -ti ubuntu
镜像下载完毕且容器启动后,你应该会看到一个新的 shell 提示符。这在你的容器内运行。它看起来类似于以下内容(容器 ID 会有所不同)
root@d8c5ca119fcd:/#
在容器内,运行以下命令安装 Node.js
$ apt update && apt install -y nodejs
此命令运行时,它会在容器内下载并安装 Node。在联合文件系统的上下文中,这些文件系统更改发生在容器独有的目录中。
运行以下命令验证是否已安装 Node
$ node -e 'console.log("Hello world!")'
然后你应该会看到控制台中出现“Hello world!”。
现在你已经安装了 Node,你就可以将你所做的更改保存为一个新的镜像层,你可以从中启动新的容器或构建新的镜像。为此,你将使用
docker container commit
命令。在一个新的终端中运行以下命令$ docker container commit -m "Add node" base-container node-base
使用 `docker image history` 命令查看镜像的层
$ docker image history node-base
你将看到类似以下内容的输出
IMAGE CREATED CREATED BY SIZE COMMENT d5c1fca2cdc4 10 seconds ago /bin/bash 126MB Add node 2b7cc08dcdbb 5 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B <missing> 5 weeks ago /bin/sh -c #(nop) ADD file:07cdbabf782942af0… 69.2MB <missing> 5 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B <missing> 5 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B <missing> 5 weeks ago /bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH 0B <missing> 5 weeks ago /bin/sh -c #(nop) ARG RELEASE 0B
注意顶行上的“添加 node”注释。此层包含你刚刚安装的 Node.js。
为了证明你的镜像已安装 Node,你可以使用这个新镜像启动一个新的容器
$ docker run node-base node -e "console.log('Hello again')"
这样,你应该会在终端中获得“Hello again”输出,显示 Node 已安装并正在运行。
现在你已经完成了基础镜像的创建,你可以删除该容器
$ docker rm -f base-container
基础镜像定义
基础镜像是构建其他镜像的基础。可以使用任何镜像作为基础镜像。但是,有些镜像是故意创建为构建块的,它们为应用程序提供了基础或起点。
在这个例子中,你可能不会部署这个 `node-base` 镜像,因为它实际上还没有做任何事情。但它是你可以用于其他构建的基础。
构建一个应用镜像
现在你有了基础镜像,你可以扩展该镜像以构建其他镜像。
使用新创建的 node-base 镜像启动一个新容器
$ docker run --name=app-container -ti node-base
在这个容器中,运行以下命令创建一个 Node 程序
$ echo 'console.log("Hello from an app")' > app.js
要运行此 Node 程序,可以使用以下命令,并看到屏幕上打印的消息
$ node app.js
在另一个终端中,运行以下命令将此容器的更改保存为一个新的镜像
$ docker container commit -c "CMD node app.js" -m "Add app" app-container sample-app
此命令不仅创建了一个名为 `sample-app` 的新镜像,还向镜像添加了其他配置,以设置启动容器时的默认命令。在本例中,你将其设置为自动运行 `node app.js`。
在容器外部的终端中,运行以下命令以查看更新后的层
$ docker image history sample-app
然后你将看到如下所示的输出。请注意,顶层注释为“添加应用”,下一层注释为“添加 node”
IMAGE CREATED CREATED BY SIZE COMMENT c1502e2ec875 About a minute ago /bin/bash 33B Add app 5310da79c50a 4 minutes ago /bin/bash 126MB Add node 2b7cc08dcdbb 5 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B <missing> 5 weeks ago /bin/sh -c #(nop) ADD file:07cdbabf782942af0… 69.2MB <missing> 5 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B <missing> 5 weeks ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B <missing> 5 weeks ago /bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH 0B <missing> 5 weeks ago /bin/sh -c #(nop) ARG RELEASE 0B
最后,使用全新的镜像启动一个新容器。由于你指定了默认命令,可以使用以下命令
$ docker run sample-app
你应该会看到你的问候语出现在终端中,来自你的 Node 程序。
现在你已经完成了容器的使用,可以使用以下命令删除它们
$ docker rm -f app-container
附加资源
如果你想更深入地了解你学到的东西,请查看以下资源
下一步
如前所述,大多数镜像构建不使用 `docker container commit`。相反,你将使用 Dockerfile,它会为你自动化这些步骤。