使用 Docker 时,最常用的命令无非是 docker container
和 docker image
相关的子命令,当然最初没有管理类命令(或者说分组)的时候,最常使用的命令也无非是 docker run
docker commit
docker build
和 docker images
这些。
今天来聊一下和 Docker 中核心概念 image
相关的重要命令, docker build
或者说 docker image build
为了简便起见,下文的命令全部使用 docker build
。
先简单介绍下 Docker Image, 通常情况下我们将其称之为镜像,镜像是由多个层组成的文件,这些层用于在容器内执行代码(命令)等。每个镜像基本上都是根据应用程序完整的可执行版本进行构建的,并且需要注意的是,它会依赖于主机的系统内核。当用户在运行镜像时,这将会创建一个或者多个容器实例。
Dockerd 是 Docker 的服务端,默认情况下提供 Unix Domain Socket 连接,当然也可以监听某个端口,用于对外提供服务。 所以有时候,我们也可以使用服务器上的 Docker daemon 来提供服务,以加快构建速度及解决一些网络问题之类的。
好的,基础概念了解了, 那我们开始进入正题。
我们知道构建镜像的方法有多种,本文中我们只介绍使用 Dockerfile 通过 docker build
的方式构建镜像。
为了简便,我们以一个简单的 Dockerfile 开始。构建一个容器内使用的 kubectl 工具 (当然选择它的原因在于 kubectl 足够大,并不考虑可用性,这个稍后解释)
FROM scratch
LABEL maintainer='Jintao Zhang <moelove.info>'
ADD kubectl /kubectl
ENTRYPOINT [ "/kubectl" ]
Dockerfile 足够简单,只是将 kubectl 的二进制文件拷贝进去,并将 Entrypoint 设置为 kubectl 。
我个人一般为了避免环境的污染,大多数的事情都在容器内完成。包括 dockerd 我也启在容器内。其中的原理不再介绍,可以参考我之前的文章或分享。使用起来很简单:
docker run --privileged -d -P docker:stable-dind
注意这里使用了 -P
所以本地会随机映射一个端口,当然你也可以直接指定映射到容器内的 2375 端口。
(Tao) ➜ build git:(master) docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b56f6483614d docker:stable-dind "dockerd-entrypoint.…" 9 hours ago Up 9 hours 0.0.0.0:32769->2375/tcp trusting_babbage
我们直接使用启动在容器内的 dockerd 进行构建,通过上面的 docker ps
命令可以看到是映射到了本地的 32769 端口。所以我们使用以下命令进行构建:
(Tao) ➜ kubectl git:(master) docker -H 0.0.0.0:32769 images
REPOSITORY TAG IMAGE ID CREATED SIZE
(Tao) ➜ kubectl git:(master) docker -H 0.0.0.0:32769 build -t local/kubectl .
Sending build context to Docker daemon 55.09MB
Step 1/4 : FROM scratch
--->
Step 2/4 : LABEL maintainer='Jintao Zhang <moelove.info>'
---> Running in ebcf44071bf0
Removing intermediate container ebcf44071bf0
---> eb4ea1725ff2
Step 3/4 : ADD kubectl /kubectl
---> 1aad06c4dbb4
Step 4/4 : ENTRYPOINT [ "/kubectl" ]
---> Running in 2fc78fe974e3
Removing intermediate container 2fc78fe974e3
---> 457802d4bf3e
Successfully built 457802d4bf3e
Successfully tagged local/kubectl:latest
(Tao) ➜ kubectl git:(master) docker -H 0.0.0.0:32769 images
REPOSITORY TAG IMAGE ID CREATED SIZE
local/kubectl latest 457802d4bf3e 3 seconds ago 55.1MB
看日志及结果,可以看到我们已经成功的构建了我们所需的镜像。说了这么多,其实我们今天的内容才刚刚开始。
在本文一开始,我已经提过 Dockerd 是 Docker 的后端服务,通过上面的
docker -H 0.0.0.0:32769 images
这条命令可以看到我们通过 -H
指定了本地 32679 端口的 dockerd 服务,这其实是个 HTTP 服务,我们来验证下。
(Tao) ➜ kubectl git:(master) curl -i 0.0.0.0:32769/_ping
HTTP/1.1 200 OK
Api-Version: 1.37
Docker-Experimental: false
Ostype: linux
Server: Docker/18.03.1-ce (linux)
Date: Tue, 04 Sep 2018 17:20:51 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8
OK%
可以看到几条关键的信息 Api-Version: 1.37 这个表明了当前使用的 API 版本,本文的内容也是以 1.37 为例进行介绍,这是当前的稳定版本。我们也可以通过 docker version
进行查看。
(Tao) ➜ kubectl git:(master) docker -H 0.0.0.0:32769 version
Client:
Version: 18.06.0-ce
API version: 1.37 (downgraded from 1.38)
Go version: go1.10.3
Git commit: 0ffa825
Built: Wed Jul 18 19:11:45 2018
OS/Arch: linux/amd64
Experimental: false
Server:
Engine:
Version: 18.03.1-ce
API version: 1.37 (minimum version 1.12)
Go version: go1.9.5
Git commit: 9ee9f40
Built: Thu Apr 26 07:23:03 2018
OS/Arch: linux/amd64
Experimental: false
可以看到我本地在用的 docker cli 版本较高,当连接到低版本的 dockerd 时,API 版本降级至与 dockerd 版本保持一致。
当然,你可能会问,如果是 dockerd 版本高会如何呢?其实我日常中的开发环境就是这样,大多数 API 都没什么影响, 不过这并不是今天的重点。
root@bdcdac73ee20:/# docker version
Client:
Version: 17.06.0-ce
API version: 1.30
Go version: go1.8.3
Git commit: 02c1d87
Built: Fri Jun 23 21:15:15 2017
OS/Arch: linux/amd64
Server:
Version: dev
API version: 1.39 (minimum version 1.12)
Go version: go1.10.3
Git commit: e8cc5a0b3
Built: Tue Sep 4 10:00:36 2018
OS/Arch: linux/amd64
Experimental: false
回到我们上面的构建过程中。我们可以看到日志内容的第一行:
...
Sending build context to Docker daemon 55.09MB
从这条日志,我们可以得到两个信息:
第一条结论,我们在上一小节已经讨论过了,我们来重点看下第二条结论。
(Tao) ➜ kubectl git:(master) ls -al
总用量 53808
drwxrwxr-x. 2 tao tao 4096 9月 5 01:00 .
drwxrwxr-x. 3 tao tao 4096 9月 5 00:57 ..
-rw-rw-r--. 1 tao tao 109 9月 5 01:00 Dockerfile
-rwxrwxr-x. 1 tao tao 55084063 9月 5 00:53 kubectl
(Tao) ➜ kubectl git:(master) du -sh .
53M .
(Tao) ➜ kubectl git:(master) du -sh kubectl Dockerfile
53M kubectl
4.0K Dockerfile
按照我们 Dockerfile 的内容,我们需要将 kubectl 的二进制包放入镜像内,所以 build context 虽然比二进制文件多出来 2M 左右的大小你也不会很意外。
但我这里做了另一个例子,不多赘述,代码可以在我的 GitHub 中找到。这里贴出来结果:
(Tao) ➜ text git:(master) ls -al
总用量 16
drwxrwxr-x. 2 tao tao 4096 9月 5 01:45 .
drwxrwxr-x. 4 tao tao 4096 9月 5 01:44 ..
-rw-rw-r--. 1 tao tao 77 9月 5 01:45 Dockerfile
-rw-rw-r--. 1 tao tao 61 9月 5 01:45 file
(Tao) ➜ text git:(master) du -b Dockerfile file
77 Dockerfile
61 file
(Tao) ➜ text git:(master) docker -H 0.0.0.0:32769 build --no-cache=true -t local/file .
Sending build context to Docker daemon 3.072kB
...
相信你看到这个结果已经明白我想表达的意思,我们继续探索下这个过程。
前面我们已经说过,这就是个普通的 HTTP 请求,所以我们当然可以直接抓包来看看到底发生了什么?
很简单,通过 dockerd 的地址,使用 POST
方法,访问 /build
接口, 当然实际情况是会增加前缀,即我在上面提到的版本号,在目前的环境中使用的是 /v1.37/build
这个接口。
而这个请求携带了一些很有用的参数,和头信息。这里我来简单说下:
build 请求的头部,主要有以下两个
Content-Type
默认值为 application/x-tar
,表明自己是一个归档。X-Registry-Config
这个头部信息中包含着 registry 的地址及认证信息,并且以 base64 进行编码。对 docker 熟悉的朋友或者看过我之前文章的朋友应该知道, Docker cli 在 login 成功后,会将认证信息保存至本地,密码做 base64 保存。而 build 的时候则会将此信息再次 base64 进行编码。通过这里也可以看出来,在使用远端 Dockerd 的时候, 应该尽量配置 TLS 以防止中间人攻击,造成密码泄漏等情况。请求参数中,列几个比较有意义的:
t
这其实就是我们 docker build -t
时候指定的参数,并且,我们可以同时指定多个 -t
同时构建多个不同名称的镜像。memory
cpusetcpus
这些主要用于资源限制buildargs
如果想要了解这个参数,可以回忆下 Dockerfile 中的 ARG
指令的用法当然,我们想要探索的过程其实重点就在于请求头部了, 整个请求的输入流,必须是一个 tar
压缩包,并且支持 identity
(不压缩), gzip
, bzip2
, xz
等压缩算法。
我们来看下基本的实现:
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
}
这篇主要内容集中在 docker build 的过程及其原理上,为什么首先要写这篇,主要是因为镜像和我们息息相关, 并且也是我们使用的第一步。而很多情况下,推进业务容器化也都要面临着性能优化及其他规范之类的。
其实关于 build 的细节还有很多,如果有空,我就再更新下一篇。