Docker(五)应用的容器化

Docker(五)应用的容器化

第一节 应用的容器化

  Docker的核心思想就是如何将应用整合到容器中,并且能在容器中实际运行。将应用整合到容器中并且运行起来的这个过程,称为“容器化”(Containerizing),有时也叫作“Docker化”(Dockerizing)。

  本节将逐步介绍容器化一个简单的Linux Web应用的过程。如果没有一个Linux的Docker环境来跟进练习,那么可以免费使用Play With Docker。只需使用浏览器打开Play With Docker的页面,并启动若干Linux Docker节点即可。

1.1 简介

  容器是为应用而生!具体来说,容器能够简化应用的构建、部署和运行过程

完整的应用容器化过程主要分为以下几个步骤。

  1. 编写应用代码。
  2. 创建一个Dockerfile,其中包括当前应用的描述、依赖以及该如何运行这个应用。
  3. 对该Dockerfile执行docker image build命令。
  4. 等待Docker将应用程序构建到Docker镜像中。

  一旦应用容器化完成(即应用被打包为一个Docker镜像),就能以镜像的形式交付并以容器的方式运行了。图8.1展示了上述步骤。

容器化的基本过程


1.2 详解

1.2.1 单体应用容器化

  接下来逐步展示如何将一个简单的单节点Node.js Web应用容器化。本部分主要完成单节点应用的容器化,采用Docker Compose去完成多节点应用容器化,使用Docker Stack去处理更复杂应用的容器化场景。接下来通过以下几个步骤,来介绍具体的过程。

(1)获取应用代码

  应用代码可以从作者的GitHub主页获取,将代码克隆到本地,创建一个名为psweb的文件夹。(本人直接用PC端DFW克隆到本地,后续环境即Windows 10)

1
$ git clone https://github.com/nigelpoulton/psweb.git

  跳转项目根目录,查看项目文件结构。

1
2
3
4
5
6
7
8
9
10
11
$ cd psweb

$ ls -l
total 28
-rw-r--r-- 1 root root 341 Sep 29 16:26 app.js
-rw-r--r-- 1 root root 216 Sep 29 16:26 circle.yml
-rw-r--r-- 1 root root 338 Sep 29 16:26 Dockerfile
-rw-r--r-- 1 root root 421 Sep 29 16:26 package.json
-rw-r--r-- 1 root root 370 Sep 29 16:26 README.md
drwxr-xr-x 2 root root 4096 Sep 29 16:26 test/
drwxr-xr-x 2 root root 4096 Sep 29 16:26 views/
(2)分析Dockerfile

  在代码目录当中,有个名称为Dockerfile的文件。这个文件包含了对当前应用的描述,并且能指导Docker完成镜像的构建。
在Docker当中,包含应用文件的目录通常被称为构建上下文(Build Context)。通常将Dockerfile放到构建上下文的根目录下。

  另外很重要的一点是,文件开头字母是大写D,这里是一个单词。 像“dockerfile”或者“Docker file”这种写法都是不允许的。

  接下来了解一下Dockerfile文件当中都包含哪些具体内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Test web-app to use with Pluralsight courses and Docker Deep Dive book
# Linux x64
FROM alpine

LABEL maintainer="nigelpoulton@hotmail.com"

# Install Node and NPM
RUN apk add --update nodejs nodejs-npm

# Copy app to /src
COPY . /src

WORKDIR /src

# Install dependencies
RUN npm install

EXPOSE 8080

ENTRYPOINT ["node", "./app.js"]

Dockerfile主要包括两个用途:

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

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

  下面是这个文件中的一些关键步骤概述:以alpine镜像作为当前镜像基础,指定维护者(maintainer)为“nigelpoultion@hotmail.com”,安装Node.js和NPM,将应用的代码复制到镜像当中,设置新的工作目录,安装依赖包,记录应用的网络端口,最后将app.js设置为默认运行的应用。

  具体分析一下每一步的作用:每个Dockerfile文件第一行都是FROM指令。FROM指令指定的镜像,会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。本例中的应用基于Linux操作系统,所以在FROM指令当中所引用的也是一个Linux基础镜像;如果要容器化的应用是一个基于Windows操作系统的应用,就需要指定一个像microsoft/aspnetcore-build这样的Windows基础镜像了。当前基础镜像的结构如图8.2所示

