一、应用的容器化简介

容器是为应用而生!容器能够简化应用的构建、部署和运行过程。一旦应用容器化完成(即应用被打包为一个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官网,进行登录,查看刚刚是否上传成功。

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】访问正在运行的程序。这里观察到,可以访问到正在运行的程序。

APP测试验证

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包管理器。因为其对空间利用率不高,会大大增加镜像的体积。