Docker应用的容器化

Docker的核心思想就是将应用整合到容器中,并且能够在容器中实际运行.将应用整合到容器中并且运行起来的这个过程,称为"容器化".

单体容器化过程

  1. 获取应用代码
  2. 分析Dockerfile
  3. 构建应用镜像
  4. 运行该应用
  5. 测试应用
  6. 容器应用化细节
  7. 生产环境的多阶段构建
  8. 最佳实践

获取应用代码

自主编写或者从版本控制系统中获取

分析Dockerfile

Dockerfile文件描述了当前应用,并且能指导Docker完成镜像的构建.Docker文件的结构如下.

$ cat 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的作用主要有两个

  • 对当前应用的描述
  • 指导Docker完成应用的容器化

FROM指定基础镜像层,LABEL标签指定了当前镜像的维护者为"nigelpoulton@hotmail.com",标签其实是一个键值对,在一个镜像中可以通过增加标签的方式来为镜像添加自定义元数据.

RUN apk add --update nodejs nodejs-npm 指令使用alpine的apk包管理将nodejs和nodejs-npm安装到当前镜像中.RUN 指令会在FROM指令的镜像层上新建一个镜像层来存储这些内容.也就是堆叠了一个新的镜像层.

COPY . /src 指令将应用相关的文件从context中复制到了当前镜像中,同样的也堆叠了一个新的镜像层.

下一步,WORKDIR指令为Dockerfile中尚未执行的指令指定工作目录,该目录与镜像有关,会作为元数据记录到镜像配置中,但不会创建新的镜像层.

然后RUN npm install指令会根据package.json中的配置信息,使用npm来安装当前应用的相关依赖包.npm命令会在之前设置的工作目录中执行,并且堆叠一个新的镜像层来保存相应的修改.

EXPOSE 8080指令完成相应端口的设置,这个信息保存在元数据中.

ENTRYPOINT指定镜像的入口程序,也就是运行镜像后默认执行的命令.这个信息也保存在元数据当中.

build

docker image build -t web:latest .

不要忘记最后的.,这个命令根据当前文件夹中的Dockerfile的描述来构建一个镜像,并将其标签(tag)设置为web:latest

推送到镜像仓库

docker image push

镜像仓库可以考虑

  • 公有库Docker Hub,这也是docker image push的默认地址,但先需要使用Docker ID登录Docker Hub
  • 私有仓库

镜像信息三元组

  1. Registry(镜像仓库服务,默认为 docker.io)
  2. Repository(镜像仓库,没有默认值,而是从被推送的镜像的REPOSITORY属性值获取)
  3. Tag(标签,默认为latest)

由于无限访问docker.io/web,需要推送到二级命名空间之下,这里使用了nigelpoulton这个ID

docker image tag web:latest nigelpoulton/web:latest

运行镜像 和 测试程序

生产环境中的多阶段构建

对于Docker镜像来说,过大的体积可能不好.越大则越慢,因此Docker镜像应该尽量小,对于生产环境的镜像来说,目标是将其缩小到仅包含运行应用所必需的内容即可.

可以是用建造者模式来改善这一问题,即开发和生产使用不同的Dockerfile(类似Spring中dev-properties和prod-properties).首先编写Dockerfile.dev,它基于一个大型基础镜像,拉去所需的构建工具,并构建应用,构建镜像再创建容器.再编写Dockerfile.prod,它基于一个较小的基础镜像开始构建,并且把之前创建的容器中相关的部分复制过来.整个过程需要编写额外的脚本才能串联起来.

这种方式是可行的,但是比较复杂.

多阶段构建(Multi-Stage Build)是一种更好的方式.

多阶段的构建方式使用一个Dockerfile,其中包含多个FROM指令,每一个FROM指令都是一个新的构建阶段(Build Stage).

FROM node:latest AS storefront
WORKDIR /usr/src/assea/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/vamen/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 ["jar","jar","/app/AtSea-0.0.1-SNAPSHOT.jar"]
CMD ["--spring.profiles.active=postgres"]

这里有3个阶段

  1. storefront
  2. appserver
  3. production

storefront阶段拉取了大小超过600MB的node:latest镜像,appserver阶段拉取了大小超过700MB的maven:latest镜像,production阶段拉取了大小为约150MB左右的java:8-jdk-alpine镜像.

生产阶段仅需要编译阶段的编译输出,比如这里的/usr/src/atsea/app/react-app/build/ 和 /usr/src/atsea/target/AtSea-0.0.1-SNAPSHOT.jar,而不需要编译环境,
这种多阶段构建的方式可以极大地缩小镜像的大小.

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

多阶段构建的方式仅使用了一个Dockerfile,并且build命令不需要额外的参数.

最佳实践

利用构建缓存

docker image build命令会从顶层开始解析Dockerfile中的指令并逐行执行,每行指令过程中都会检查缓存中是否已经有匹配的镜像层,如果有则为缓命中(Cache Hit),并且使用这个镜像层,如果没有,则会根据指令构建新的镜像层.缓存命中可以显著加速构建过程.

从实现角度来说,就是将通用的过程写在Dockerfile前面,并且顺序也保持一致.将发生变化的指令放在Dockerfile文件的后方.

一旦指令在缓冲中未命中,则后续整个构建过程将不再使用缓存.

docker image build --nocache=true可以强制忽略缓存.

还有一点是COPY和ADD指令会检查复制到镜像中的内容自上一次构建之后是否发生了变化.例如,Dockerfile中COPY . /src指令没有发生变化,但是被复制的目录中的内容发生了变化.

针对这一问题,Docker会计算每一个被复制文件的Checksum值,并与缓存镜像层中文件的
Checksum进行对比,如果不匹配,那么就认为缓存无效并构建新的镜像层.

合并镜像层

当镜像层太多时,合并是一个不错的优化方式,缺点是合并的镜像将无法共享镜像层.

执行docker iamge build时,可以通过增加--squash 参数来创建一个合并的镜像层.

使用 no-install-recommends

Linux环境中若包管器是APT,在执行apt install 时,增加no-install-recommends参数避免安装不需要的推荐包.

不要安装MSI包(Windows)

尽量避免使用MSI包管理器,它对空间的利用率不高,会增加镜像的体积.

用进废退,放码过来 ヽ(ー_ー)ノ