基础镜像的结构

  接下来,Dockerfile中通过标签(LABLE)方式指定了当前镜像的维护者为“nigelpoulton@hotmail.com”。每个标签其实是一个键值对(Key-Value),在一个镜像当中可以通过增加标签的方式来为镜像添加自定义元数据。备注维护者信息有助于为该镜像的潜在使用者提供沟通途径,这是一种值得提倡的做法。

  RUN apk add –update nodejs nodejs-npm指令使用alpine的apk包管理器将nodejs和nodejs-npm安装到当前镜像之中。RUN指令会在FROM指定的alpine基础镜像之上,新建一个镜像层来存储这些安装内容。当前镜像的结构如图8.3所示。

当前镜像的结构

  COPY. / src指令将应用相关文件从构建上下文复制到了当前镜像中,并且新建一个镜像层来存储。COPY执行结束之后,当前镜像共包含3层,如图8.4所示。

当前的3层镜像

  下一步,Dockerfile通过WORKDIR指令,为Dockerfile中尚未执行的指令设置工作目录。该目录与镜像相关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层。

  然后,RUN npm install指令会根据package.json中的配置信息,使用npm来安装当前应用的相关依赖包。npm命令会在前文设置的工作目录中执行,并且在镜像中新建镜像层来保存相应的依赖文件。目前镜像一共包含4层,如图8.5所示。

当前的4层镜像

  因为当前应用需要通过TCP端口8080对外提供一个Web服务,所以在Dockerfile中通过EXPOSE 8080指令来完成相应端口的设置。这个配置信息会作为镜像的元数据被保存下来,并不会产生新的镜像层。

  最终,通过ENTRYPOINT指令来指定当前镜像的入口程序。ENTRYPOINT指定的配置信息也是通过镜像元数据的形式保存下来,而不是新增镜像层。

(3)构建应用镜像

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

1
$ docker image build -t web:latest . 

  命令执行结束后,检查本地Docker镜像库是否包含了刚才构建的镜像。可以通过docker image inspect web:latest来确认刚刚构 建的镜像配置是否正确。这个命令会列出Dockerfile中设置的所有配置 项。

1
2
3
$ docker image ls 
REPO TAG IMAGE ID CREATED SIZE
web latest fc69fdc4c18e 10 seconds ago 64.4MB
(4)推送镜像到仓库

  在创建一个镜像之后,将其保存在一个镜像仓库服务是一个不错的方式。这样存储镜像会比较安全,并且可以被其他人访问使用。Docker Hub就是这样的一个开放的公共镜像仓库服务,并且这也是docker image push命令默认的推送地址。

  在推送镜像之前,需要先使用Docker ID登录Docker Hub(xxx表示docker id)。除此之外,还需要为待推送的镜像打上合适的标签。因为Docker在镜像推送的过程中需要如下信息:Registry(镜像仓库服务),Repository(镜像仓库),Tag(镜像标签)。

1
2
3
4
$ docker login 
Login with **your** Docker ID to push and pull images from Docker Hub...
Username: xxx
Password: Login Succeeded

  Docker会默认Registry=docker.io、Tag=latest。但是 Docker并没有给Repository提供默认值,而是从被推送镜像中的REPOSITORY属性值获取。

  执行docker image push命令,会尝试将镜像推送到docker.io/web:latest中。但是xxx用户并没有web这个镜像仓库的访问权限,所以只能尝试推送到xxx这个二级命名空间(Namespace)之下。因此需要使用xxx这个ID,为当前镜像重新打一个标签。

1
$ docker image tag web:latest nigelpoulton/web:latest

  再次执行docker image ls命令,可以看到这个镜像现在有了两个标签,其中一个包含Docker ID nigelpoulton。

