共计 17306 个字符,预计需要花费 44 分钟才能阅读完成。
大家好,我是张晋涛。
我将在这篇文章中深刻 Docker 的源码,与你聊聊镜像构建的原理。
Docker 架构
这里咱们先从宏观上对 Docker
有个大略的意识,它整体上是个 C/S 架构;咱们平时应用的 docker
命令就是它的 CLI 客户端,而它的服务端是 dockerd
在 Linux 零碎中,通常咱们是应用 systemd
进行治理,所以咱们能够应用 systemctl start docker
来启动服务。(然而请留神,dockerd
是否能运行与 systemd
并无任何关系,你能够像平时执行一个一般的二进制程序一样,间接通过 dockerd
来启动服务,留神须要 root 权限)
实际上也就是
(图片起源:docker overview)
docker
CLI 与 dockerd
的交互是通过 REST API 来实现的,当咱们执行 docker version
的时候过滤 API 能够看到如下输入:
➜ ~ docker version |grep API
API version: 1.41
API version: 1.41 (minimum version 1.12)
下面一行是 docker
CLI 的 API 版本,上面则代表了 dockerd
的 API 版本,它的前面还有个括号,是因为 Docker 具备了很良好的兼容性,这里示意它最小可兼容的 API 版本是 1.12。
对于咱们进行 C/S 架构的我的项目开发而言,个别都是 API 后行, 所以咱们先来看下 API 的局部。
当然,本文的主体是构建零碎相干的,所以咱们就间接来看构建相干的 API 即可。
接下来会说 CLI,代码以 v20.10.5 为准。最初说服务端 Dockerd。
API
Docker 保护团队在每个版本正式公布之后,都会将 API 文档公布进去,能够通过 Docker Engine API 在线浏览,也能够自行构建 API 文档。
首先 clone Docker 的源代码仓库, 进入我的项目仓库内执行 make swagger-docs
即可在启动一个容器同时将端口裸露至本地的 9000
端口,你能够间接通过 http://127.0.0.1:9000 拜访本地的 API 文档。
(MoeLove) ➜ git clone https://github.com/docker/docker.git docker
(MoeLove) ➜ cd docker
(MoeLove) ➜ docker git:(master) git checkout -b v20.10.5 v20.10.5
(MoeLove) ➜ docker git:(v20.10.5) make swagger-docs
API docs preview will be running at http://localhost:9000
关上 http://127.0.0.1:9000/#operation/ImageBuild 这个地址就能够看到 1.41 版本的构建镜像所需的 API 了。咱们对此 API 进行下剖析。
申请地址和办法
接口地址是 /v1.41/build
办法是 POST
,咱们能够应用一个较新版本的 curl
工具来验证下此接口(须要应用 --unix-socket
连贯 Docker 监听的 UNIX Domain Socket)。dockerd
默认状况下监听在 /var/run/docker.sock
,当然你也能够给 dockerd
传递 --host
参数用于监听 HTTP 端口或者其余门路的 unix socket .
/ # curl -X POST --unix-socket /var/run/docker.sock localhost/v1.41/build
{"message":"Cannot locate specified Dockerfile: Dockerfile"}
从下面的输入咱们能够看到,咱们的确拜访到了该接口,同时该接口的响应是提醒须要 Dockerfile
.
申请体
A tar archive compressed with one of the following algorithms: identity (no compression), gzip, bzip2, xz.
string <binary>
申请体是一个 tar
归档文件,可抉择无压缩、gzip
、bzip2
、xz
压缩等模式。对于这几种压缩格局就不再开展介绍了,但值得注意的是 如果应用了压缩,则传输体积会变小,即网络耗费会相应缩小。但压缩 / 解压缩须要消耗 CPU 等计算资源 这在咱们对大规模镜像构建做优化时是个值得衡量的点。
申请头
因为要发送的是个 tar
归档文件,Content-type
默认是 application/x-tar
。另一个会发送的头是 X-Registry-Config
,这是一个由 Base64 编码后的 Docker Registry 的配置信息,内容与 $HOME/.docker/config.json
中的 auths
内的信息统一。
这些配置信息,在你执行 docker login
后会主动写入到 $HOME/.docker/config.json
文件内的。这些信息被传输到 dockerd
在构建过程中作为拉取镜像的认证信息应用。
申请参数
最初就是申请参数了,参数有很多,通过 docker build --help
根本都能够看到对应含意的,这里不再一一开展了,前面会有一些要害参数的介绍。
小结
下面咱们介绍了 Docker
构建镜像相干的 API,咱们能够间接拜访 Docker Engine 的 API 文档。或者通过源码仓库,本人来构建一个本地的 API 文档服务,应用浏览器进行拜访。
通过 API 咱们也晓得了该接口所需的申请体是一个 tar
归档文件(可抉择压缩算法进行压缩),同时它的申请头中会携带用户在镜像仓库中的认证信息。这揭示咱们,如果在应用近程 Dockerd
构建时,请注意安全,尽量应用 tls
进行加密,免得数据透露。
CLI
API 曾经介绍完了,咱们来看下 docker
CLI,我以前的文章中介绍过当初 Docker 中有两个构建零碎,一个是 v1 版本的 builder
另一个是 v2 版本的即 BuildKit
咱们来别离深刻源码来看看在构建镜像时,他们各自的行为吧。
筹备代码
CLI 的代码仓库在 https://github.com/docker/cli 本文的代码以 v20.10.5
为准。
通过以下步骤应用此版本的代码:
(MoeLove) ➜ git clone https://github.com/docker/cli.git
(MoeLove) ➜ cd cli
(MoeLove) ➜ cli git:(master) git checkout -b v20.10.5 v20.10.5
逐渐合成
docker
是咱们所应用的客户端工具,用于与 dockerd
进行交互。对于构建相干的局部,咱们所熟知的便是 docker build
或者是 docker image build
,在 19.03 中新增的是 docker builder build
,但其实他们都是同一个只是做了个 alias 罢了:
// cmd/docker/docker.go#L237
if v, ok := aliasMap["builder"]; ok {
aliases = append(aliases,
[2][]string{{"build"}, {v, "build"}},
[2][]string{{"image", "build"}, {v, "build"}},
)
}
真正的入口函数其实在 cli/command/image/build.go
;辨别如何调用的逻辑如下:
func runBuild(dockerCli command.Cli, options buildOptions) error {buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
if err != nil {return err}
if buildkitEnabled {return runBuildBuildKit(dockerCli, options)
}
// 省略掉了对于 builder 的理论逻辑
}
这里就是判断下是否反对 buildkit
// cli/command/cli.go#L176
func BuildKitEnabled(si ServerInfo) (bool, error) {
buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit
if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" {
var err error
buildkitEnabled, err = strconv.ParseBool(buildkitEnv)
if err != nil {return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
}
}
return buildkitEnabled, nil
}
当然,从这里能够失去两个信息:
- 通过
dockerd
的配置可开启buildkit
。在/etc/docker/daemon.json
中增加如下内容,并重启dockerd
即可:
{
"features": {"buildkit": true}
}
- 在
docker
CLI 上也可开启buildkit
的反对,并且 CLI 的配置可笼罩服务端配置。通过export DOCKER_BUILDKIT=1
即可开启buildkit
的反对,设置为 0 则敞开(0/false/f/F 之类的也都是雷同的后果)
从下面的介绍也看到了,对于本来默认的 builder 而言,入口逻辑在 runBuild
中,而对于应用 buildkit 的则是 runBuildBuildKit
接下来,咱们对两者进行逐渐合成。
builder v1
在 runBuild
函数中,大抵经验了以下阶段:
参数解决
最开始的局部是一些对参数的解决和校验。
stream
和compress
不可同时应用。
因为如果咱们指定了 compress
的话,则 CLI 会应用 gzip
将构建上下文进行压缩,这样也就没法很好的通过 stream
的模式来解决构建的上下文了。
当然你也可能会想,从技术上来讲,压缩和流式没有什么必然的抵触,是可实现的。事实的确如此,如果从技术的角度上来讲两者并非齐全不能一起存在,无非就是减少解压缩的动作。然而当开启 stream
模式,对每个文件都进行压缩和解压的操作那将会是很大的资源节约,同时也减少了其复杂度,所以在 CLI 中便间接进行了限度,不容许同时应用 compress
和 stream
- 不可同时应用
stdin
读取Dockerfile
和build context
。
在进行构建时,如果咱们将 Dockerfile
的名字传递为 -
时,示意从 stdin
读取其内容。
例如,某个目录下有三个文件 foo
bar
和 Dockerfile
,通过管道将 Dockerfile
的内容通过 stdin
传递给 docker build
(MoeLove) ➜ x ls
bar Dockerfile foo
(MoeLove) ➜ x cat Dockerfile | DOCKER_BUILDKIT=0 docker build -f - .
Sending build context to Docker daemon 15.41kB
Step 1/3 : FROM scratch
--->
Step 2/3 : COPY foo foo
---> a2af45d66bb5
Step 3/3 : COPY bar bar
---> cc803c675dd2
Successfully built cc803c675dd2
能够看到通过 stdin
传递 Dockerfile
的形式能胜利的构建镜像。接下来咱们尝试通过 stdin
将 build context
传递进去。
(MoeLove) ➜ x tar -cvf x.tar foo bar Dockerfile
foo
bar
Dockerfile
(MoeLove) ➜ x cat x.tar| DOCKER_BUILDKIT=0 docker build -f Dockerfile -
Sending build context to Docker daemon 10.24kB
Step 1/3 : FROM scratch
--->
Step 2/3 : COPY foo foo
---> 09319712e220
Step 3/3 : COPY bar bar
---> ce88644a7395
Successfully built ce88644a7395
能够看到通过 stdin
传递 build context
的形式也能够胜利构建镜像。
但如果 Dockerfile
的名称与构建的上下文都指定为 -
即 docker build -f - -
时,会产生什么呢?
(MoeLove) ➜ x DOCKER_BUILDKIT=0 docker build -f - -
invalid argument: can't use stdin for both build context and dockerfile
就会报错了。所以,不能同时应用 stdin
读取 Dockerfile
和 build context
。
build context
反对四种行为。
switch {case options.contextFromStdin():
// 省略
case isLocalDir(specifiedContext):
// 省略
case urlutil.IsGitURL(specifiedContext):
// 省略
case urlutil.IsURL(specifiedContext):
// 省略
default:
return errors.Errorf("unable to prepare context: path %q not found", specifiedContext)
}
从 stdin
传入,上文曾经演示过了,传递给 stdin
的是 tar
归档文件。当然也能够是指定一个具体的 PATH
,咱们通常应用的 docker build .
便是这种用法;
或者能够指定一个 git
仓库的地址,CLI 会调用 git
命令将仓库 clone
至一个长期目录,进行应用;
最初一种是,给定一个 URL
地址,该地址能够是 一个具体的 Dockerfile 文件地址 或者是 一个 tar
归档文件的下载地址。
这几种根本就是字面上的区别,至于 CLI 的行为差别,次要是最初一种,当 URL
地址是一个具体的 Dockerfile
文件地址,在这种状况下 build context
相当于只有 Dockerfile
本身,所以并不能应用 COPY
之类的指定,至于 ADD
也只能应用可拜访的内部地址。
- 可应用
.dockerignore
疏忽不须要的文件
我在之前的文章中有分享过相干的内容。这里咱们看看它的实现逻辑。
// cli/command/image/build/dockerignore.go#L13
func ReadDockerignore(contextDir string) ([]string, error) {var excludes []string
f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
switch {case os.IsNotExist(err):
return excludes, nil
case err != nil:
return nil, err
}
defer f.Close()
return dockerignore.ReadAll(f)
}
.dockerignore
是一个固定的文件名,并且须要放在build context
的根目录下。相似后面提到的,应用一个Dockerfile
文件的 URL 地址作为build context
传入的形式,便无奈应用.dockerignore
。.dockerignore
文件能够不存在,但在读取的时候如果遇到谬误,便会抛出谬误。- 通过
.dockerignore
将会过滤掉不心愿退出到镜像内,或者过滤掉与镜像无关的内容。
最初 CLI 会将 build context
中的内容通过 .dockerignore
过滤后,打包成为真正的 build context
即真正的构建上下文。这也是为什么有时候你发现自己明明在 Dockerfile
外面写了 COPY xx xx
然而最初没有发现该文件的状况。很可能就是被 .dockerignore
给疏忽掉了。这样有利于优化 CLI 与 dockerd
之间的传输压力之类的。
docker
CLI 还会去读取~/.docker/config.json
中的内容。
这与后面 API 局部所形容的内容根本是统一的。将认证信息通过 X-Registry-Config
头传递给 dockerd
用于在须要拉取镜像时进行身份校验。
- 调用 API 进行理论构建工作
当所有所需的校验和信息都准备就绪之后,则开始调用 dockerCli.Client
封装的 API 接口,将申请发送至 dockerd
,进行理论的构建工作。
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
if err != nil {
if options.quiet {fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
}
cancel()
return err
}
defer response.Body.Close()
到这里其实一次构建的过程中 CLI 所解决的流程就根本完结了,之后便是依照传递的参数进行进度的输入或是将镜像 ID 写入到文件之类的。这部分就不进行开展了。
小结
整个过程大抵如下图:
从入口函数 runBuild
开始,通过判断是否反对 buildkit
,如果不反对 buildkit
则持续应用 v1 的 builder
。接下来读取各类参数,依照不同的参数执行各类不同的解决逻辑。这里须要留神的就是 Dockerfile
及 build context
都可反对从文件或者 stdin
等读入,具体应用时,须要留神。另外 .dockerignore
文件可过滤掉 build context
中的一些文件,在应用时,可通过此办法进行构建效率的优化,当然也须要留神,在通过 URL 获取 Dockerfile
的时候,是不存在 build context
的,所以相似 COPY
这样的命令也就无奈应用了。当所有的 build context
和参数都准备就绪后,接下来调用封装好的客户端,将这些申请依照本文开始之初介绍的 API 发送给 dockerd
,由其进行真正的构建逻辑。
最初当构建完结后,CLI 依据参数决定是否要显示构建进度或者后果。
buildkit
接下来咱们来看看 buildkit
如何来执行构建,办法入口与 builder
统一,然而在 buildkitEnabled
处,因为开启了 buildkit
反对,所以跳转到了 runBuildBuildKit
。
func runBuild(dockerCli command.Cli, options buildOptions) error {buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
if err != nil {return err}
if buildkitEnabled {return runBuildBuildKit(dockerCli, options)
}
// 省略掉了对于 builder 的理论逻辑
}
创立会话
然而与 builder
不同的是,这里先执行了一次 trySession
函数。
// cli/command/image/build_buildkit.go#L50
s, err := trySession(dockerCli, options.context, false)
if err != nil {return err}
if s == nil {return errors.Errorf("buildkit not supported by daemon")
}
这个函数是用来做什么的呢?咱们来找到该函数所在的文件 cli/command/image/build_session.go
// cli/command/image/build_session.go#L29
func trySession(dockerCli command.Cli, contextDir string, forStream bool) (*session.Session, error) {if !isSessionSupported(dockerCli, forStream) {return nil, nil}
sharedKey := getBuildSharedKey(contextDir)
s, err := session.NewSession(context.Background(), filepath.Base(contextDir), sharedKey)
if err != nil {return nil, errors.Wrap(err, "failed to create session")
}
return s, nil
}
当然还包含它其中最次要的 isSessionSupported
函数:
// cli/command/image/build_session.go#L22
func isSessionSupported(dockerCli command.Cli, forStream bool) bool {if !forStream && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.39") {return true}
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
}
isSessionSupported
很显著是用于判断是否反对 Session
,这里因为咱们会传入 forStream
为 false
,而且以后的 API 版本是 1.41 比 1.39 大,所以此函数会返回 true
。其实在 builder
中也执行过雷同的逻辑,只不过是在传递了 --stream
参数后,应用 Session
获取一个长连贯以达到 stream
的解决能力。
这也就是为什么会有上面 dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
这个判断存在的起因了。
当确认反对 Session
时,则会调用 session.NewSession
创立一个新的会话。
// github.com/moby/buildkit/session/session.go#L47
func NewSession(ctx context.Context, name, sharedKey string) (*Session, error) {id := identity.NewID()
var unary []grpc.UnaryServerInterceptor
var stream []grpc.StreamServerInterceptor
serverOpts := []grpc.ServerOption{}
if span := opentracing.SpanFromContext(ctx); span != nil {tracer := span.Tracer()
unary = append(unary, otgrpc.OpenTracingServerInterceptor(tracer, traceFilter()))
stream = append(stream, otgrpc.OpenTracingStreamServerInterceptor(span.Tracer(), traceFilter()))
}
unary = append(unary, grpcerrors.UnaryServerInterceptor)
stream = append(stream, grpcerrors.StreamServerInterceptor)
if len(unary) == 1 {serverOpts = append(serverOpts, grpc.UnaryInterceptor(unary[0]))
} else if len(unary) > 1 {serverOpts = append(serverOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(unary...)))
}
if len(stream) == 1 {serverOpts = append(serverOpts, grpc.StreamInterceptor(stream[0]))
} else if len(stream) > 1 {serverOpts = append(serverOpts, grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(stream...)))
}
s := &Session{
id: id,
name: name,
sharedKey: sharedKey,
grpcServer: grpc.NewServer(serverOpts...),
}
grpc_health_v1.RegisterHealthServer(s.grpcServer, health.NewServer())
return s, nil
}
它创立了一个长连贯会话,接下来的操作也都会基于这个会话来做。接下来的操作与 builder
大体一致,先判断 context
是以哪种模式提供的;当然它也与 builder
一样,是不容许同时从 stdin
获取 Dockerfile
和 build context
。
switch {case options.contextFromStdin():
// 省略解决逻辑
case isLocalDir(options.context):
// 省略解决逻辑
case urlutil.IsGitURL(options.context):
// 省略解决逻辑
case urlutil.IsURL(options.context):
// 省略解决逻辑
default:
return errors.Errorf("unable to prepare context: path %q not found", options.context)
}
这里的解决逻辑与 v1 builder
保持一致的起因,次要在于用户体验上,以后的 CLI 的性能曾经根本稳固,用户也曾经习惯,所以即便是减少了 BuildKit
也并没有对主体的操作逻辑造成多大扭转。
抉择输入模式
BuildKit
反对了三种不同的输入模式 local
tar
和失常模式(即存储在 dockerd
中),格局为 -o type=local,dest=path
如果须要将构建的镜像进行散发,或是须要进行镜像内文件浏览的话,应用这个形式也是很不便的。
outputs, err := parseOutputs(options.outputs)
if err != nil {return errors.Wrapf(err, "failed to parse outputs")
}
for _, out := range outputs {
switch out.Type {
case "local":
// 省略
case "tar":
// 省略
}
}
其实它反对的模式还有第 4 种,名为 cacheonly
但它并不会像后面提到的三种模式一样,有个很直观的输入,而且用的人可能会很少,所以就没有独自写了。
读取认证信息
dockerAuthProvider := authprovider.NewDockerAuthProvider(os.Stderr)
s.Allow(dockerAuthProvider)
这里的行为与下面提到的 builder
的行为基本一致,这里次要有两个须要留神的点:
- Allow() 函数
func (s *Session) Allow(a Attachable) {a.Register(s.grpcServer)
}
这个 Allow
函数就是容许通过下面提到的 grpc 会话拜访给定的服务。
authprovider
authprovider
是 BuildKit
提供的一组形象接口汇合,通过它们能够拜访到机器上的配置文件,进而拿到认证信息,行为与 builder
基本一致。
高阶个性:mount secrets
和 ssh
我其余的文章讲过这两种高阶个性的应用了,本篇中就不再多应用进行过多阐明了,只来大体看下该局部的原理和逻辑。
secretsprovider
和 sshprovider
都是 buildkit
在提供的,利用这两种个性能够在 Docker 镜像进行构建时更加平安,且更加灵便。
func parseSecretSpecs(sl []string) (session.Attachable, error) {fs := make([]secretsprovider.Source, 0, len(sl))
for _, v := range sl {s, err := parseSecret(v)
if err != nil {return nil, err}
fs = append(fs, *s)
}
store, err := secretsprovider.NewStore(fs)
if err != nil {return nil, err}
return secretsprovider.NewSecretProvider(store), nil
}
对于 secrets
方面,最终的 parseSecret
会实现格局相干的校验之类的;
func parseSSHSpecs(sl []string) (session.Attachable, error) {configs := make([]sshprovider.AgentConfig, 0, len(sl))
for _, v := range sl {c := parseSSH(v)
configs = append(configs, *c)
}
return sshprovider.NewSSHAgentProvider(configs)
}
而对于 ssh
方面,则与上方的 secrets
基本一致,通过 sshprovider
容许进行 ssh 转发之类的,这里不再深刻开展了。
调用 API 发送构建申请
这里次要有两种状况。
- 当
build context
是从stdin
读,并且是一个tar
文件时
buildID := stringid.GenerateRandomID()
if body != nil {eg.Go(func() error {
buildOptions := types.ImageBuildOptions{
Version: types.BuilderBuildKit,
BuildID: uploadRequestRemote + ":" + buildID,
}
response, err := dockerCli.Client().ImageBuild(context.Background(), body, buildOptions)
if err != nil {return err}
defer response.Body.Close()
return nil
})
}
它会执行上述这部分逻辑,但同时也要留神,这是应用的是 Golang 的 goroutine
,到这里也并不是完结,这部分代码之后的代码也同样会被执行。这就说到了另一种状况了(通常状况)。
- 应用
doBuild
实现逻辑
eg.Go(func() error {defer func() {s.Close()
}()
buildOptions := imageBuildOptions(dockerCli, options)
buildOptions.Version = types.BuilderBuildKit
buildOptions.Dockerfile = dockerfileName
buildOptions.RemoteContext = remote
buildOptions.SessionID = s.ID()
buildOptions.BuildID = buildID
buildOptions.Outputs = outputs
return doBuild(ctx, eg, dockerCli, stdoutUsed, options, buildOptions)
})
那 doBuild
会做些什么呢?它同样也调用了 API 向 dockerd
发动了构建申请。
func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, stdoutUsed bool, options buildOptions, buildOptions types.ImageBuildOptions, at session.Attachable) (finalErr error) {response, err := dockerCli.Client().ImageBuild(context.Background(), nil, buildOptions)
if err != nil {return err}
defer response.Body.Close()
// 省略
}
从以上的介绍咱们能够先做个小的总结。当 build context
从 stdin
读,并且是个 tar
归档时,理论会向 dockerd
发动两次 /build
申请 而个别状况下只会发送一次申请。
那这里会有什么差异呢?此处先不开展,咱们留到上面讲 dockerd
服务端的时候再来解释。
小结
这里咱们对开启了 buildkit
反对的 CLI 构建镜像的过程进行了剖析,大抵过程如下:
从入口函数 runBuild
开始,判断是否反对 buildkit
,如果反对 buildkit
则调用 runBuildBuildKit
。与 v1 的 builder
不同的是,开启了 buildkit
后,会首先创立一个长连贯的会话,并始终放弃。其次,与 builder
雷同,判断 build context
的起源,格局之类的,校验参数等。当然,buildkit
反对三种不同的输入格局 tar
, local
或失常的存储于 Docker 的目录中。另外是在 buildkit
中新增的高阶个性,能够配置 secrets
和 ssh
密钥等性能。最初,再调用 API 与 dockerd
交互实现镜像的构建。
服务端:dockerd
下面别离介绍了 API,CLI 的 v1 builder
和 buildkit
,接下来咱们看看服务端的具体原理和逻辑。
Client 函数
还记得下面局部中最初通过 API 与服务端交互的 ImageBuild
函数吗?在开始 dockerd
的介绍前,咱们来看下这个客户端接口的具体内容。
// github.com/docker/docker/client/image_build.go#L20
func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {query, err := cli.imageBuildOptionsToQuery(options)
if err != nil {return types.ImageBuildResponse{}, err
}
headers := http.Header(make(map[string][]string))
buf, err := json.Marshal(options.AuthConfigs)
if err != nil {return types.ImageBuildResponse{}, err
}
headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))
headers.Set("Content-Type", "application/x-tar")
serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
if err != nil {return types.ImageBuildResponse{}, err
}
osType := getDockerOS(serverResp.header.Get("Server"))
return types.ImageBuildResponse{
Body: serverResp.body,
OSType: osType,
}, nil
}
没有什么太特地的中央,行为与 API 统一。通过这里咱们确认它的确拜访的 /build
接口,所以,咱们来看看 dockerd
的 /build
接口,看看它在构建镜像的时候做了什么。
dockerd
因为本文集中探讨的是构建零碎相干的局部,所以也就不再过多赘述与构建无关的内容了,咱们间接来看,当 CLI 通过 /build
接口发送申请后,会产生什么。
先来看该 API 的入口:
// api/server/router/build/build.go#L32
func (r *buildRouter) initRoutes() {r.routes = []router.Route{router.NewPostRoute("/build", r.postBuild),
router.NewPostRoute("/build/prune", r.postPrune),
router.NewPostRoute("/build/cancel", r.postCancel),
}
}
dockerd
提供了一套类 RESTful 的后端接口服务,解决逻辑的入口便是下面的 postBuild
函数。
该函数的内容较多,咱们来合成下它的次要步骤。
buildOptions, err := newImageBuildOptions(ctx, r)
if err != nil {return errf(err)
}
newImageBuildOptions
函数就是结构构建参数的,将通过 API 提交过去的参数转换为构建动作理论须要的参数模式。
buildOptions.AuthConfigs = getAuthConfigs(r.Header)
getAuthConfigs
函数用于从申请头拿到认证信息
imgID, err := br.backend.Build(ctx, backend.BuildConfig{
Source: body,
Options: buildOptions,
ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader),
})
if err != nil {return errf(err)
}
这里就须要留神了: 真正的构建过程要开始了。应用 backend 的 Build
函数来实现真正的构建过程
// api/server/backend/build/backend.go#L53
func (b *Backend) Build(ctx context.Context, config backend.BuildConfig) (string, error) {
options := config.Options
useBuildKit := options.Version == types.BuilderBuildKit
tagger, err := NewTagger(b.imageComponent, config.ProgressWriter.StdoutFormatter, options.Tags)
if err != nil {return "", err}
var build *builder.Result
if useBuildKit {build, err = b.buildkit.Build(ctx, config)
if err != nil {return "", err}
} else {build, err = b.builder.Build(ctx, config)
if err != nil {return "", err}
}
if build == nil {return "", nil}
var imageID = build.ImageID
if options.Squash {if imageID, err = squashBuild(build, b.imageComponent); err != nil {return "", err}
if config.ProgressWriter.AuxFormatter != nil {if err = config.ProgressWriter.AuxFormatter.Emit("moby.image.id", types.BuildResult{ID: imageID}); err != nil {return "", err}
}
}
if !useBuildKit {
stdout := config.ProgressWriter.StdoutFormatter
fmt.Fprintf(stdout, "Successfully built %s\n", stringid.TruncateID(imageID))
}
if imageID != "" {err = tagger.TagImages(image.ID(imageID))
}
return imageID, err
}
这个函数看着比拟长,但次要性能就以下三点:
NewTagger
是用于给镜像打标签,也就是咱们的-t
参数相干的内容,这里不做开展。- 通过判断是否应用了
buildkit
来调用不同的构建后端。
useBuildKit := options.Version == types.BuilderBuildKit
var build *builder.Result
if useBuildKit {build, err = b.buildkit.Build(ctx, config)
if err != nil {return "", err}
} else {build, err = b.builder.Build(ctx, config)
if err != nil {return "", err}
}
- 解决构建实现后的动作。
到这个函数之后,就别离是 v1 builder
与 buildkit
对 Dockerfile
的解析,以及对 build context
的操作了。
这里波及到的内容与我下一篇文章《高效构建 Docker 镜像的最佳实际》的外部关联比拟大,此处就不再进行开展了。敬请期待下一篇文章。
总结
本文首先介绍了 Docker 的 C/S 架构,介绍了构建镜像所用的 API , API 文档能够在线查看或者本地构建。之后深刻到 Docker CLI 的源码中,逐渐合成 v1 builder
与 buildkit
在构建镜像时执行的过程的差别。最初,咱们深刻到 dockerd
的源码中,理解到了对不同构建后端的调用。至此,Docker 构建镜像的原理及主体代码就介绍结束。
但这还并不是完结,我会在后续文章中分享镜像构建的相干实际,敬请期待!
欢送订阅我的文章公众号【MoeLove】