一、应用的容器化简介¶
容器是为应用而生!容器能够简化应用的构建、部署和运行过程。一旦应用容器化完成(即应用被打包为一个Docker镜像),就能以镜像的形式交付并以容器的方式运行。
完整的应用容器化过程主要分为以下四个步骤 1、编写应用代码 2、创建一个Dockerfile,其中包括当前应用的描述、依赖以及该如何运行这个应用 3、对该Dockerfile执行docker image build命令 4、等待Docker将应用程序构建到Docker镜像中
二、应用的容器化详解¶
2.1 单体应用容器化¶
下面展示如何将一个简单的单节点Node.js Web应用容器化。其中分为以下过程: 1、获取应用代码 2、分析Dockerfile 3、构建应用镜像 4、运行该应用 5、测试该应用 6、容器应用化细节 7、生产环境中的多阶段构建 8、最佳实践
2.1.1 获取应用代码¶
1、执行apt install git下载git。
root@zq-virtual-machine:/home/zq/Desktop# apt install git
2、执行git clone https://github.com/nigelpoulton/psweb.git命令获取应用代码。
root@zq-virtual-machine:/home/zq/Desktop# git clone https://github.com/nigelpoulton/psweb.git
Cloning into 'psweb'...
remote: Enumerating objects: 63, done.
remote: Counting objects: 100% (34/34), done.
remote: Compressing objects: 100% (22/22), done.
remote: Total 63 (delta 13), reused 25 (delta 9), pack-reused 29
Unpacking objects: 100% (63/63), 13.27 KiB | 261.00 KiB/s, done.
3、执行cd psweb/和ls命令进入下载的文件夹并查看文件夹中文件。
root@zq-virtual-machine:/home/zq/Desktop# cd psweb/
root@zq-virtual-machine:/home/zq/Desktop/psweb# ls
app.js circle.yml Dockerfile package.json README.md test views
2.1.2 分析Dockerfile¶
1、执行cat Dockerfile命令查看文件中内容。
root@zq-virtual-machine:/home/zq/Desktop/psweb# cat Dockerfile
# 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 npm curl
# Copy app to /src
COPY . /src
WORKDIR /src
# Install dependencies
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
2、Dockerfile主要用途如下: (1)对当前应用的描述 (2)指导Docker完成应用的容器化(创建一个包含当前应用的镜像) 3、分析Dockerfile每一步内容 第一步 (1)FROM alpine 用于指定镜像,会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。 (2)LABEL maintainer="nigelpoulton@hotmail.com" 通过标签的方式指定维护者为"nigelpoulton@hotmail.com"
第二步 (1)RUN apk add --update nodejs npm curl 使用alpine的apk包管理器将node.js和nodejs-npm安装到当前镜像之中。RUN指令会在FROM指定的alpine基础镜像之上,新建一个镜像层来存储这些安装内容。
第三步 (1)COPY . /src 将应用相关文件从构建上下文复制到了当前镜像中,并且新建了一个镜像层来存储。 (2)WORKDIR /src 为Dockerfile中尚未执行的指令设置工作目录。该目录与镜像相关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层。
第四步 (1)RUN npm install 根据package.json中的配置信息,使用npm来安装当前应用的相关依赖包。npm命令会在前文设置的工作目录中执行,并在镜像中新建镜像层来保存相应的依赖文件。 (2)EXPOSE 8080 Dockerfile中通过EXPOSE 8080指令来完成相应端口的设置。这个配置信息会作为镜像的元数据保存下来,但不会产生新的镜像层。 (3)ENTRYPOINT ["node", "./app.js"] 指定当前镜像的入口程序,也是通过镜像元数据形式保存下来,而不是新增镜像层。
2.1.3 构建应用镜像¶
1、在Dockerfile所在文件夹视图执行docker image build -t web:latest . 命令构建生成一个名为web:latest的镜像,其中命令最后的点代表Docker在进行构建的时候,使用当前目录作为构建上下文。
zq@zq-virtual-machine:~/Desktop/psweb$ docker image build -t web:latest .
2、执行docker image ls命令查看镜像是否下载成功。这里观察到,镜像已成功下载。
zq@zq-virtual-machine:~/Desktop/psweb$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
web latest 683c55e54235 2 minutes ago 87MB
3、可以执行docker image inspect web:latest命令列出Dockerfile中设置的所有配置项。用于确认刚刚构建的镜像配置是否正确。
zq@zq-virtual-machine:~/Desktop/psweb$ docker image inspect web:latest
[
{
"Id": "sha256:683c55e54235a078596938ab758dbe0ae807bc8ad540b1e4690087e276bacad4",
"RepoTags": [
"web:latest"
],
"RepoDigests": [],
"Parent": "sha256:4f4a2d392749af2db86d113d7e89be7f5189522e413615fb5c7ea9735a639902",
"Comment": "",
"Created": "2022-10-05T04:23:59.310976304Z",
"Container": "d24de2e89e6aa9ef5b9a05d77689747432d662058e2cfdf7e345dd21b125876a",
"ContainerConfig": {
"Hostname": "d24de2e89e6a",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"8080/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"ENTRYPOINT [\"node\" \"./app.js\"]"
],
"Image": "sha256:4f4a2d392749af2db86d113d7e89be7f5189522e413615fb5c7ea9735a639902",
"Volumes": null,
"WorkingDir": "/src",
"Entrypoint": [
"node",
"./app.js"
],
"OnBuild": null,
"Labels": {
"maintainer": "nigelpoulton@hotmail.com"
}
},
"DockerVersion": "20.10.18",
"Author": "",
"Config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"8080/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": null,
"Image": "sha256:4f4a2d392749af2db86d113d7e89be7f5189522e413615fb5c7ea9735a639902",
"Volumes": null,
"WorkingDir": "/src",
"Entrypoint": [
"node",
"./app.js"
],
"OnBuild": null,
"Labels": {
"maintainer": "nigelpoulton@hotmail.com"
}
},
"Architecture": "amd64",
"Os": "linux",
"Size": 86962167,
"VirtualSize": 86962167,
"GraphDriver": {
"Data": {
"DeviceId": "38",
"DeviceName": "docker-8:5-1065834-ff04e6302d5734fc9fe845bf5f1b4f9913908f10ddd8539d32b7284be56dfb29",
"DeviceSize": "10737418240"
},
"Name": "devicemapper"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7",
"sha256:e0eb8cd31e9fecefdcac3e106befe3e9df63c81eeb411f63b21370ec6481d650",
"sha256:7b222b64804f8b652a68be574362a4c3e04e9442e372cc888c0efb0625a3eaf1",
"sha256:32afb898c2967a5c449cd31906da7bd76a37bb859e7f734384415e403ea554c7"
]
},
"Metadata": {
"LastTagTime": "2022-10-05T12:23:59.41148434+08:00"
}
}
]
2.1.4 推送镜像到仓库¶
1、点击Docker Hub官网,进行注册。 2、注册完成后,执行docker login命令登录Docker Hub,这里观察到,登录成功。
zq@zq-virtual-machine:~/Desktop/psweb$ docker login
Username: jeckjohn
Password:
WARNING! Your password will be stored unencrypted in /home/zq/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
3、执行docker image tag web:latest jeckjohn/web:latest命令为当前镜像重新打一个标签。之所以重打标签是因为jeckjohn用户没有web这个镜像仓的访问权限,需要尝试推送到jeckjohn这个二级空间之下。
zq@zq-virtual-machine:~/Desktop/psweb$ docker image tag web:latest jeckjohn/web:latest
4、继续执行docker image ls命令查看标签重打是否成功。这里观察到,标签重打成功。
zq@zq-virtual-machine:~/Desktop/psweb$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
jeckjohn/web latest 683c55e54235 37 minutes ago 87MB
web latest 683c55e54235 37 minutes ago 87MB
5、继续执行docker image push jeckjohn/web:latest命令将该镜像推送到Docker Hub上。
zq@zq-virtual-machine:~/Desktop/psweb$ docker image push jeckjohn/web:latest
The push refers to repository [docker.io/jeckjohn/web]
32afb898c296: Pushed
7b222b64804f: Pushed
e0eb8cd31e9f: Pushed
994393dc58e7: Mounted from library/alpine
latest: digest: sha256:0d02bf3f3a6a16c98235f3d1e6d3e2501f34665e009dea3b0fd69cfb592a519d size: 1161
6、点击Docker Hub官网,进行登录,查看刚刚是否上传成功。