1
2
3
4
$ docker image ls 
REPO TAG IMAGE ID CREATED SIZE
web latest fc69fdc4c18e 10 secs ago 64.4MB
xxx/web latest fc69fdc4c18e 10 secs ago 64.4MB

  现在将该镜像推送到Docker Hub。

1
$ docker image push xxx/web:latest

  图8.6展示了Docker如何确定镜像所要推送的目的仓库。

确定镜像所要推送的目的仓库

(5)运行该应用

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

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

1
2
3
$ docker container run -d --name c1 \  
-p 80:8080 \
web:latest

  接下来验证一下程序是否真的成功运行,并且对外提供服务的端口是否正常工作。

1
2
3
4
$ docker container ls

ID IMAGE COMMAND STATUS PORTS
49.. web:latest "node ./app.js" UP 6 secs 0.0.0.0:80->8080/tcp
(6)测试应用

  打开浏览器,在地址栏输入DNS名称或者IP地址,就能访问到正在运行的应用程序了。

  如果没有出现正常的界面,尝试执行下面的检查来确认原因所在。

  • 使用docker container ls指令来确认容器已经启动并且正常运行。容器名称是c1,并且从输出内容中能看到0.0.0.0:80>8080/tcp。
  • 确认防火墙或者其他网络安全设置没有阻止访问Docker主机的80端口。
(7)容器应用化细节

  一些细节部分的回顾和总结。

  Dockerfile中的注释行,都是以#开头的。除注释之外,每一行都是一条指令(Instruction)。指令的参数格式如下:INSTRUCTION argument,指令是不区分大小写的,但是通常都采用大写的方式,这样Dockerfile的可读性会高一些。

  Docker image build命令会按行来解析Dockerfile中的指令并顺序执行。部分指令会在镜像中创建新的镜像层,其他指令只会增加或修改镜像的元数据信息。在上面的例子当中,新增镜像层的指令包括FROM、RUN以及COPY,而新增元数据的指令包括EXPOSE、WORKDIR、ENV以及ENTERPOINT。 关于如何区分命令是否会新建镜像层,一个基本的原则是,如果指令的作用是向镜像中增添新的文件或者程序,那么这条指令就会新建镜像层;如果只是告诉Docker如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据。

  可以通过docker image history来查看在构建镜像的过程中都执行了哪些指令。

1
2
3
4
5
6
7
8
9
10
11
12
$ docker image history web:latest

IMAGE CREATED BY SIZE
fc6..18e /bin/sh -c #(nop) ENTRYPOINT ["node" "./a... 0B
334..bf0 /bin/sh -c #(nop) EXPOSE 8080/tcp 0B
b27..eae /bin/sh -c npm install 14.1MB
932..749 /bin/sh -c #(nop) WORKDIR /src 0B
052..2dc /bin/sh -c #(nop) COPY dir:2a6ed1703749e80... 22.5kB
c1d..81f /bin/sh -c apk add --update nodejs nodejs-npm 46.1MB
336..b92 /bin/sh -c #(nop) LABEL maintainer=nigelp... 0B
3fd..f02 /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
/bin/sh -c #(nop) ADD file:093f0723fa46f6c... 4.15MB

  在上面的输出内容当中,有两点是需要注意的。首先,每行内容都对应了Dockerfile中的一条指令(顺序是自下而上)。CREATE BY这一列中还展示了当前行具体对应Dockerfile中的哪条指令。其次,从这个输出内容中,可以观察到只有4条指令会新建镜像层 (就是那些SIZE列对应的数值不为零的指令),分别对应Dockerfile中的FROM、RUN以及COPY指令。虽然其他指令看上去跟这些新建镜像层的指令并无区别,但实际上它们只在镜像中新增了元数据信息。这些指令之所以看起来没有区别,是因为Docker对之前构建镜像层方式的兼容。

  可以通过执行docker image inspect指令来确认确实只有4个层被创建了。

