1. 概述

容器化技术帮助我们快速构建并配置部署环境,以较低的成本解决现代应用的复杂性。秉持“一次编写,到处部署”的理念,我们使用容器化来应对现代应用的复杂性。

在本教程中,我们将深入探讨 Docker 镜像层,它们是容器化技术的基本构建块。

2. 镜像层

Docker 镜像是通过连接许多只读层创建的,这些层叠放在一起形成一个完整的镜像。如 Docker 和 Podman 这样的平台将这些层组合成一个单一的统一对象。

例如,让我们从注册表拉取一个 MySQL 镜像并快速查看:

# docker pull mysql
Using default tag: latest
latest: Pulling from library/mysql
492d84e496ea: Pull complete
bbe20050901c: Pull complete
e3a5e171c2f8: Pull complete
c3aceb7e4f48: Pull complete
269002e5cf58: Pull complete
d5abeb1bd18e: Pull complete
cbd79da5fab6: Pull complete
Digest: sha256:cdf3b62d78d1bbb1d2bd6716895a84014e00716177cbb7e90f6c6a37a21dc796
Status: Downloaded newer image for mysql:latest

上面片段中的每一行以“Pull complete”结尾,代表从注册表拉取的用于形成镜像的层。我们可以看到 MySQL 镜像有七层。

2.1. 镜像层的创建

现在,让我们深入理解通过 Dockerfile 示例如何通过创建新层来构建层。

Dockerfile 中的指令如 RUNCOPYADD 创建新的层,而其他指令仅创建中间层。前者命令会影响层大小,但后者不会。

让我们通过 Dockerfile 构建一个镜像。我们可以从这个链接引用 Dockerfile链接。我们使用 docker build 命令通过 Dockerfile 创建镜像:

# docker build -t layer-demo/latest .
Sending build context to Docker daemon  3.072kB
Step 1/8 : FROM ubuntu:latest
 ---> df5de72bdb3b
Step 2/8 : MAINTAINER baeldung.com
 ---> Running in 2c90e21f29e2
Removing intermediate container 2c90e21f29e2
 ---> 460d0651cc3d
Step 3/8 : ADD get_date.sh /root/get_date.sh
 ---> 492d1b205a94
Step 4/8 : RUN chmod 0644 /root/get_date.sh
 ---> Running in 08d04f1db0de
Removing intermediate container 08d04f1db0de
 ---> 480ba7f4bc50
Step 5/8 : RUN apt-get update
...
... output truncated ...
...
 ---> 28182a44db71
Step 6/8 : RUN apt-get -y install cron
...
... output truncated ...
...
 ---> 822f3eeca346
Step 7/8 : RUN crontab -l | { cat; echo "* * * * * bash /root/get_date.sh"; } | crontab -
 ---> Running in 635190dfb8d7
no crontab for root
Removing intermediate container 635190dfb8d7
 ---> 2822aac1f51b
Step 8/8 : CMD cron
 ---> Running in 876f0d5aca27
Removing intermediate container 876f0d5aca27
 ---> 5fc87be0f286
Successfully built 5fc87be0f286

这里发生了什么?为了创建这个镜像,我们注意到它需要八个步骤,对应于 Dockerfile 中的每个指令。最初,从注册表拉取了 Ubuntu 镜像 [镜像ID:df5de72bdb3b]:

  1. 它在上一步的镜像 [镜像ID:df5de72bdb3b] 上启动了中间容器 [容器ID:2c90e21f29e2]。
  2. 然后,在中间容器 [容器ID:2c90e21f29e2] 上执行指令。
  3. 中间容器通过提交转换为镜像 [镜像ID:460d0651cc3d],导致中间容器 [容器ID:2c90e21f29e2] 的删除。
  4. 删除中间容器后,镜像变为只读层,然后执行 Dockerfile 中的下一个指令。

然而,新层创建的步骤与上述相同。中间层无法影响镜像大小,而使用 RUNADDCOPY 的常规层可以增加大小。

3. 层大小

通常,镜像的大小完全由关联的层决定。使用 docker history 命令显示与镜像关联的每个层的大小。

以下示例中,大小为 0B 的层代表中间层,而 RUNCOPYADD 指令对镜像大小有所贡献:

# docker history layer-demo/latest
IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
5fc87be0f286   8 hours ago   /bin/sh -c #(nop)  CMD ["/bin/sh" "-c" "cron…   0B      
2822aac1f51b   8 hours ago   /bin/sh -c crontab -l | { cat; echo "* * * *…   208B
822f3eeca346   8 hours ago   /bin/sh -c apt-get -y install cron              987kB
28182a44db71   8 hours ago   /bin/sh -c apt-get update                       36MB
480ba7f4bc50   8 hours ago   /bin/sh -c chmod 0644 /root/get_date.sh         5B
492d1b205a94   8 hours ago   /bin/sh -c #(nop) ADD file:1f79f73be93042145…   5B
460d0651cc3d   8 hours ago   /bin/sh -c #(nop)  MAINTAINER baeldung.com      0B
df5de72bdb3b   4 weeks ago   /bin/sh -c #(nop)  CMD ["bash"]                 0B
      4 weeks ago   /bin/sh -c #(nop) ADD file:396eeb65c8d737180…   77.8MB 

现在,让我们计算所有层的大小,从基础镜像开始:

  • df5de72bdb3b – 77.800000 MB ## 第一步:基础 Ubuntu 镜像
  • 492d1b205a94 – 0.000005 MB ## 第三步:ADD 指令
  • 480ba7f4bc50 – 0.000005 MB ## 第四步:RUN 指令
  • 28182a44db71 – 36.000000 MB ## 第五步:RUN 指令
  • 822f3eeca346 – 0.987000 MB ## 第六步:RUN 指令
  • 2822aac1f51b – 0.000208 MB ## 第七步:RUN 指令

将上述数字相加得到 114.787 MB,进一步四舍五入为 115 MB。正如我们所见,计算的总和恰好与 docker image 命令中 layer-demo:latest 镜像的大小匹配:

# docker images 
REPOSITORY            TAG       IMAGE ID       CREATED       SIZE
layer-demo/latest     latest    5fc87be0f286   8 hours ago   115MB
ubuntu                latest    df5de72bdb3b   4 weeks ago   77.8MB

4. 遗留镜像

遗留镜像是在创建镜像过程中创建的镜像层。然而,在创建镜像之后,这些层与任何被打标签的镜像没有关系。因此,安全地移除所有这些镜像,因为它们会消耗不必要的磁盘空间。

要列出所有遗留镜像,我们可以使用 docker image 命令,并在搜索过滤器中设置 dangle 属性为 true:

# docker images --filter "dangling=true"

下面的命令显示遗留镜像,然后随后删除它们:

# docker images --quiet --filter=dangling=true | xargs --no-run-if-empty docker rmi

5. 总结

本文我们探讨了 Docker 镜像层的概念以及层的创建。此外,我们还讨论了可以用来识别与镜像关联的列表以及每个层大小的命令。

最后,我们看到了如何创建中间层,并了解到如果我们不频繁清除它们,遗留镜像会在我们的系统上停留。