2.1.5 运行应用程序¶
1、执行docker container run -d --name c1 -p 8080:8080 web:latest命令基于web:latest镜像,启动一个名为c1的容器。并且该容器将内部的8080端口与Docker主机(就是你的电脑)的80端口进行映射。其中-p 8080:8080参数将容器内部程序的8080端口映射到主机的8080端口;-d参数是让应用程序以守护进程的方式在后台运行。这里观察到,80端口已经成功映射到8080之上,并且任意外部主机(0.0.0.0:8080)均可以通过8080端口访问该容器。
root@zq-virtual-machine:/home/zq/Desktop/psweb# docker container run -d --name c1 -p 8080:8080 web:latest
64e68cf57c84855e1834b6ae06c0ef01fd44a1b084624be35ae1e2f212fb09ee
2、执行docker container ls命令验证程序是否成功运行。这里观察到,程序已成功运行。
root@zq-virtual-machine:/home/zq/Desktop/psweb# docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
64e68cf57c84 web:latest "node ./app.js" 15 seconds ago Up 13 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp c1
2.1.6 APP测试¶
1、打开浏览器,输入【自己虚拟机IP地址:8080】访问正在运行的程序。这里观察到,可以访问到正在运行的程序。

2、如果没有出现以上界面,排查思路如下: (1)执行docker container ls命令确认容器是否已经启动并正常运行。以下为正常情况下的回显信息。
root@zq-virtual-machine:/home/zq/Desktop/psweb# docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
64e68cf57c84 web:latest "node ./app.js" 15 seconds ago Up 13 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp c1
(2)确认防火墙或其他网络安全设置没有阻止访问Docker主机的8080端口。
2.2 生产环境中的多阶段构建¶
多阶段构建方式使用一个Dockerfile,其中多个FROM指令。每一个FROM指令都是一个新的构建阶段,并且可以方便地复制之前阶段的构件。
2.2.1 获取应用代码¶
1、执行git clone https://github.com/nigelpoulton/atsea-sample-shop-app.git命令获取应用代码。
zq@zq-virtual-machine:~/Desktop$ git clone https://github.com/nigelpoulton/atsea-sample-shop-app.git
Cloning into 'atsea-sample-shop-app'...
remote: Enumerating objects: 632, done.
remote: Counting objects: 100% (92/92), done.
remote: Compressing objects: 100% (29/29), done.
remote: Total 632 (delta 69), reused 63 (delta 63), pack-reused 540
Receiving objects: 100% (632/632), 7.23 MiB | 6.89 MiB/s, done.
Resolving deltas: 100% (198/198), done.
2.2.2 分析Dockerfile¶
1、执行cat Dockerfile命令查看Dockerfile文件。
root@zq-virtual-machine:/home/zq/Desktop# cd atsea-sample-shop-app/app
root@zq-virtual-machine:/home/zq/Desktop/atsea-sample-shop-app/app# cat 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 openjdk:8-jdk-alpine
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"]
2、Dockerfile有3个FROM指令,每一个FROM指令构成一个单独的构建阶段,各个阶段在内部从0开始编号: (1)阶段0叫做storefront storefront阶段拉取了大小超过600M的node:latest镜像,设置了工作目录,复制一些应用代码进行;使用2个RUN指令来执行npm操作。会生成3个镜像层并显著增加镜像大小。 (2)阶段1叫做appserver appserver阶段拉取了大小超过700M的maven:latest镜像,然后通过2个COPY指令和2个RUN指令生成了4个镜像层。 (3)阶段2叫做openjdk:8-jdk-alpine 此阶段拉取了openjdk:8-jdk-alpine镜像,约150M;然后创建一个用户,设置工作目录,从storefront阶段生成的镜像中复制一些应用代码过来;再设置一个不同的工作目录,然后从appserver阶段生成的镜像中复制应用相关的代码。最后,production设置当前应用程序为容器启动时的主程序。
2.2.3 构建应用镜像¶
1、进入在Dockerfile所在文件夹
root@zq-virtual-machine:/home/zq/Desktop# cd atsea-sample-shop-app/app/
2、执行docker image build -t muti:stage . 命令构建生成一个名为web:latest的镜像,其中命令最后的点代表Docker在进行构建的时候,使用当前目录作为构建上下文。
root@zq-virtual-machine:/home/zq/Desktop/atsea-sample-shop-app/app# docker image build -t multi:stage .
3、执行docker image ls命令查看由构建命令拉取和生成的镜像。观察到,第一行是在storefront阶段拉取的node:latest镜像,第二行是该阶段产生的镜像;第三行和第四行是appserver阶段拉取和生成的镜像;第五行和第六行是openjdk:8-jdk-alpine拉取和生成的镜像,它是其中最小的。
root@zq-virtual-machine:/home/zq/Desktop/atsea-sample-shop-app/app# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
node latest 9d209f347d70 6 days ago 991MB
<none> <none> e12fb5d3ce7d 53 minutes ago 1.16GB
maven latest 7f8c1fcb5106 4 weeks ago 535MB
<none> <none> 8648b8bf5e95 13 minutes ago 676MB
openjdk 8-jdk-alpine a3562aa0b991 3 years ago 105MB
multi stage d13973206dc4 13 minutes ago 170MB
2.3 最佳实践¶
2.3.1 利用构建缓存¶
Docker的构建过程利用了缓存机制。docker image build命令会从顶层开始解析Dockerfile中的指令并逐行执行。Docker针对每条指令都会检查缓存中是否已经有与该指令对应的镜像层。如果有,即为缓存命中,并会使用这个镜像;如果没有,则缓存未命中,Docker会基于该指令构建新的镜像层。缓存命中能够显著加快构建过程。
下面通过实例进行演示
FROM alpine
RUN apk add --update nodejs npm curl
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
1、FROM alpine指令告诉Docker使用alpine:latest作为基础镜像。如果主机中已经存在这个镜像,那么构建时会直接跳到下一条指令;如果镜像不存在,则会从Dock Hub拉取。 2、RUN apk add --update nodejs npm curl指令执行后,Docker会检查构建缓存中是否存在同一基础镜像,并且执行了相同指令的镜像层。在这里,Docker会检查缓存中是否存在一个基于alpine:latest镜像并执行了RUN apk add --update nodejs npm curl指令构建得到的镜像层。如果找到该镜像层,Docker会跳过此指令,并链接到这个已经存在的镜像层,然后继续构建;如果无法找到符合要求的镜像层,则设置缓存无效并构建该镜像层。 3、COPY . /src指令会复制一些代码到镜像中(COPY./src),因为上一条指令命中缓存,Docker会继续查找是否有一个缓存的镜像层也是基于AAA层并执行了COPY . /src命令。如果有,Docker会链接到这个缓存的镜像层并继续执行后续指令;如果没有,则构建镜像层,并对后续的构建操作设置缓存无效。
总结: 1、一旦有指令在缓存中未命中(没有该指令对应的镜像层),则后续的整个构建过程将不再使用缓存。在编写Dockerfile时,尽量将易于发生变化的指令置于Dockfile后方执行。 2、COPY和ADD指令会检查复制到镜像中的内容自上次构建之后是否发生了变化。若变化,则会构建新的镜像层。
2.3.2 合并镜像¶
当镜像中层数太多,可以选择合并。在使用docker image push命令发送镜像到Docker Hub时,合并的镜像需要发送全部字节,不合并的镜像只需要发送不同的镜像层即可。
1、执行docker image build ubuntu:latest--squash命令创建一个合并的镜像。
root@zq-virtual-machine:~# docker image build ubuntu:latest--squash
2.3.3 使用no-install-recommends¶
在构建Linux镜像时,若使用的是APT包管理器,则应该执行apt-get install命令时增加no-install-recommends参数。确保APT仅安装核心依赖包,而不是推荐和建议的包。
2.3.4 不要安装MSI包(Windows)¶
在构建Windows镜像时,尽量避免使用MSI包管理器。因为其对空间利用率不高,会大大增加镜像的体积。