Docker的核心思想就是如何将应用整合到容器中,并且能在容器中实际运行。将应用整合到容器中并且运行起来的这个过程,称为“容器化”(Containerizing),有时也叫做“Docker化”(Dockerizing)。
8.1 应用的容器化——简介
容器就是为应用而生!具体来说,容器能够简化应用的构建、部署和运行过程。
完整的应用容器化过程主要分为以下几个步骤:
- 编写应用代码
- 创建一个Dockerfile,其中包括当前应用的描述、依赖以及该如何运行这个应用。
- 对该Dockerfile执行docker image build 命令。
- 等待Docker 将应用程序构建到Docker镜像中。
一旦应用容器化完成,就能以镜像的形式交付并以容器的方式运行了。
8.2 应用的容器化——详解
8.2.1 单体应用容器化
下面演示如何将一个简单的单节点Node.js Web应用容器化,一共有8个步骤:
获取应用代码——>分析Dockerfile——>构建应用镜像——>运行该应用——>测试应用——>容器应用化细节——>生产环境中多阶段构建——>最佳实践
- 获取应用代码
## 使用git 去下载github上的代码
git clone https://github.com/nigelpoulton/psweb.git
- 分析Dockerfile
在代码目录中,有个名称为Dockerfile的文件。这个文件包含了对当前应用的描述,并且能知道Docker完成镜像的构建。在Docker当中,包含应用文件的目录通常被称为构建上下文(Build Context)。通常将Dockerfile放到构建上下文的根目录下。
另外,文件开头字母是大写的D,这里是一个单词。像dockerfile 或者 Docker file 这种写法是不允许的。下面我们看下Dockerfile里面的文件内容吧。
FROM alpine
LABEL maintainer="nigelpoulton@hotmail.com"
RUN apk add --update nodejs nodejs-npm
COPY . /src
WORKDIR /src
RUN npm install
EXPOSE 8080
ENTRYPOINT ["node", "./app.js"]
Dockerfile主要有两个用途:1是对当前应用的描述,2是知道Docker完成应用的容器化。Dockerfile文件非常重要,它能实现开发和部署两个过程的无缝切合。同时Dockerfile还能帮助新手快速熟悉这个项目,所以必须要理解Dockerfile。下面是该Dockerfile文件的一些步骤描述。
首先使用alpine镜像作为基础镜像,指定维护者为 "nigelpoulton@hotmail.com" 。然后安装node.js和NPM,将应用从当前目录复制到镜像当中,并且配置工作目录为 /src 。最后安装依赖包,记录应用的网络端口,然后把app.js设置为默认运行的应用。
下面对每一步进行一个详解。
每个Dockerfile文件的第一行都是FROM指令。FROM指令指定的镜像,会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。这里我们使用alpine作为一个基础的镜像。
接下来,Dockerfile中通过标签(LABEL)方式指定当前镜像的维护者是谁。每个标签其实是一个键值对,在一个镜像当中可以通过增加标签的方式为镜像添加自定义的元数据。
RUN apk add --update nodejs nodejs-npm
指定使用alpine的apk包管理器将nodejs和nodejs-npm安装到当前镜像中。RUN指令会在FROM指定alpine基础镜像之上,新建一个镜像层来存储这些安装内容。这时候一共有了两个镜像层。
COPY . /src
指令将应用相关文件从构建上下文复制到当前镜像中,并且新建一个镜像层来存储。COPY执行结束后,当前镜像有3层了。
下一步,Dockerfile通过WORKDIR指令,为Dockerfile中尚未执行的指令设置工作目录。该目录也镜像相关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层。
然后 RUN npm install
指令会根据package.json中的配置信息,使用npm来安装当前应用的相关依赖包。npm命令会在前文设置的工作目录下执行,并且在镜像中新建镜像层来保存相应的依赖文件。目前镜像一共包含了4层。
因为当前应用需要通过8080端口对外提供一个web服务,所以Dockerfile中通过 EXPOSE 8080
指令来完成相应端口的设置。这个配置信息会作为镜像的元数据被保存下来,并不会产生新的镜像层。
最后,通过ENTRYPOINT 指令来指定当前应用程序的入口程序。ENTRYPOINT指定的二配置信息也是通过镜像元数据的形式保存下来,而不是新增镜像层。
- 容器化当前应用/构建具体的镜像
我们使用Docker image build 来构建一个镜像,下面的命令表示使用当前目录作为构建上下文去构建一个名为web:latest的镜像。
docker image build -t web:latest .
命令执行完之后,我们可以通过docker image ls去查看刚刚构建的镜像,或者可以通过docker image inspect web:latest
来确认刚刚构建的镜像。
- 推送镜像到仓库
镜像构建好之后,这时候镜像只存在你的本地Docker主机上,接下来要推送到镜像仓库上。比如,我们可以推送到Docker Hub上,首先你要有Docker Hub的账号,然后在Docker 主机上登录该账号。
docker login
这里要注意的是在推送之前想要给自己的镜像打标签,原因是我们并没有docker.io的访问权限,只能推送到我们自己的二级命名空间中(比如我的pangcm)。我们使用docker image tag来打标签。
docker image tag web:latest pangcm/web:latest
打好标签之后我们就可以把镜像推送到Docker Hub中去了
docker image push pangcm/web:latest
- 运行应用程序
这里我们启动这个镜像,命名为c1,并且使用Docker主机的80端口进行映射。
docker container run -d --name c1 -p 80:8080 web:latest
6.APP测试
打开浏览器,输入Docker 主机的IP地址就可以访问到这个应用程序了。
- 详述
我们可以使用docker image history来查看在构建镜像的过程中都执行了哪些指令。
[pangcm@docker01 psweb]$ docker image history web:latest
IMAGE CREATED CREATED BY SIZE COMMENT
d36f8bbd5ccf 10 minutes ago /bin/sh -c #(nop) ENTRYPOINT ["node" "./app… 0B
ee60c0c4e5e7 10 minutes ago /bin/sh -c #(nop) EXPOSE 8080 0B
e51298cd61da 10 minutes ago /bin/sh -c npm install 20.6MB
163ad5f277a7 11 minutes ago /bin/sh -c #(nop) WORKDIR /src 0B
4a6bd98a85bc 11 minutes ago /bin/sh -c #(nop) COPY dir:270a2b97bd349a826… 22kB
e578b1e719d7 11 minutes ago /bin/sh -c apk add --update nodejs nodejs-npm 45.3MB
370ef133d092 19 minutes ago /bin/sh -c #(nop) LABEL maintainer=nigelpou… 0B
961769676411 3 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 3 months ago /bin/sh -c #(nop) ADD file:fe64057fbb83dccb9… 5.58MB
显示的顺序应该由下而上去查看,这里看到一共构建了4层镜像,我们可以通过docker image inspect 去确认。
8.2.2 生产环境中的多阶段构建
对于Docker镜像来说,过大的体积并不好。越大则越慢,这意味着更难使用,而且可能更加脆弱,更容易遭受攻击。
因此,Docker镜像应该尽量小。对于生产环境的镜像来说,目标是将其缩小到仅包含运行应用所必需的内容即可。问题在于,生成较小的镜像并非易事。
不同Dockerfile写法会对镜像的大小产生不同的影响。例如,每个RUN指令都会新增一个镜像层。因此,通过使用 && 连接多个命令以及使用反斜杠换行的方法,将多个命令包含在一个RUN指令中,这是一种值得提倡的做法。此外,在执行完RUn指令之后,应该要清理一些临时文件以及工具。这些中间文件都不应该出现在生产环境中的。
有多种办法可以改善上面提到的问题,比如使用建造者模式。使用建造者模式需要至少两个Dockerfile,一个用于开发环境一个用于生产环境,构建难度比较大。或者可以使用多阶段构建的办法,只需要一个Dockerfile,使用起来更加简单。
下面是使用多阶段构建方式的一个示例,只有一个Dockerfile,其中有多个FROM指令。
下载该示例项目
git clone https://github.com/nigelpoulton/atsea-sample-shop-app.git
##进入到app目录下,Dockerfile文件在这里
cd atsea-sample-shop-app/app
查看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
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指令和两个RUN生成了4个镜像层。这个阶段同样会构建出一个非常大的包含许多构建工具和非常少量应用程序代码的镜像。
production阶段拉取了 java:8-jdk-alpine镜像,这个镜像大约150MB,明显小于前两个构建阶段用到的node和maven镜像。这个阶段会创建一个用户,设置工作目录,从storefront阶段生成的镜像中复制一些应用程序代码过来。之后,设置一个不同的工作目录,然后从appserver阶段生成的镜像复制应用相关的代码。最后,production设置当前应用程序为容器启动时的主程序。
重点在于COPY --from 指令,它从之前的阶段构建的镜像中仅复制生产环境相关的应用代码,而不会复制生产环境不需要的构件。
还有一点也很重要,多阶段构建这种方式仅用到一个Dockerfile,并且docker image build命令不需要增加额外参数。
接下来,我们进入app目录,然后执行构建容器的命令吧。
cd atsea-sample-shop-app/app
docker image build -t multi:stage
构建完成后可以通过docker image ls 来查看构建命令拉取和生成的镜像。
[pangcm@docker01 ~]$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
multi stage ff8bd7176247 5 weeks ago 210MB
node latest a8d7efbae951 6 weeks ago 908MB
maven latest e941463218b9 2 months ago 616MB
java 8-jdk-alpine 3fd9dd82815c 2 years ago 145MB
由于我这里清除了一些悬虚镜像,所以显示的镜像会少了点,只有3个下载的镜像和最后构建的镜像。实际上还应该有3个中间的镜像的,也就是我们中途构建使用到的镜像。
多阶段构建是随着Docker 17.05 版本新增的一个特性,用于构建精简的生产环境镜像。
8.2.3 最佳实践
- 利用构建缓存
Docker的构建过程利用了缓存机制。验证的办法很简单,重新构建一次上面构建的镜像,肯定是瞬间完成。在做构建的时候,Docker会检查缓存的情况,如果缓存有需要下载的镜像那就不下载了,有了已经构建好的镜像层,那也可以跳过这一步,直接到与缓存不一样的构建命令上。
所以,为了提高构建的效率,我们通常把经常变化的部分放到了构建最后面。如果你不想使用缓存可以在构建命令后面加入 --nocache=true。
- 合并镜像
当要构建镜像的层次很多的时候,我们可以考虑把这些镜像层给合并。但是合并的镜像不能被共享,这会导致了存储空间的浪费,因为你是没办法使用这个大的镜像层的。
要合并镜像,可以在 docker image build 的时候加上参数 --squash 即可。
- 使用 no-install-recommends
在构建Linux镜像时,若使用的是APT包管理器,则应该在执行 apt-get install 命令时增加 no-install-recomments 参数。这样能确保APT仅安装核心包,而不是推荐和建议包。这样能够显著减少不必要包的下载数量。
8.3 应用的容器化——命令
- docker image build 命令会读取 Dockerfile,并将应用程序容器化。使用 -t 参数给镜像打标签,使用 -f 参数指定Dockerfile的路径和名称。构建上下文是指文件存放的位置,可能是本地Docker主机上的一个目录或者远程的Git库。
- Dockerfile 中的FROM 指令用于指定要构建的镜像的基础镜像。
- Dockerfile 中的RUN指令用于在镜像中执行命令,这会构建新的镜像层。
- Dockerfile 中的COPY指令用于将文件作为一个新的层添加到镜像中。
- Dockerfile 中的EXPOSE指令用于记录应用所使用的网络端口。
- Dockerfile 中的ENTRYPOINT指令用于指定镜像以容器方式启动后默认运行的程序。
- 其他的Dockerfile指令还有LABEL、ENV、ONBUILD、HEALTHCHECK、CMD等
8.4 本章小结
本章介绍了如何容器化一个应用。首先从远程Git库拉取一些应用代码,库中除了应用代码,还包括Dockerfile,后者包括了一系列指令,用于定义如何将应用构建为一个镜像。然后介绍了Dockerfile基本的工作机制,并用docker image build 命令创建了一个新的镜像。
镜像创建后,基于该镜像启动了容器,并借助Web浏览器对其进行了测试。接下来,读者可以了解多阶段构建提供了一个简单的方式,能够构建更加精简的生产环境镜像。
读者从本章中还可以了解到Dockerfile是一个将应用程序文档化的有力工具。正因如此,他能够帮助新加入的开发人员迅速进入状态,能够为开发人员和运维人员弥合分歧。处于这种考虑,请将其视为代码,并用源控制系统进行管理。
网友评论