1
2
3
4
5
6
7
8
9
10
11
$ docker image inspect web:latest 
<Snip>
}, "RootFS": {
"Type": "layers",
"Layers": [
"sha256:cd7100...1882bd56d263e02b6215",
"sha256:b3f88e...cae0e290980576e24885",
"sha256:3cfa21...cc819ef5e3246ec4fe16",
"sha256:4408b4...d52c731ba0b205392567"
]
},

  使用FROM指令引用官方基础镜像是一个很好的习惯,官方的镜像通常会遵循一些最佳实践,并且能帮助使用者规避一些已知的问
题。除此之外,使用FROM的时候选择一个相对较小的镜像文件通常也能避免一些潜在的问题。

  我们也可以观察docker image build命令具体的输出内容,了解镜像构建的过程。在下面的片段中,可以看到基本的构建过程是,运行临时容器>在该容器中运行Dockerfile中的指令>将指令运行结果保存为 一个新的镜像层>删除临时容器。

1
2
3
4
5
6
7
8
9
Step 3/8 : RUN apk add --update nodejs nodejs-npm 
---> Running in e690ddca785f << Run inside of temp container
fetch http://dl-cdn...APKINDEX.tar.gz
fetch http://dl-cdn...APKINDEX.tar.gz
(1/10) Installing ca-certificates (20171114-r0)
<Snip>
OK: 61 MiB in 21 packages ---> c1d31d36b81f << Create new layer
Removing intermediate container << Remove temp container
Step 4/8 : COPY . /src

1.2.2 生产环境中的多阶段构建

  对于Docker镜像来说,体积越大则越慢,就意味着更难使用,而且可能更加脆弱,更容易遭受攻击。对于生产环境镜像来说,目标是将其缩小到仅包含运行应用所必需的内容即可,但生成较小的镜像并非易事。

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

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

  有多种方式来改善这一问题——比如常见的是采用建造者模式(Builder Pattern)。但无论采用哪种方式,通常都需要额外的培训,并且会增加构建的复杂度。建造者模式需要至少两个Dockerfile——一个用于开发环境,一个用于生产环境。首先需要编写Dockerfile.dev,它基于一个大型基础镜像(Base Image),拉取所需的构建工具,并构建应用。接下来,需要基于Dockerfile.dev构建一个镜像,并用这个镜像创建一个容器。这时再编写Dockerfile.prod,它基于一个较小的基础镜像开始构建,并从刚才创建的容器中将应用程序相关的部分复制过来。整个过程需要编写额外的脚本才能串联起来。这种方式是可行的,但是比较复杂。

  多阶段构建(Multi-Stage Build)是一种更好的方式!多阶段构建是随Docker 17.05版本新增的一个特性,用于构建精简的生产环境镜像。多阶段构建能够在不增加复杂性的情况下优化构建过程。多阶段构建方式使用一个Dockerfile,其中包含多个FROM指令。每一个FROM指令都是一个新的构建阶段(Build Stage),并且可以方便地复制之前阶段的构件。示例源码可从作者GitHub主页中atsea-sample-shopapp仓库获得,Dockerfile位于app目录。这是一个基于Linux系统的应用,因此只能运行在Linux容器环境上。

  Dockerfile如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 dependen
cy
\: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,阶段1叫作appserver,阶段2叫作production。

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

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

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

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

  还有一点也很重要,多阶段构建这种方式仅用到了一个Dockerfile,并且docker image build命令不需要增加额外参数。

  下面就进行一下构建,首先克隆代码库并切换到app目录,并确保其中有Dockerfile。

1
2
3
4
5
6
7
8
$ 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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ 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

  执行docker image ls命令查看由构建命令拉取和生成的镜像。

1
2
3
4
5
6
7
8
9
$ docker image ls

