深入浅出Docker之应用容器化

本文详细介绍了Docker应用容器化的全过程,包括单体应用的容器化步骤,如获取应用代码、分析、构建镜像和运行。讨论了Dockerfile的重要性和编写要点,并提到了生产环境中的多阶段构建最佳实践,以及如何利用构建缓存和合并镜像来优化镜像大小。此外,文章还强调了Docker镜像的管理和推送,以及如何运行应用程序。

应用的容器化简介

容器是为应用而生!具体来说,容器能够简化应用的构建、部署和运行过程
完整的应用容器化过程主要分为以下几个步骤
(1)编写应用代码。
(2)创建一个 Dockerfile,其中包括当前应用的描述依赖以及该如何运行这个应用
(3)对该 Dockerfile 执行 docker image build 命令。
(4)等待 Docker 将应用程序构建到 Docker 镜像中。
一旦应用容器化完成(即应用被打包为一个 Docker 镜像),就能以镜像的形式交付并以容器的方式运行了。
在这里插入图片描述

单体应用容器化

如何将一个简单的单节点 Node.js Web 应用容器化。如果是 Windows 操作系统的话,处理过程也是大同小异。现在通过以下几个步骤,来介绍具体的过程
(1)获取应用代码。
(2)分析 Dockerfile
(3)构建应用镜像。
(4)运行该应用。
(5)测试应用。
(6)容器应用化细节。
(7)生产环境中的多阶段构建。
(8)最佳实践。

获取应用代码

执行 git clone https://github.com/nigelpoulton/pswebGitHub 将代码克隆到本地。克隆操作会创建一个名为 psweb 的文件夹。可以进入该文件夹,并查看其中的内容:

[user1@bogon MyDocker]$ sudo git clone https://github.com/nigelpoulton/psweb
正克隆到 'psweb'...
remote: Enumerating objects: 13, done.
remote: Counting objects: 100% (13/13), done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 42 (delta 3), reused 1 (delta 0), pack-reused 29
Unpacking objects: 100% (42/42), done.
[user1@bogon MyDocker]$ cd psweb/
[user1@bogon psweb]$ ll
总用量 20
-rw-r--r--. 1 root root 341 12月 24 15:25 app.js
-rw-r--r--. 1 root root 216 12月 24 15:25 circle.yml
-rw-r--r--. 1 root root 338 12月 24 15:25 Dockerfile
-rw-r--r--. 1 root root 403 12月 24 15:25 package.json
-rw-r--r--. 1 root root 455 12月 24 15:25 README.md
drwxr-xr-x. 2 root root  39 12月 24 15:25 test
drwxr-xr-x. 2 root root  22 12月 24 15:25 views

该目录下包含了全部的应用源码,以及包含界面和单元测试的子目录。应用的代码已就绪,接下来分析一下 Dockerfile 的具体内容。

分析 Dockerfile

