乐趣区

Docker-实战笔记多阶段构建镜像

生产环境的多阶段构建

在生产环境中,使用 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 文件,分析如下:

    • 阶段0build-env

      build-env阶段拉取了 aspnetcore-build:2.0 作为基础镜像,然后设置了工作目录,复制一些应用代码,接着执行两个 RUN 指令,生成 1 个镜像层并显著得到一个比原镜像大得多的镜像,包含许多构建工具和应用代码。

    • 阶段1microsoft/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 文件的小技巧:尽量将易于发生变化的指令置于文件的后方执行,让缓存未命中的情况一直持续到最后才出现,从而避免了退出缓存检查的机制对其他指令造成的影响。

      COPYADD 指令会检查复制到镜像中的内容自上一次构建之后是否发生了变化,我们无法得知被复制目录和复制后目录是否发生变化,Docker提供了一个解决这个问题的办法:Docker会计算每一个被复制文件的校验和 Checksum(相当于散列值,用于身份验证),并与缓存镜像层中同一文件的Checksum 进行对比。如果不匹配,那么就认为 该缓存无效并重新构建新的镜像层

    • 合并镜像

      可以通过执行 docker image build 命令,传入 --squash 参数来创建一个合并镜像层

      当镜像层数太多的时候,使用合并镜像层的方法是一个不错的优化方法,但它也不是万金油,并非适用于所有场景,但它的缺点也很明显,即:合并的镜像将无法共享镜像层,导致存储空间低效利用,而且 pushpull操作的镜像体积更大。

    • 使用 no-install-recommends

      在构建 Linux 镜像时,若使用的是 APT 包管理器,则应该在执行 apt-get install 命令后传入 no-install-recommends 参数,以确保仅安装核心依赖包,而不是推荐和建议的软件包,这样能够显著减少镜像的体积和不必要的包下载数量。

    • <s> 不要安装 MSI 包(Windows 环境下)</s>
退出移动版