理解镜像层

解释

正如你在什么是镜像?中学到的那样,容器镜像由多层组成。每一层一旦创建,就是不可变的。但是,这究竟意味着什么?这些层是如何用来创建容器可以使用的文件系统的呢?

镜像层

镜像中的每一层都包含一组文件系统更改——添加、删除或修改。让我们来看一个理论上的镜像。

  1. 第一层添加基本命令和包管理器,例如 apt。
  2. 第二层安装 Python 运行时和 pip 用于依赖项管理。
  3. 第三层复制应用程序特定的 requirements.txt 文件。
  4. 第四层安装应用程序特定的依赖项。
  5. 第五层复制应用程序的实际源代码。

这个例子可能看起来像这样

screenshot of the flowchart showing the concept of the image layers

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

screenshot of the flowchart showing the benefits of the image layering

层允许你通过重用其他人的基础层来扩展镜像,只添加你的应用程序所需的数据。

层叠

分层是由内容寻址存储和联合文件系统实现的。虽然这会变得技术性很强,但以下是它的工作原理:

  1. 下载每一层后,它都会解压缩到主机文件系统上的自身目录中。
  2. 当你从镜像运行容器时,会创建一个联合文件系统,其中各层叠加在一起,创建一个新的统一视图。
  3. 容器启动时,其根目录设置为该统一目录的位置,使用 `chroot`。

创建联合文件系统时,除了镜像层之外,还会为正在运行的容器创建一个目录。这允许容器进行文件系统更改,同时允许原始镜像层保持不变。这使你可以从相同的底层镜像运行多个容器。

试试看

在这个实践指南中,你将使用 docker container commit 命令手动创建新的镜像层。请注意,你很少会以这种方式创建镜像,因为你通常会使用 Dockerfile。但是,它使理解这一切如何工作更容易。

创建一个基础镜像

在第一步中,你将创建你自己的基础镜像,然后在接下来的步骤中使用它。

  1. 下载并安装 Docker Desktop。

  2. 在终端中,运行以下命令以启动一个新的容器

    $ docker run --name=base-container -ti ubuntu
    

    镜像下载完毕且容器启动后,你应该会看到一个新的 shell 提示符。这在你的容器内运行。它看起来类似于以下内容(容器 ID 会有所不同)

    root@d8c5ca119fcd:/#
    
  3. 在容器内,运行以下命令安装 Node.js

    $ apt update && apt install -y nodejs
    

    此命令运行时,它会在容器内下载并安装 Node。在联合文件系统的上下文中,这些文件系统更改发生在容器独有的目录中。

  4. 运行以下命令验证是否已安装 Node

    $ node -e 'console.log("Hello world!")'
    

    然后你应该会看到控制台中出现“Hello world!”。

  5. 现在你已经安装了 Node,你就可以将你所做的更改保存为一个新的镜像层,你可以从中启动新的容器或构建新的镜像。为此,你将使用 docker container commit 命令。在一个新的终端中运行以下命令

    $ docker container commit -m "Add node" base-container node-base
    
  6. 使用 `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。

  7. 为了证明你的镜像已安装 Node,你可以使用这个新镜像启动一个新的容器

    $ docker run node-base node -e "console.log('Hello again')"
    

    这样,你应该会在终端中获得“Hello again”输出,显示 Node 已安装并正在运行。

  8. 现在你已经完成了基础镜像的创建,你可以删除该容器

    $ docker rm -f base-container
    

基础镜像定义

基础镜像是构建其他镜像的基础。可以使用任何镜像作为基础镜像。但是,有些镜像是故意创建为构建块的,它们为应用程序提供了基础或起点。

在这个例子中,你可能不会部署这个 `node-base` 镜像,因为它实际上还没有做任何事情。但它是你可以用于其他构建的基础。

构建一个应用镜像

现在你有了基础镜像,你可以扩展该镜像以构建其他镜像。

  1. 使用新创建的 node-base 镜像启动一个新容器

    $ docker run --name=app-container -ti node-base
    
  2. 在这个容器中,运行以下命令创建一个 Node 程序

    $ echo 'console.log("Hello from an app")' > app.js
    

    要运行此 Node 程序,可以使用以下命令,并看到屏幕上打印的消息

    $ node app.js
    
  3. 在另一个终端中,运行以下命令将此容器的更改保存为一个新的镜像

    $ docker container commit -c "CMD node app.js" -m "Add app" app-container sample-app
    

    此命令不仅创建了一个名为 `sample-app` 的新镜像,还向镜像添加了其他配置,以设置启动容器时的默认命令。在本例中,你将其设置为自动运行 `node app.js`。

  4. 在容器外部的终端中,运行以下命令以查看更新后的层

    $ 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
    
  5. 最后,使用全新的镜像启动一个新容器。由于你指定了默认命令,可以使用以下命令

    $ docker run sample-app
    

    你应该会看到你的问候语出现在终端中,来自你的 Node 程序。

  6. 现在你已经完成了容器的使用,可以使用以下命令删除它们

    $ docker rm -f app-container
    

附加资源

如果你想更深入地了解你学到的东西,请查看以下资源

下一步

如前所述,大多数镜像构建不使用 `docker container commit`。相反,你将使用 Dockerfile,它会为你自动化这些步骤。