在代码目录当中,有个名称为 Dockerfile 的文件。这个文件包含了对当前应用的描述,并且能指导 Docker 完成镜像的构建。在 Docker 当中,包含应用文件的目录通常被称为构建上下文(Build Context。通常将 Dockerfile 放到构建上下文的根目录下。
另外很重要的一点是,文件开头字母是大写 D ,这里是一个单词。像“dockerfile”或者“Docker file”这种写法都是不允许的。接下来了解一下 Dockerfile 文件当中都包含哪些具体内容:

[user1@bogon psweb]$ nl Dockerfile
     1	# Test web-app to use with Pluralsight courses and Docker Deep Dive book
     2	# Linux x64
     3	FROM alpine
       
     4	LABEL maintainer="nigelpoulton@hotmail.com"
       
     5	# Install Node and NPM
     6	RUN apk add --update nodejs nodejs-npm
       
     7	# Copy app to /src
     8	COPY . /src
       
     9	WORKDIR /src
       
    10	# Install dependencies
    11	RUN  npm install
       
    12	EXPOSE 8080
       
    13	ENTRYPOINT ["node", "./app.js"]
[user1@bogon psweb]$ 

Dockerfile 主要包括两个用途:

  • 对当前应用的描述。
  • 指导 Docker 完成应用的容器化(创建一个包含当前应用的镜像)。

不要因 Dockerfile 就是一个描述文件而对其有所轻视!Dockerfile 能实现开发和部署两个过程的无缝切换。同时 Dockerfile 还能帮助新手快速熟悉这个项目。Dockerfile 对当前的应用及其依赖有一个清晰准确的描述,并且非常容易阅读和理解。因此,要像重视你的代码一样重视这个文件,并且将它纳入到源控制系统当中。

下面是这个文件中的一些关键步骤概述
# 号开头的行是注释
第 3 行FROM alpine:以 alpine 镜像作为当前镜像基础
第 4 行LABEL maintainer="nigelpoulton@hotmail.com":指定维护者(maintainer)为 nigelpoultion@hotmail.com
第 6 行RUN apk add --update nodejs nodejs-npm:安装 Node.jsNPM
第 8 行COPY . /src:将应用的代码复制到镜像当中
第 9 行WORKDIR /src:设置新的工作目录
第 11 行RUN npm install:安装依赖包
第 12 行EXPOSE 8080:记录应用的网络端口
第 13 行ENTRYPOINT ["node", "./app.js"]:最后将 app.js 设置为默认运行的应用

具体分析一下每一步的作用。
除注释以外,每个 Dockerfile 文件第一行都是 FROM 指令。FROM 指令指定的镜像,会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。本例中的应用基于 Linux 操作系统,所以在 FROM 指令当中所引用的也是一个 Linux 基础镜像;如果要容器化的应用是一个基于 Windows 操作系统的应用,就需要指定一个像 microsoft/aspnetcore-build 这样的 Windows 基础镜像了。基础镜像的结构如下图所示:
在这里插入图片描述
接下来,Dockerfile 中通过标签(LABLE)方式指定了当前镜像的维护者为“nigelpoulton@hotmail. com”。每个标签其实是一个键值对(Key-Value),在一个镜像当中可以通过增加标签的方式来为镜像添加自定义元数据。备注维护者信息有助于为该镜像的潜在使用者提供沟通途径,这是一种值得提倡的做法。

第 6 行 RUN apk add --update nodejs nodejs-npm 指令使用 alpineapk 包管理器将 nodejsnodejs-npm 安装到当前镜像之中。RUN 指令会在 FROM 指定的 alpine 基础镜像之上,新建一个镜像层来存储这些安装内容。当前镜像的结构如下图所示:
在这里插入图片描述
第 8 行 COPY . /src 指令将应用相关文件构建上下文复制到了当前镜像中,并且新建一个镜像层来存储。COPY执行结束之后,当前镜像共包含 3 层,如下图所示:
在这里插入图片描述
下一步,Dockerfile 通过 WORKDIR 指令,为 Dockerfile 中尚未执行的指令设置工作目录。该目录与镜像相关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层
然后,RUN npm install 指令会根据 package.json 中的配置信息,使用 npm 来安装当前应用的相关依赖包。npm 命令会在前文设置的工作目录中执行,并且在镜像中新建镜像层来保存相应的依赖文件。目前镜像一共包含 4 层,如下图所示:
在这里插入图片描述
因为当前应用需要通过 TCP 端口 8080 对外提供一个 Web 服务,所以在 Dockerfile 中通过 EXPOSE 8080 指令来完成相应端口的设置。这个配置信息会作为镜像的元数据被保存下来,并不会产生新的镜像层
最终,通过 ENTRYPOINT 指令来指定当前镜像的入口程序。ENTRYPOINT 指定的配置信息也是通过镜像元数据的形式保存下来,而不是新增镜像层。

容器化当前应用/构建具体的镜像

下面的命令会构建并生成一个名为 web:latest 的镜像。命令最后的点(.)表示 Docker在进行构建的时候,使用当前目录作为构建上下文
一定要在命令最后包含这个点,并且在执行命令前,要确认当前目录是 psweb

[user1@bogon psweb]$ pwd
/home/MyDocker/psweb
[user1@bogon psweb]$ ls -l | grep Docker
-rw-r--r--. 1 root root 338 12月 24 15:25 Dockerfile
[user1@bogon psweb]$ docker image build -t web:latest .
Sending build context to Docker daemon  117.8kB
Step 1/8 : FROM alpine
latest: Pulling from library/alpine
Digest: sha256:3c7497bf0c7af93428242d6176e8f7905f2201d8fc5861f45be7a346b5f23436
Status: Downloaded newer image for alpine:latest
 ---> 389fef711851
Step 2/8 : LABEL maintainer="nigelpoulton@hotmail.com"
 ---> Running in 6ab665445b10
Removing intermediate container 6ab665445b10
 ---> 8b4f424d522f
Step 3/8 : RUN apk add --update nodejs nodejs-npm
 ---> Running in 793a124d5499
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz
(1/9) Installing ca-certificates (20191127-r4)
(2/9) Installing brotli-libs (1.0.9-r1)
(3/9) Installing c-ares (1.16.1-r0)
(4/9) Installing libgcc (9.3.0-r2)
(5/9) Installing nghttp2-libs (1.41.0-r0)
(6/9) Installing libstdc++ (9.3.0-r2)
(7/9) Installing libuv (1.38.1-r0)
(8/9) Installing nodejs (12.18.4-r0)
(9/9) Installing npm (12.18.4-r0)
Executing busybox-1.31.1-r19.trigger
Executing ca-certificates-20191127-r4.trigger
OK: 65 MiB in 23 packages
Removing intermediate container 793a124d5499
 ---> 07c5a1bc5dcd
Step 4/8 : COPY . /src
 ---> 8737f3167b8e
Step 5/8 : WORKDIR /src
 ---> Running in adba20782fef
Removing intermediate container adba20782fef
 ---> ee975f1c64dd
Step 6/8 : RUN  npm install
 ---> Running in 5ac76daa3f95
npm WARN deprecated debug@4.1.1: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)
npm WARN deprecated fsevents@2.1.3: Please update to v 2.2.x
npm WARN deprecated core-js@2.6.12: core-js@<3 is no longer maintained and not recommended for usage due to the number of issues. Please, upgrade your dependencies to the actual version of core-js@3.

