写在前面

在前面我们对Dockerfile文件进行了较为详细的学习,介绍了其中的配置指令和操作指令,那么在编写完Dockerfile文件后,开发者如何基于它来创建镜像呢?本篇就来学习相关内容。

创建镜像

在编写完Dockerfile文件后,开发者可以使用docker build [image]命令来创建镜像。其基本的命令格式如下所示:

1
dokcer build [OPTIONS] PATH | URL | -

该命令用于读取指定路径下(包括子目录)的Dockerfile,并将该路径下所有数据作为上下文(Context)发送给Docker服务端。Docker服务端在校验Docker格式并通过后,将逐条执行其中定义的指令,注意在遇到ADD、COPY和RUN指令时,会生成一层新的镜像。最终镜像如果创建成功,那么会返回最终镜像的ID。

上下文不能太大,过大会导致发送大量数据给服务端,延缓创建过程,因此除非是生成该镜像所必须的文件,否则请不要放到上下文路径中。前面也说了,默认是使用上下文路径中的Dockerfile,而如果使用的不是上下文中路径中的Dockerfile,那么就可以通过-f参数来指定其路径。

要指定生成镜像的标签信息,可以通过-t参数,该参数可以重复使用多次为镜像一次添加多个名称。

举个例子,假设当前的上下文路径为/tmp/helloworld,且希望生成的镜像标签为envy/first_image:1.0.0,那么可以使用如下的命令:

1
docker build -t envy/first_image:1.0.0 /tmp/ helloworld

命令选项

在介绍docker build命令的时候,就提到它可以支持一些选项参数,可以动态的通过覆盖Dockerfile文件中配置的方式来调整创建镜像过程中的行为,常用的选项参数如下所示:

选项参数(后接类型) 说明
–add-host (list) 添加自定义的主机到IP的映射
–build-arg (list) 添加创建时的变量
–cache-from (strings) 使用指定镜像作为缓存源
–cgroup-parent (string) 继承的上层cgroup
–compress 使用gzip来压缩创建上下文数据
–cpu-period (int) 分配的CFS调度器时长
–cpu-quota(int) CFS调度器总份额
-c,–cpu-shares (int) CPU权重
–cpuset-cpus string 多CPU允许使用的CPU
–cpuset-mems string 多CPU允许使用的内存
–disable-content-trust 不进行镜像校验,默认为真
-f,–file (string) Dockerfile存在的路径,及Dockerfile文件
–iidfile (string) 将镜像ID写入到文件
–isolation (string) 容器的隔离机制
–label (list) 配置镜像的元数据
-m,–memory (bytes) 限制使用内存量
–memory-swap (bytes) 限制内存和缓存的总量
–network (string) 指定RUN命令时的网络模式
–no-cache 创建镜像时不使用缓存
–platform (string) 指定平台类型
–pull 总是尝试获取镜像的最新版本
-q,–quiet 不打印创建过程中的日志信息
–rm 创建成功后自动删除中间过程容器,默认为真
–security-opt (strings) 指定安全相关的选项
–shm-size (bytes) /dev/shm的大小
–squash 将新创建的多层挤压放入到一层中
–stream 持续获取创建的上下文
-t,–tag (list) 指定镜像的标签列表
–platform (string) 指定平台类型
–target (string) 指定创建的目标阶段
–ulimit (ulimit) 指定ulimit的配置

选择父镜像

大部分情况下,生成新的镜像都需要通过FROM指令来指定父镜像,也就是说父镜像是生成镜像的基础,它直接会影响到所生成镜像的大小和功能。

用户可以选择基础镜像和普通镜像来作为父镜像。

基础镜像比较特殊,其Dockerfile中往往不存在FROM指令,或者是基于scratch镜像(FROM scratch),这也就意味着它在整个镜像树中是处于根的位置。举个例子,下面的Dockerfile中定义了一个简单的基础镜像,将用户提前编译好的二进制可执行文件binary复制到镜像中,运行容器时将执行binary命令,如下所示:

