生产环境的多阶段构建
在生产环境中,使用 Dockerfile
文件构建的镜像应该尽量小,只需要将其缩小到仅包含运行应用所必需的内容即可。不同的 Dockerfile
写法也会对镜像的大小产生显著的影响,例如,使用 RUN
指令会新增一个镜像层,如果出现多个这样的指令,镜像层数将会变得很臃肿;因此,通过使用 &&
连接多个命令以及使用反斜杠 \
换行的方法,将多个命令包含在一个 RUN
指令中,就可以有效地利用镜像层。
构建镜像完成后的清理工作同样也是尤为重要的!例如,当 RUN
指令执行完毕后,会残留一些构建镜像完成后的文件,这些文件会随着镜像移交至生产环境而存在,无论从哪方面来说,这是极为不妥的!Docker 提供了多种方式来解决这一问题,传统的做法是采用 建造者模式。
建造者模式(Builder Pattern)
建造者模式至少需要两个 Dockerfile
文件:一个用于开发环境(.dev),另一个用于生产环境(.prod),需要注意的是:整个过程需要 编写额外的脚本 才能串联起来。
- 首先,编写
Dockerfile.dev
,它基于一个大型的基础镜像,拉取所需的构建工具并构建应用。 - 然后,基于
Dockerfile.dev
作为基础镜像,构建一个新的镜像层,再使用新的镜像层创建并启动容器。 - 接着,编写
Dockerfile.prod
,它基于一个较小的基础镜像,并从上一步创建的容器将应用程序的相关文件和代码复制过来。
多阶段构建方式
多阶段构建是 Docker 17.05
版本新增的一个特性,用于构建精简的生产环境!
多阶段构建方式使用一个 Dockerfile
,其中包含多个FROM
指令,每一个 FROM
指令都是一个新的 构建阶段(Build Stage),并且可以方便地复制之前阶段的构件。多阶段构建的优点是 能够在不增加复杂性的情况下优化构建过程,而且不需要编写额外的脚本就能完成构建。
-
拉取源码
$ git clone https://github.com/nigelpoulton/dotnet-docker-samples.git $ cd dotnet-docker-samples/aspnetapp/ $ ls #查找 Dockerfile appsettings.Development.json aspnetapp.csproj bundleconfig.json Dockerfile README.md Views appsettings.json bower.json Controllers Program.cs Startup.cs wwwroot
-
分析 Dockerfile 文件
FROM microsoft/aspnetcore-build:2.0 AS build-env WORKDIR /app # copy csproj and restore as distinct layers COPY *.csproj ./ RUN dotnet restore # copy everything else and build COPY . ./ RUN dotnet publish -c Release -o out # build runtime image FROM microsoft/aspnetcore:2.0 WORKDIR /app COPY --from=build-env /app/out . ENTRYPOINT ["dotnet", "aspnetapp.dll"]
首先注意到,
Dockerfile
有三个FROM
指令。每一个FROM
指令构成一个单独的 构建阶段 。每个构建阶段在内部从0
开始编号,多阶段构建方式只用到了一个Dockerfile
文件,分析如下:- 阶段
0
:build-env
build-env
阶段拉取了aspnetcore-build:2.0
作为基础镜像,然后设置了工作目录,复制一些应用代码,接着执行两个RUN
指令,生成1
个镜像层并显著得到一个比原镜像大得多的镜像,包含许多构建工具和应用代码。 - 阶段
1
:microsoft/aspnetcore:2.0
aspnetcore:2.0
阶段拉取了aspnetcore:2.0
作为基础镜像,设置工作目录,然后执行COPY --from
指令从build-env
阶段生成的镜像中复制一些应用代码过来,最后执行ENTRYPOINT
指令指定容器的默认应用程序。
- 阶段
上述构建过程的重点在于 COPY --from
指令 表示从之前的构建阶段复制生产环境相关的应用代码,而不会复制生产环境不需要的构件。
-
执行构建
$ docker image build -t multi:stage . #自定义标签,注意最后的点不能省略 ... Removing intermediate container 69bd8970d56c ---> e17d0c874151 Successfully built e17d0c874151 Successfully tagged test:stage $ docker image ls #查看镜像 REPOSITORY TAG IMAGE ID CREATED SIZE test stage e17d0c874151 3 minutes ago 350MB <none> <none> 91c0355d00e2 30 minutes ago 2.06GB <none> <none> feb5ba879e46 23 hours ago 5.55MB ubuntu latest 775349758637 4 weeks ago 64.2MB alpine latest 965ea09ff2eb 5 weeks ago 5.55MB microsoft/aspnetcore-build 2.0 06a6525397c2 15 months ago 2.02GB microsoft/aspnetcore 2.0 db030c19e94b 15 months ago 347MB
输出内容两行,显示在
build-env
阶段拉取的aspnetcore-build
镜像,以及microsoft/aspnetcore:2.0
阶段拉取的aspnetcore
镜像。这两个镜像都包含着许多构建工具,因此镜像体积十分大。第
2~5
行则是在两个构建阶段中拉取和生成的镜像,它们也因为包含着许多构建工具而导致镜像体积较大。第
1
行是Dockerfile
中最后一个构建阶段aspnetcore:2.0
指定标签的镜像multi:stage
。可见它比之前所构建的镜像体积都十分小,因为这个镜像是基于相对精简的alpine
镜像所构建的,仅仅添加了用于生产环境的应用程序。构建镜像的优化方案
-
利用构建缓存
Docker 构建镜像利用了缓存机制,
docker image run
命令会从顶层开始解析Dockerfile
文件指令,同时还会检查缓存中是否已经有与该指令对应的镜像层。* 如果有,就是缓存命中(Cache Hit),然后使用这个镜像层;如果没有,就是缓存未命中(Cache Miss),然后就会基于指令构建新的镜像层,设置缓存无效标志,作用于本次构建的后续部分,后续部分将不会执行查找缓存这一操作。缓存命中能够加快构建过程!利用
docker image build
命令传入--nocache=true
参数可以强制忽略对缓存的检查。关于缓存未命中机制,此处提供了一个写
Dockerfile
文件的小技巧:尽量将易于发生变化的指令置于文件的后方执行,让缓存未命中的情况一直持续到最后才出现,从而避免了退出缓存检查的机制对其他指令造成的影响。COPY
和ADD
指令会检查复制到镜像中的内容自上一次构建之后是否发生了变化,我们无法得知被复制目录和复制后目录是否发生变化,Docker
提供了一个解决这个问题的办法:Docker
会计算每一个被复制文件的校验和Checksum
(相当于散列值,用于身份验证),并与缓存镜像层中同一文件的Checksum
进行对比。如果不匹配,那么就认为 该缓存无效并重新构建新的镜像层。 -
合并镜像
可以通过执行
docker image build
命令,传入--squash
参数来创建一个合并镜像层当镜像层数太多的时候,使用合并镜像层的方法是一个不错的优化方法,但它也不是万金油,并非适用于所有场景,但它的缺点也很明显,即:合并的镜像将无法共享镜像层,导致存储空间低效利用,而且
push
和pull
操作的镜像体积更大。 - 使用 no-install-recommends
在构建 Linux 镜像时,若使用的是
APT
包管理器,则应该在执行apt-get install
命令后传入no-install-recommends
参数,以确保仅安装核心依赖包,而不是推荐和建议的软件包,这样能够显著减少镜像的体积和不必要的包下载数量。 - <s> 不要安装 MSI 包(Windows 环境下)</s>
-