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
                                         91c0355d00e2        30 minutes ago      2.06GB
                                         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参数,以确保仅安装核心依赖包,而不是推荐和建议的软件包,这样能够显著减少镜像的体积和不必要的包下载数量。

    • 不要安装 MSI 包(Windows环境下)

你可能感兴趣的:(docker,容器,运维)