8.2.2 生产环境中的多阶段构建
对于Docker镜像来说,过大的体积并不好!
越大则越慢,这就意味着更难使用,而且可能更加脆弱,更容易遭受攻击。
鉴于此,Docker镜像应该尽量小。对于生产环境镜像来说,目标是将其缩小到仅包含运行应用所 必需 的内容即可。问题在于,生成较小的镜像并非易事。
例如,不同的Dockerfile写法就会对镜像的大小产生显著影响。常见的例子是,每一个RUN指令会新增一个镜像层。因此,通过使用 &&
连接多个命令以及使用反斜杠( \
)换行的方法,将多个命令包含在一个RUN指令中,通常来说是一种值得提倡的方式。这并不难掌握,多加练习即可。
另一个问题是开发者通常不会在构建完成后进行清理。当使用 RUN
执行一个命令时,可能会拉取一些构建工具,这些工具会留在镜像中移交至生产环境。这是不合适的!
有多种方式来改善这一问题——比如常见的是采用建造者模式(Builder Pattern)。但无论采用哪种方式,通常都需要额外的培训,并且会增加构建的复杂度。
建造者模式需要至少两个Dockerfile—— 一个用于开发环境,一个用于生产环境。首先需要编写Dockerfile.dev,它基于一个大型基础镜像(Base Image),拉取所需的构建工具,并构建应用。接下来,需要基于Dockerfile.dev构建一个镜像,并用这个镜像创建一个容器。这时再编写Dockerfile.prod,它基于一个较小的基础镜像开始构建,并从刚才创建的容器中将应用程序相关的部分复制过来。整个过程需要编写额外的脚本才能串联起来。
这种方式是可行的,但是比较复杂。
多阶段构建(Multi-Stage Build)是一种更好的方式!
多阶段构建能够在不增加复杂性的情况下优化构建过程。
下面介绍一下多阶段构建方式。
多阶段构建方式使用一个Dockerfile,其中包含多个 FROM
指令。每一个 FROM
指令都是一个新的 构建阶段(Build Stage) ,并且可以方便地复制之前阶段的构件。
示例源码可从我的GitHub主页中atsea-sample-shopapp仓库获得, Dockerfile
位于 app
目录。这是一个基于Linux系统的应用,因此只能运行在Linux容器环境上。
这个代码库是从 dockersamples/atsea-sample-shop-app
fork过来的,以防上游代码库被删除。
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 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。
$ 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
执行构建(这可能会花费几分钟)。
$ 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
注:
示例中
multi:stage
标签是自行定义的,读者可以根据自己的需要和规范来指定标签名称。不过并不要求一定必须为多阶段构建指定标签。
执行 docker image ls
命令查看由构建命令拉取和生成的镜像。
$ 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
命令,就创建了一个精简的生产环境镜像。
多阶段构建是随Docker 17.05版本新增的一个特性,用于构建精简的生产环境镜像。