通常我们使用docker构建镜像的步骤是这样的:
- 编写Dockerfile文件
- 执行docker build 命令
执行以上两部就可以得到构建好的镜像,这也是docker带给我们的便利。但是以上步骤背后执行了哪些操作?Dockerfile文件指令是如何被执行的?构建输出内容分别代表什么含义?本文现在就针对docker build操作专门说一说隐藏在背后的细节。
阅读本文的知识前提:
- 对docker有所了解,能够区分镜像、容器、传统虚拟机;
- 知道docker build是用于构建镜像的命令;
- 听说过镜像是分层组成的;
本文适用的阅读对象:
- 刚接触docker,对docker build背后的操作有好奇心;
- 想要搞清楚docker镜像是如何构建的;
本文以构建一个asp.net core应用镜像为例,Dockerfile文件内容如下:
FROM harbor.xxxx.top:9999/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /appd
FROM harbor.xxxx.top:9999/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["Tccc.BIZ.CMS.Srv/Tccc.BIZ.CMS.Srv.csproj", "Tccc.BIZ.CMS.Srv/"]
COPY ["Tccc.BIZ.ATMS.SPI/Tccc.BIZ.ATMS.SPI.csproj", "Tccc.BIZ.ATMS.SPI/"]
RUN dotnet restore "Tccc.BIZ.CMS.Srv/Tccc.BIZ.CMS.Srv.csproj"
COPY . .
WORKDIR "/src/Tccc.BIZ.CMS.Srv"
RUN dotnet build "Tccc.BIZ.CMS.Srv.csproj" -c Release -o /appd/build
FROM build AS publish
RUN dotnet publish "Tccc.BIZ.CMS.Srv.csproj" -c Release -o /appd/publish
FROM base AS final
WORKDIR /appd
COPY --from=publish /appd/publish .
ENTRYPOINT ["dotnet", "Tccc.BIZ.CMS.Srv.dll"]
执行命令:sudo docker build -t xxxx -f xxxx ./,构建输出内容如下:
Sending build context to Docker daemon 50.69kB
Step 1/16 : FROM harbor.xxxx.top:9999/dotnet/core/aspnet:3.1-buster-slim AS base
---> 014a41b1f39a
Step 2/16 : WORKDIR /appd
---> Running in 42f5287928dd
Removing intermediate container 42f5287928dd
---> b944a6f67809
Step 3/16 : FROM harbor.xxxx.top:9999/dotnet/core/sdk:3.1-buster AS build
---> 006ded9ddf29
Step 4/16 : WORKDIR /src
---> Using cache
---> 93f67f62562c
Step 5/16 : COPY ["Tccc.BIZ.CMS.Srv/Tccc.BIZ.CMS.Srv.csproj", "Tccc.BIZ.CMS.Srv/"]
---> Using cache
---> bc0a32ea122b
Step 6/16 : COPY ["Tccc.BIZ.ATMS.SPI/Tccc.BIZ.ATMS.SPI.csproj", "Tccc.BIZ.ATMS.SPI/"]
---> Using cache
---> 32c675319114
Step 7/16 : RUN dotnet restore "Tccc.BIZ.CMS.Srv/Tccc.BIZ.CMS.Srv.csproj"
---> Using cache
---> f2c85873c12a
Step 8/16 : COPY . .
---> Using cache
---> 005ea2bc08d0
Step 9/16 : WORKDIR "/src/Tccc.BIZ.CMS.Srv"
---> Using cache
---> 3f2d9b1f4bed
Step 10/16 : RUN dotnet build "Tccc.BIZ.CMS.Srv.csproj" -c Release -o /appd/build
---> Running in 7d8df4377351
Microsoft (R) Build Engine version 16.6.0+5ff7b0c9e for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
All projects are up-to-date for restore.
Tccc.BIZ.ATMS.SPI -> /appd/build/Tccc.BIZ.ATMS.SPI.dll
Tccc.BIZ.CMS.Srv -> /appd/build/Tccc.BIZ.CMS.Srv.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:03.60
Removing intermediate container 7d8df4377351
---> faa92e4ff2f9
Step 11/16 : FROM build AS publish
---> faa92e4ff2f9
Step 12/16 : RUN dotnet publish "Tccc.BIZ.CMS.Srv.csproj" -c Release -o /appd/publish
---> Running in 9034962126ba
Microsoft (R) Build Engine version 16.6.0+5ff7b0c9e for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
All projects are up-to-date for restore.
Tccc.BIZ.ATMS.SPI -> /src/Tccc.BIZ.ATMS.SPI/bin/Release/netstandard2.0/Tccc.BIZ.ATMS.SPI.dll
Tccc.BIZ.CMS.Srv -> /src/Tccc.BIZ.CMS.Srv/bin/Release/netcoreapp3.1/Tccc.BIZ.CMS.Srv.dll
Tccc.BIZ.CMS.Srv -> /appd/publish/
Removing intermediate container 9034962126ba
---> 8581c0bf6a5b
Step 13/16 : FROM base AS final
---> b944a6f67809
Step 14/16 : WORKDIR /appd
---> Running in 20233c270934
Removing intermediate container 20233c270934
---> 33640fdd297a
Step 15/16 : COPY --from=publish /appd/publish .
---> e79a9b48f4f7
Step 16/16 : ENTRYPOINT ["dotnet", "Tccc.BIZ.CMS.Srv.dll"]
---> Running in bb2ba15b786c
Removing intermediate container bb2ba15b786c
---> b182d5480d46
Successfully built b182d5480d46
Successfully tagged xxxx
总结以上构建输出,可以看到主要有以下几类内容,这里先对其含义进行简单说明:
【Sending build context to Docker daemon 50.69kB】作为构建输出内容的第一行,根据内容说明我们执行docker build命令后,先将context发送到了docker daemon,且注意到发送内容的大小是50.69kb。
【Step 1/16 : xxxx】很明显这代表执行总步骤和当前步骤。通过对比上面Dockerfile文件中的指令可以看到,有多少行指令就有多少步骤。
【---> Using cache】此类输出的含义是:当前指令找到了可以利用的cache层。
【---> Running in 59bdc0332ec6】此类输出代表启动了容器,容器ID为59bdc0332ec6。
【Removing intermediate container 59bdc0332ec6】可以看到前面创建的容器,用完后清理掉。
【---> a6c2a40e277c】此行代表:指令执行结束后生成的镜像层的id。官方文档原文:The Docker daemon runs the instructions in the Dockerfile one-by-one, committing the result of each instruction to a new image if necessary
【Successfully tagged xxxx】构建最后一步就是对镜像打标签。
根据完整的构建输出和其中内容的含义,我们基本能够梳理清楚docker build执行每一个指令的过程:启动容器-->执行指令操作-->生成镜像层-->清理容器。下面针对以上步骤中的几个重要部分深入分析。
Sending build context部分
其实docker是一个C/S架构组成的系统,我们使用的docker命令称之为docker cli,命令对应的实际操作是由docker daemon来执行的。
因为C/S架构的原因,因此构建镜像时所依赖的文件就需要在client端发送给server端(即dockers daemon)。那么是如何发送的呢?很直接,就是docker cli通过http接口发送给docker daemon的,我们看一下接口文档就清晰了:
接口文档中明确要求了Content-type的值为application/x-tar,即压缩文件。也就是将docker build命令的path部分的文件内容(递归的)打包压缩然后通过http restfule接口发送给了docker daemon。说到这里如果你在实际应用的时候一定会立即想到:path目录中有很多文件如obj/*,bin/*等中间文件又多又没用,是不是可以屏蔽掉来提高效率?是的没错,请搜索”dockerignore”,本文不再展开。
Using cache部分
由于docker的镜像是分层结构的,主要的目的就是为了提高空间利用率和效率。分层后的镜像构成示意图如下:
根据以上示意图,我们查看一个镜像真实的分层结构:
$ sudo docker history 8581c0bf6a5b IMAGE CREATED CREATED BY SIZE COMMENT 8581c0bf6a5b About a minute ago /bin/sh -c dotnet publish "Tccc.BIZ.CMS.Srv.… 7.97MB faa92e4ff2f9 About a minute ago /bin/sh -c dotnet build "Tccc.BIZ.CMS.Srv.cs… 4.15MB 3f2d9b1f4bed 7 minutes ago /bin/sh -c #(nop) WORKDIR /src/Tccc.BIZ.CMS.… 0B 005ea2bc08d0 7 minutes ago /bin/sh -c #(nop) COPY dir:96eb41ae59fd6d263… 24kB f2c85873c12a 7 minutes ago /bin/sh -c dotnet restore "Tccc.BIZ.CMS.Srv/… 61.1MB 32c675319114 8 minutes ago /bin/sh -c #(nop) COPY file:5e6b41c9a79d1b7f… 343B bc0a32ea122b 8 minutes ago /bin/sh -c #(nop) COPY file:401d0d012517703b… 796B 93f67f62562c 8 minutes ago /bin/sh -c #(nop) WORKDIR /src 0B 006ded9ddf29 7 weeks ago /bin/sh -c powershell_version=7.0.1 && c… 38.2MB <missing> 7 weeks ago /bin/sh -c dotnet_sdk_version=3.1.301 &&… 340MB <missing> 7 weeks ago /bin/sh -c apt-get update && apt-get ins… 32.9MB <missing> 7 weeks ago /bin/sh -c #(nop) ENV DOTNET_RUNNING_IN_CON… 0B <missing> 2 months ago /bin/sh -c apt-get update && apt-get install… 146MB <missing> 2 months ago /bin/sh -c set -ex; if ! command -v gpg > /… 17.5MB <missing> 2 months ago /bin/sh -c apt-get update && apt-get install… 16.5MB <missing> 2 months ago /bin/sh -c #(nop) CMD ["bash"] 0B <missing> 2 months ago /bin/sh -c #(nop) ADD file:1ab357efe422cfed5… 114MB
出现此类提示就代表在本地缓存路径(默认为/var/lib/docker)中中找到了可用的镜像层,取消重复构建并选用可用的镜像作为当前指令的构建结果。关于镜像分层的更多细节不再展开,有兴趣的搜索“镜像 分层”即可。
Running in 59bdc0332ec6部分
前面说此类输出的代表启动了新容器,我们想办法验证一下:刻意修改Dockerfile内的指令RUN dotnet1 restore xxxx使其异常中断,得到输出内容如下:
Step 7/16 : RUN dotnet1 restore "Tccc.BIZ.CMS.Srv/Tccc.BIZ.CMS.Srv.csproj" ---> Running in 887fed352f5e /bin/sh: 1: dotnet1: not found The command '/bin/sh -c dotnet1 restore "Tccc.BIZ.CMS.Srv/Tccc.BIZ.CMS.Srv.csproj"' returned a non-zero code: 127
此时执行docker ps -a看到确实刚刚启动了容器3449e0b5f619:
既然是启动容器,那么此容器依赖的镜像是什么?通过上面的截图,可以看到当前指令启动的容器恰好就是上一个指令生成的镜像2da6630520ad。
---> a6c2a40e277c生成镜像部分
这一步实际就是将running的容器保存为镜像(由于镜像的分层结构设计,且容器是在镜像分层的基础上附加了一层读写层,因此保存操作就是将此容器的读写层转换为只读层并保存为静态的镜像),我们通过docker inspect 命令(查看容器/镜像完整详细信息的命令)查看生成的镜像的详细信息来验证一下这个结论。输出内容的Container属性值恰好就是Step 12/16开头创建的容器ID。同时查看截图红色部分,内容也正好是Dockerfile这一步骤的指令,这也说名这层的含义是存储的相对于上一层的改变,即这条指令产生变更的部分;
总结:
现在回顾以上步骤可以看到,我们执行docker build操作后,docker daemon就按照提供的dockerfile指令一步一步的层层处理,构建出最终需要的镜像;限于篇幅原因,本文无法涵盖其中所有的细节,有兴趣的读者还可以继续研究镜像分层、镜像cache、存储驱动storage driver等详细细节。
至此,本文以构建一个asp.net core应用镜像作为线索,阐述了docker构建镜像的总体步骤和相关内部细节,希望能让你对镜像构建的内部过程有一个全面、深刻的理解。