REPO TAG IMAGE ID CREATED SIZE
node latest 9ea1c3e33a0b 4 days ago 673MB
<none> <none> 6598db3cefaf 3 mins ago 816MB
maven latest cbf114925530 2 weeks ago 750MB
<none> <none> d5b619b83d9e 1 min ago 891MB
java 8-jdk-alpine 3fd9dd82815c 7 months ago 145MB
multi stage 3dc0d5e6223e 1 min ago 210MB

  第一行显示了在storefront阶段拉取的node:latest镜像,第二行内容为该阶段生成的镜像(通过添加代码,执行npm安装和构建操作生成该镜像)。这两个都包含许多的构建工具,因此镜像体积非常大。

  第3~4行是在appserver阶段拉取和生成的镜像,它们也都因为包含许多构建工具而导致体积较大。

  最后一行是Dockerfile中的最后一个构建阶段(stage2/production)生成的multi:stage镜像。可见它明显比之前阶段拉取和生成的镜像要小。这是因为该镜像是基于相对精简的java:8-jdk-alpine镜像构建的,并且仅添加了用于生产环境的应用程序文件。

  最终,无须额外的脚本,仅对一个单独的Dockerfile执行docker image build命令,就创建了一个精简的生产环境镜像。

1.2.3 最佳实践

(1)利用构建缓存

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
--告诉Docker使用alpine:latest作为基础镜像,如果镜像不存在,会从Docker Hub(docker.io)拉取。
FROM alpine

--对镜像执行一条命令,Docker会检查构建缓存中是否存在基于同一基础镜像,并且执行了相同指令的镜像层
--如果找到该镜像层,Docker会跳过这条指令,并链接到这个已经存在的镜像层,然后继续构建;
--如果无法找到符合要求的镜像层,则设置缓存无效(后续不再取缓存)并构建该镜像层
RUN apk add --update nodejs nodejs-npm

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

WORKDIR /src

RUN npm install

EXPOSE 8080

ENTRYPOINT ["node", "./app.js"]

  一旦有指令在缓存中未命中(没有该指令对应的镜像层),则后续的整个构建过程将不再使用缓存。在编写Dockerfile时须特别注意这一点,尽量将易于发生变化的指令置于Dockerfile文件的后方执行。通过对docker image build命令加入–nocache=true参数可以强制忽略对缓存的使用。

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

(2)合并镜像

  合并镜像并非一个最佳实践,因为这种方式利弊参半。总体来说,Docker会遵循正常的方式构建镜像,但之后会增加一个额外的步骤,将所有的内容合并到一个镜像层中。当镜像中层数太多时,合并是一个不错的优化方式。例如,当创建一个新的基础镜像,以便基于它来构建其他镜像的时候,这个基础镜像就最好被合并为一层。缺点是,合并的镜像将无法共享镜像层。这会导致存储空间的低效利用,而且push和pull操作的镜像体积更大。

  执行docker image build命令时,可以通过增加–squash参数来创建一个合并的镜像。

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

合并的与不合并的镜像

(3)使用no-install-recommends

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

(4)不要安装MSI包(Windows)

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


1.3 命令

  • docker image build命令会读取Dockerfile,并将应用程序容器化。使用-t参数为镜像打标签,使用-f参数指定Dockerfile的路径和名称,使用-f参数可以指定位于任意路径下的任意名称的Dockerfile。构建上下文是指应用文件存放的位置,可能是本地Docker主机上的一个目录或一个远程的Git库。
  • Dockerfile中的FROM指令用于指定要构建的镜像的基础镜像。它通常是Dockerfile中的第一条指令。
  • Dockerfile中的RUN指令用于在镜像中执行命令,这会创建新的镜像层。每个RUN指令创建一个新的镜像层。
  • Dockerfile中的COPY指令用于将文件作为一个新的层添加到镜像中。通常使用COPY指令将应用代码赋值到镜像中。
  • Dockerfile中的EXPOSE指令用于记录应用所使用的网络端口。
  • Dockerfile中的ENTRYPOINT指令用于指定镜像以容器方式启动后默认运行的程序。
  • 其他的Dockerfile指令还有LABEL、ENV、ONBUILD、HEALTHCHECK、CMD等。

参考博客和文章书籍等:

《深入浅出Docker》

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容