> core-js@2.6.12 postinstall /src/node_modules/core-js
> node -e "try{require('./postinstall')}catch(e){}"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js on Open Collective or Patreon: 
> https://opencollective.com/core-js 
> https://www.patreon.com/zloirock 

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~2.1.2 (node_modules/chokidar/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

added 266 packages from 227 contributors and audited 267 packages in 53.228s

39 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Removing intermediate container 5ac76daa3f95
 ---> 4773bd46c72b
Step 7/8 : EXPOSE 8080
 ---> Running in 89180a80cd9c
Removing intermediate container 89180a80cd9c
 ---> 15115bd85a9c
Step 8/8 : ENTRYPOINT ["node", "./app.js"]
 ---> Running in 98db641206d5
Removing intermediate container 98db641206d5
 ---> a511ab5c8a95
Successfully built a511ab5c8a95
Successfully tagged web:latest

如上代码块所示,倒数第 2 行成功创建了 IDa511ab5c8a95 的镜像。执行 image ls 命令查看镜像库中的镜像列表:

[user1@bogon psweb]$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
web          latest    a511ab5c8a95   17 minutes ago   87.9MB
<none>       <none>    bccbfe065bbf   36 minutes ago   464MB
alpine       latest    389fef711851   7 days ago       5.58MB
ubuntu       20.04     f643c72bc252   4 weeks ago      72.9MB
ubuntu       18.04     2c047404e52d   4 weeks ago      63.3MB

上述结果中包含web镜像,说明应用容器化已经成功了!可以通过 docker image inspect web:latest确认刚刚构建的镜像配置是否正确。这个命令会列出 Dockerfile 中设置的所有配置项。

推送镜像到仓库

在创建一个镜像之后,将其保存在一个镜像仓库服务是一个不错的方式。这样存储镜像会比较安全,并且可以被其他人访问使用。Docker Hub 就是这样的一个开放的公共镜像仓库服务,并且这也是 docker image push 命令默认的推送地址。
在推送镜像之前,需要先使用 Docker ID 登录 Docker Hub。除此之外,还需要为待推送的镜像打上合适的标签。接下来介绍如何登录 Docker Hub,并将镜像推送到其中。
首先登录自己的账号(可以访问 Docker Hub进行注册 ),Username 为自己的账号名字,Password 为密码:

[user1@bogon psweb]$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: aaa
Password: 
Login Succeeded

推送 Docker 镜像之前,还需要为镜像打标签。这是因为 Docker 在镜像推送的过程中需要如下信息:

  • Registry(镜像仓库服务)
  • Repository(镜像仓库)
  • Tag(镜像标签)

我们无须为 RegistryTag 指定值。当你没有为上述信息指定具体值的时候,Docker 会默认 Registry=docker.ioTag=latest 。但是 Docker 并没有给 Repository 提供默认值,而是从被推送镜像中的 REPOSITORY 属性值获取。这一点可能不好理解,下面会通过一个完整的例子来介绍如何向 Docker Hub 推送一个镜像。

在本章节前面的例子中执行了 docker image ls 命令。在该命令对应的输出内容中可以看到,镜像仓库的名称是 web。这意味着执行 docker image push 命令,会尝试将镜像推送到docker.io/web:latest 中。但是其实 aaa 账号没有 web 这个镜像仓库的访问权限,所以只能尝试推送到 aaa 这个二级命名空间(Namespace)之下。因此需要使用 aaa 这个 ID,为当前镜像重新打一个标签。

[user1@bogon psweb]$ docker image tag web:latest aaa/web:20.12.24

为镜像打标签命令的格式是 docker image tag ,其作用是为指定的镜像添加一个额外的标签,并且不需要覆盖已经存在的标签。
再次执行 docker image ls 命令,可以看到这个镜像现在有了两个标签,其中一个包含 Docker ID aaa

REPOSITORY      TAG        IMAGE ID       CREATED             SIZE
aaa/web   20.12.24   a511ab5c8a95   About an hour ago   87.9MB
web             latest     a511ab5c8a95   About an hour ago   87.9MB
<none>          <none>     bccbfe065bbf   About an hour ago   464MB
alpine          latest     389fef711851   7 days ago          5.58MB
ubuntu          20.04      f643c72bc252   4 weeks ago         72.9MB
ubuntu          18.04      2c047404e52d   4 weeks ago         63.3MB

现在执行 docker image push 命令将该镜像推送到 Docker Hub

[user1@bogon psweb]$ docker image push aaa/web:20.12.24
The push refers to repository [docker.io/aaa/web]
89002f789868: Pushed 
0e5091a59b5c: Pushed 
f1d97470266f: Pushed 
777b2c648970: Mounted from library/alpine 
20.12.24: digest: sha256:ba7f78e135a5824c70fede0a2179fc9b64ebddde0864c28ce328a0ebf29e4dec size: 1161

执行结果可以在登录账户的仓库页面查看,假定你的用户名是 nigelpoulton ,下图展示了 Docker 如何确定镜像所要推送的目的仓库:
在这里插入图片描述

运行应用程序

app.js 这个文件内容中可以看出,这其实就是一个在 8080 端口提供 Web 服务的应用程序。下面的命令会基于 web:latest 这个镜像,启动一个名为 c1 的容器。该容器将内部的 8080 端口与 Docker 主机的 8000 端口进行映射。这意味你可以打开一个浏览器,在地址栏输入 Docker 主机的 DNS 名称或者 IP 地址,然后就能直接访问这个 Web 应用了。

如果 Docker 主机已经运行了某个使用 80 端口的应用程序,大家可以在执行 docker container run 命令时指定一个不同的映射端口。例如,可以使用 -p 5000:8080 参数,将 Docker 内部应用程序的 8080 端口映射到主机的 5000 端口。

这里我们将容器中的 8080 端口映射到系统的 8000 端口

[user1@bogon psweb]$ docker container run -d --name c1 -p 8000:8080 web:latest

-d 参数的作用是让应用程序以守护线程的方式在后台运行-p 参数的作用是将主机的 8000 端口与容器内的 8080 端口进行映射。
接下来验证一下程序是否真的成功运行,并且对外提供服务的端口是否正常工作:

[user1@bogon psweb]$ docker container ls
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                    NAMES
8d01b0ca1069   web:latest     "node ./app.js"          2 minutes ago   Up 2 minutes   0.0.0.0:8000->8080/tcp   c1

从上面的输出内容中可以看到,容器已经正常运行。需要注意的是,8000 端口已经成功映射到了 8080 之上,并且任意外部主机(0.0.0.0:8000)均可以通过 8000 端口访问该容器。

生产环境中的多阶段构建

对于 Docker 镜像来说,过大的体积并不好!越大则越慢,这就意味着更难使用,而且可能更加脆弱,更容易遭受攻击。

鉴于此,Docker 镜像应该尽量小。对于生产环境镜像来说,目标是将其缩小到仅包含运行应用所必需的内容即可。问题在于,生成较小的镜像并非易事。

例如,不同的 Dockerfile 写法就会对镜像的大小产生显著影响。常见的例子是,每一个 RUN 指令会新增一个镜像层。因此,通过使用 && 连接多个命令以及使用反斜杠 \ 换行的方法,将多个命令包含在一个 RUN 指令中,通常来说是一种值得提倡的方式。这并不难掌握,多加练习即可。

另一个问题是开发者通常不会在构建完成后进行清理。当使用 RUN 执行一个命令时,可能会拉取一些构建工具,这些工具会留在镜像中移交至生产环境。这是不合适的!

有多种方式来改善这一问题——比如常见的是采用建造者模式(Builder Pattern。但无论采用哪种方式,通常都需要额外的培训,并且会增加构建的复杂度。

建造者模式需要 至少两个Dockerfile : 一个用于开发环境,一个用于生产环境。首先需要编写 Dockerfile.dev,它基于一个大型基础镜像(Base Image),拉取所需的构建工具,并构建应用。接下来,需要基于 Dockerfile.dev 构建一个镜像,并用这个镜像创建一个容器。这时再编写 Dockerfile.prod,它基于一个较小的基础镜像开始构建,并从刚才创建的容器中将应用程序相关的部分复制过来。整个过程需要编写额外的脚本才能串联起来。

这种方式是可行的,但是比较复杂。 多阶段构建(Multi-Stage Build 是一种更好的方式!

多阶段构建能够在不增加复杂性的情况下优化构建过程。

多阶段构建方式使用一个 Dockerfile,其中包含多个 FROM 指令。每一个 FROM 指令都是一个新的构建阶段(Build Stage),并且可以方便地复制之前阶段的构件。

示例源码可从这个地址获得,Dockerfile 位于 app 目录。这是一个基于 Linux 系统的应用,因此只能运行在 Linux 容器环境上。这个代码库是从 dockersamples/atsea-sample-shop-app fork 过来的,以防上游代码库被删除。

Dockerfile 如下所示。

FROM node:latest AS storefront
WORKDIR /usr/src/atsea/app/react-app
COPY react-app .
RUN npm install
RUN npm run build

FROM maven:latest AS appserver
WORKDIR /usr/src/atsea
COPY pom.xml .
RUN mvn -B -f pom.xml -s /usr/share/maven/ref/settings-docker.xml dependency
\:resolve
COPY . .
RUN mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests

FROM java:8-jdk-alpine AS production
RUN adduser -Dh /home/gordon gordon
WORKDIR /static
COPY --from=storefront /usr/src/atsea/app/react-app/build/ .
WORKDIR /app
COPY --from=appserver /usr/src/atsea/target/AtSea-0.0.1-SNAPSHOT.jar .
ENTRYPOINT ["java", "-jar", "/app/AtSea-0.0.1-SNAPSHOT.jar"]
CMD ["--spring.profiles.active=postgres"]

首先注意到,Dockerfile 中有 3 个 FROM 指令。每一个 FROM 指令构成一个单独的构建阶段。各个阶段在内部从 0 开始编号。不过,示例中针对每个阶段都定义了便于理解的名字:

  • 阶段 0 叫作 storefront

storefront 阶段拉取了大小超过 600MBnode:latest 镜像,然后设置了工作目录,复制一些应用代码进去,然后使用 2 个 RUN 指令来执行 npm 操作。这会生成 3 个镜像层并显著增加镜像大小。指令执行结束后会得到一个比原镜像大得多的镜像,其中包含许多构建工具和少量应用程序代码。

  • 阶段 1 叫作 appserver

appserver 阶段拉取了大小超过 700MBmaven:latest 镜像。然后通过 2 个 COPY 指令和 2 个 RUN 指令生成了 4 个镜像层。这个阶段同样会构建出一个非常大的包含许多构建工具和非常少量应用程序代码的镜像。

  • 阶段 2 叫作 production

production 阶段拉取 java:8-jdk-alpine 镜像,这个镜像大约 150MB,明显小于前两个构建阶段用到的 nodemaven 镜像。这个阶段会创建一个用户,设置工作目录,从 storefront 阶段生成的镜像中复制一些应用代码过来。之后,设置一个不同的工作目录,然后从 appserver 阶段生成的镜像中复制应用相关的代码。最后,production 设置当前应用程序为容器启动时的主程序。

重点在于 COPY --from 指令,它从之前的阶段构建的镜像中仅复制生产环境相关的应用代码,而不会复制生产环境不需要的构件

还有一点也很重要,多阶段构建这种方式仅用到了一个 Dockerfile,并且 docker image build 命令不需要增加额外参数。下面演示一下构建操作。克隆代码库并切换到 app 目录,并确保其中有 Dockerfile

$ cd atsea-sample-shop-app/app

$ ls -l
total 24
-rw-r--r-- 1 root root  682 Oct 1 22:03 Dockerfile
-rw-r--r-- 1 root root 4365 Oct 1 22:03 pom.xml
drwxr-xr-x 4 root root 4096 Oct 1 22:03 react-app
drwxr-xr-x 4 root root 4096 Oct 1 22:03 src

执行构建(这可能会花费几十分钟):

$ docker image build -t multi:stage .

Sending build context to Docker daemon 3.658MB
Step 1/19 : FROM node:latest AS storefront
latest: Pulling from library/node
aa18ad1a0d33: Pull complete
15a33158a136: Pull complete
<Snip>
Step 19/19 : CMD --spring.profiles.active=postgres
 ---> Running in b4df9850f7ed
 ---> 3dc0d5e6223e
Removing intermediate container b4df9850f7ed
Successfully built 3dc0d5e6223e
Successfully tagged multi:stage

示例中 multi:stage 标签是自行定义的,你可以根据自己的需要和规范来指定标签名称。不过并不要求一定必须为多阶段构建指定标签。

实践

利用构建缓存

Docker 的构建过程利用了缓存机制。观察缓存效果的一个方法,就是在一个干净的 Docker 主机上构建一个新的镜像,然后再重复同样的构建。第一次构建会拉取基础镜像,并构建镜像层,构建过程需要花费一定时间;第二次构建几乎能够立即完成。这就是因为第一次构建的内容(如镜像层)能够被缓存下来,并被后续的构建过程复用。

docker image build 命令会从顶层开始解析 Dockerfile 中的指令并逐行执行。而对每一条指令,Docker 都会检查缓存中是否已经有与该指令对应的镜像层。如果有,即为缓存命中(Cache Hit,并且会使用这个镜像层;如果没有,则是缓存未命中(Cache MissDocker 会基于该指令构建新的镜像层。缓存命中能够显著加快构建过程。

示例用的 Dockerfile 如下:

FROM alpine
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
  • 第1条指令FROM alpine:告诉 Docker 使用 alpine:latest 作为基础镜像。如果主机中已经存在这个镜像,那么构建时会直接跳到下一条指令;如果镜像不存在,则会从 Docker Hubdocker.io)拉取。

  • 第2条指令RUN apk add --update nodejs nodejs-npm:对镜像执行一条命令。此时,Docker 会检查构建缓存中是否存在基于同一基础镜像,并且执行了相同指令的镜像层。在此例中,Docker会检查缓存中是否存在一个基于 alpine:latest 镜像且执行了 RUN apk add --update nodejs nodejs-npm 指令构建得到的镜像层。如果找到该镜像层,Docker 会跳过这条指令,并链接到这个已经存在的镜像层,然后继续构建;如果无法找到符合要求的镜像层,则设置缓存无效并构建该镜像层。此处“设置缓存无效”作用于本次构建的后续部分。也就是说 Dockerfile 中接下来的指令将全部执行而不会再尝试查找构建缓存。

假设 Docker 已经在缓存中找到了该指令对应的镜像层(缓存命中),并且假设这个镜像层的 IDAAA 。下一条指令会复制一些代码到镜像中(COPY . /src)。因为上一条指令命中了缓存,Docker 会继续查找是否有一个缓存的镜像层也是基于 AAA 层并执行了 COPY . /src 命令。如果有,Docker 会链接到这个缓存的镜像层并继续执行后续指令;如果没有,则构建镜像层,并对后续的构建操作设置缓存无效。

假设 Docker 没有对应该指令的缓存镜像层(缓存未命中),并且假设这个镜像层的 IDBBB 。那么 Docker 将继续执行 Dockerfile 中剩余的指令。

首先,一旦有指令在缓存中未命中(没有该指令对应的镜像层),则后续的整个构建过程将不再使用缓存。在编写 Dockerfile 时须特别注意这一点,尽量将易于发生变化的指令置于 Dockerfile 文件的后方执行。这意味着缓存未命中的情况将直到构建的后期才会出现——从而构建过程能够尽量从缓存中获益。

通过对 docker image build 命令加入 --nocache=true 参数可以强制忽略对缓存的使用。

还有一点也很重要,那就是 COPYADD 指令会检查复制到镜像中的内容自上一次构建之后是否发生了变化。例如,有可能 Dockerfile 中的 COPY . /src 指令没有发生变化,但是被复制的目录中的内容已经发生变化了。为了应对这一问题,Docker 会计算每一个被复制文件的 Checksum 值,并与缓存镜像层中同一文件的 checksum 进行对比。如果不匹配,那么就认为缓存无效并构建新的镜像层。

合并镜像

合并镜像并非一个最佳实践,因为这种方式利弊参半。
总体来说,Docker 会遵循正常的方式构建镜像,但之后会增加一个额外的步骤,将所有的内容合并到一个镜像层中。**当镜像中层数太多时,合并是一个不错的优化方式。**例如,当创建一个新的基础镜像,以便基于它来构建其他镜像的时候,这个基础镜像就最好被合并为一层。

缺点是,**合并的镜像将无法共享镜像层。**这会导致存储空间的低效利用,而且 pushpull 操作的镜像体积更大。执行 docker image build 命令时,可以通过增加 --squash 参数来创建一个合并的镜像。

下图阐释了合并镜像层带来的存储空间低效利用的问题。两个镜像的内容是完全一样的,区别在于是否进行了合并。在使用 docker image push 命令发送镜像到 Docker Hub 时,合并的镜像需要发送全部字节,而不合并的镜像只需要发送不同的镜像层即可:
在这里插入图片描述

使用 no-install-recommends

在构建 Linux 镜像时,若使用的是 APT 包管理器,则应该在执行 apt-get install 命令时增加 no-install-recommends 参数。这能够确保 APT 仅安装核心依赖(Depends 中定义)包,而不是推荐和建议的包。这样能够显著减少不必要包的下载数量。

在构建 Windows 镜像时,尽量避免使用 MSI 包管理器。因其对空间的利用率不高,会大幅增加镜像的体积。

参考:
[1] 深入浅出Docker.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

great-wind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值