1
2
3
FROM scratch
ADD binary
CMD ["/binary"]

普通镜像往往是由第三方基于基础镜像创建而来的,像常见的busybox、debian、ubuntu等。

下图展示了Docker不同类型镜像之间的继承关系,如下所示:

使用.dockerignore文件

关于这一点,其实我们在前面已经介绍过,但是作为系统学习Dockerfile的一部分,这里再学习一次。开发者可以通过来.dockerignore文件中每行添加一条匹配模式的方式来让Docker忽略匹配路径或者文件,这样在创建镜像的时候就不会将无关的数据发送到服务端了。

举个例子,下面的.dockerignore文件一共有6行代码,其中第一行是注释,其余五行都是匹配模式:

1
2
3
4
5
6
7
# .dockerignore configuration file
*/tmp*
*/*/temp*
tmp?
~*
Dockerfile
!README.md

.dockerignore文件支持Golang风格的正则表达式,其中*表示任意多个字符;表示单个字符;表示不匹配,即不忽略指定的路径或者文件。

多步骤创建

注意自Docker17.05版本开始,就支持多步骤创建(Multi-stage build)镜像这一特性,它可以精简最终生成的镜像的大小。

对于一些需要编译的应用,如C、Go、Java等,通常情况下至少需要准备两个环境的Docker镜像,一个是编译环境镜像,另一个是运行环境镜像。

编译环境镜像,包括完整的编译引擎、依赖库等,因此体积往往较为庞大,其作用使用编译应用为二进制文件;运行环境镜像,主要是利用编译好的二进制文件,运行应用,由于不需要编译环境,因此体积较小。

使用多步骤创建,可以在保证最终生成的运行环境镜像保持精简的情况下,使用单一的Dockerfile,以降低维护的复杂度。

接下来以Go语言为例介绍如何多步骤创建镜像并运行容器。

第一步,在当前目录下(/root目录),创建一个名为main.go的文件,其中的代码为:

1
2
3
4
5
6
7
8
9
10
//main.go file will output "hello,envythink"
package main

import (
"fmt"
)

func main() {
fmt.Println("hello,envythink")
}

第二步,创建Dockerfile,使用golang:1.15镜像编译应用二进制文件为app,同时使用精简的镜像alpine:latest作为运行环境,其中Dockerfile中完整的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# define stage name as builder
FROM golang:1.15 as builder
RUN mkdir -p /go/src/test
WORKDIR /go/src/test
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# copy file from the builder stage
COPY --from=builder /go/src/test/app .
CMD ["./app"]

之后执行下面的命令来创建镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
[root@envythink ~]# docker build -t envythink/multi-stage:v0.1 .
Sending build context to Docker daemon 38.4kB
Step 1/10 : FROM golang:1.15 as builder
---> 4a581cd6feb1
Step 2/10 : RUN mkdir -p /go/src/test
---> Using cache
---> c78abb8b7614
Step 3/10 : WORKDIR /go/src/test
---> Using cache
---> 254616756185
Step 4/10 : COPY main.go .
---> 0b2f60db5ca2
Step 5/10 : RUN CGO_ENABLED=0 GOOS=linux go build -o app .
---> Running in c790e06b5716
Removing intermediate container c790e06b5716
---> 98f5e870a740
Step 6/10 : FROM alpine:latest
latest: Pulling from library/alpine
188c0c94c7c5: Pull complete
Digest: sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a
Status: Downloaded newer image for alpine:latest
---> d6e46aa2470d
Step 7/10 : RUN apk --no-cache add ca-certificates
---> Running in 193a9f424996
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz
(1/1) Installing ca-certificates (20191127-r4)
Executing busybox-1.31.1-r19.trigger
Executing ca-certificates-20191127-r4.trigger
OK: 6 MiB in 15 packages
Removing intermediate container 193a9f424996
---> 26e86324257b
Step 8/10 : WORKDIR /root/
---> Running in 557e7f9b1269
Removing intermediate container 557e7f9b1269
---> e8bc6387ef30
Step 9/10 : COPY --from=builder /go/src/test/app .
---> 0e4023d316a8
Step 10/10 : CMD ["./app"]
---> Running in 785a4c753cb8
Removing intermediate container 785a4c753cb8
---> edc2bb6bd937
Successfully built edc2bb6bd937
Successfully tagged envythink/multi-stage:v0.1

可以看到这个这就是一步一步的按照Dockerfile文件中的指令来执行。之后开发者可以使用如下命令来运行应用,可以看到输出了hello,envythink,说明多步骤构建镜像并运行容器成功:

1
2
[root@envythink ~]# docker run --rm envythink/multi-stage:v0.1
hello,envythink

同时可以看到新创建的envythink/multi-stage:v0.1镜像非常小,只有8.12MB:

1
2
[root@envythink ~]# docker images|grep envythink
envythink/multi-stage v0.1 edc2bb6bd937 12 minutes ago 8.12MB

最佳实践

其实这里应当不能说是最佳实践,而是经验总结,从需求出发,来定制合适自己、高效方便的镜像。

首先需要理解和熟悉每个配置指令和操作指令的含义和执行效果,多编写一些简单的例子进行测试,弄清楚之后再撰写正式的Dockerfile。此外Docker Hub中提供了大量的优秀镜像和对应的Dockerfile,可以通过阅读它们来提升自己编写简介、高效的Dockerfile的能力。

下面一些是笔者从网络上,并结合自身经历的一些总结,在生成镜像过程中,尝试从下面的角度进行思考,并完善所生成的镜像。

(1)精简镜像用途。分工明确,尽量让每个镜像的用途都比较集中,避免构造大而复杂、功能较多的镜像。

(2)选用合适的基础镜像。容器的核心是应用。选择较大的父镜像,如Ubuntu系统镜像会使最终生成的应用镜像十分臃肿,推荐使用瘦身过的应用镜像(如node:slim)或者较为小巧的系统镜像(如alpine、busybox、debian)。

(3)提供注释和维护者信息。Dockerfile也是一种代码,需要后续扩展和维护。

(4)正确使用版本号。推荐使用较为明确的版本号,如v1.0、v1.2等,而不是采用默认的latest。版本号可以解决一些环境冲突问题。

(5)减少镜像层数。如果开发者希望所生成的镜像的层数尽量少,那么就要尽量合并RUN、ADD和COPY指令。通常情况下,多个RUN指令可以合并为一条RUN指令。

(6)合适使用多步骤创建。当开发者使用Docker版本高于17.05时,通过多步骤创建,可以将编译和运行等过程分开,保证最终生成的镜像只包括运行应用所需要的最小环境。当然用户也可以通过分别构造编译镜像和运行镜像来达到上述效果,但是这样需要维护多个Dockerfile。

(7)使用.dockerignore文件。使用它可以标记在执行docker build命令时忽略的路径和文件,避免发送不必要的数据内容,从而加快整个镜像的创建过程。

(8)及时删除临时文件和缓存文件。尤其是当你的Dockerfile文件中设计到apt-get指令后,那么/var/cache/apt下面就会缓存一些安装包。

(9)提高生成速度。合理使用cache缓存,减少上下文环境中的文件,或者使用.dockerignore文件指定等。

(10)调整合理的指令顺序。在开启cache缓存的情况下,内容不变的指令尽量放在前面,这样可以尽量复用。

(11)减少外部源的干扰。如果确实需要从外部引入数据,需要指定持久的地址,并附带版本信息,这样可以让他人复用的时候不易发生错误。

小结

本篇主要学习了如何基于Dockerfile来创建镜像及运行容器,还有一些合理构建镜像的最佳实践。当然想编写一个高质量的Dockerfile并不是一件非常容易的事,需要阅读一些比较优秀的Dockerfile文件,并且要多练习和多思考。

这样关于Docker的基础学习就到此结束了,后续是一些实战的例子,当然还有一些高级的用法,这些是后续的学习内容。