每天5分钟玩转Docker容器技术
上QQ阅读APP看书,第一时间看更新

3.2 构建镜像

对于Docker用户来说,最好的情况是不需要自己创建镜像。几乎所有常用的数据库、中间件、应用软件等都有现成的Docker官方镜像或其他人和组织创建的镜像,我们只需要稍作配置就可以直接使用。

使用现成镜像的好处除了省去自己做镜像的工作量外,更重要的是可以利用前人的经验。特别是使用那些官方镜像,因为Docker的工程师知道如何更好地在容器中运行软件。

当然,某些情况下我们也不得不自己构建镜像,比如:

(1)找不到现成的镜像,比如自己开发的应用程序。

(2)需要在镜像中加入特定的功能,比如官方镜像几乎都不提供ssh。

所以本节我们将介绍构建镜像的方法。

同时分析构建的过程也能够加深我们对前面镜像分层结构的理解。

Docker提供了两种构建镜像的方法: docker commit命令与Dockerfile构建文件。

3.2.1 docker commit

docker commit命令是创建新镜像最直观的方法,其过程包含三个步骤:

● 运行容器。

● 修改容器。

● 将容器保存为新的镜像。

举个例子:在Ubuntu base镜像中安装vi并保存为新镜像。

(1)运行容器

如图3-13所示。

图3-13

-it参数的作用是以交互模式进入容器,并打开终端。412b30588f4a是容器的内部ID。

(2)安装vi

确认vi没有安装,如图3-14所示。

图3-14

安装vi,如图3-15所示。

图3-15

(3)保存为新镜像

在新窗口中查看当前运行的容器,如图3-16所示。

图3-16

silly_goldberg是Docker为我们的容器随机分配的名字。

执行docker commit命令将容器保存为镜像,如图3-17所示。

图3-17

新镜像命名为ubuntu-with-vi。

查看新镜像的属性,如图3-18所示。

图3-18

从size上看到镜像因为安装了软件而变大了。

从新镜像启动容器,验证vi已经可以使用,如图3-19所示。

图3-19

以上演示了如何用docker commit创建新镜像。然而,Docker并不建议用户通过这种方式构建镜像。原因如下:

(1)这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱。比如要在debian base镜像中也加入vi,还得重复前面的所有步骤。

(2)更重要的:使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患。

既然docker commit不是推荐的方法,我们为什么还要花时间学习呢?

原因是:即便是用Dockerfile(推荐方法)构建镜像,底层也是docker commit一层一层构建新镜像的。学习docker commit能够帮助我们更加深入地理解构建过程和镜像的分层结构。

3.2.2 Dockerfile

Dockerfile是一个文本文件,记录了镜像构建的所有步骤。

1.第一个Dockerfile

用Dockerfile创建上节的ubuntu-with-vi,其内容如图3-20所示。

图3-20

下面我们运行docker build命令构建镜像并详细分析每个细节。

    root@ubuntu:~# pwd ①
    /root
    root@ubuntu:~# ls ②
    Dockerfile
    root@ubuntu:~# docker build -t ubuntu-with-vi-dockerfile . ③
    Sending build context to Docker daemon 32.26 kB ④
    Step 1 : FROM ubuntu ⑤
    ---> f753707788c5
    Step 2 : RUN apt-get update && apt-get install -y vim ⑥
    ---> Running in 9f4d4166f7e3 ⑦
    ......
    Setting up vim (2:7.4.1689-3ubuntu1.1) ...
    ---> 35ca89798937 ⑧
    Removing intermediate container 9f4d4166f7e3 ⑨
    Successfully built 35ca89798937 ⑩
    root@ubuntu:~#

① 当前目录为 /root。

② Dockerfile准备就绪。

③ 运行docker build命令,-t将新镜像命名为ubuntu-with-vi-dockerfile,命令末尾的.指明build context为当前目录。Docker默认会从build context中查找Dockerfile文件,我们也可以通过-f参数指定Dockerfile的位置。

④ 从这步开始就是镜像真正的构建过程。首先Docker将build context中的所有文件发送给Docker daemon。build context为镜像构建提供所需要的文件或目录。

Dockerfile中的ADD、COPY等命令可以将build context中的文件添加到镜像。此例中,build context为当前目录 /root,该目录下的所有文件和子目录都会被发送给Docker daemon。

所以,使用build context就得小心了,不要将多余文件放到build context,特别不要把 /、/usr作为build context,否则构建过程会相当缓慢甚至失败。

⑤ Step 1:执行FROM,将Ubuntu作为base镜像。

Ubuntu镜像ID为f753707788c5。

⑥ Step 2:执行RUN,安装vim,具体步骤为 ⑦ ⑧ ⑨。

⑦ 启动ID为9f4d4166f7e3的临时容器,在容器中通过apt-get安装vim。

⑧ 安装成功后,将容器保存为镜像,其ID为35ca89798937。

这一步底层使用的是类似docker commit的命令。

⑨ 删除临时容器9f4d4166f7e3。

⑩ 镜像构建成功。

通过docker images查看镜像信息,如图3-21所示。

图3-21

镜像ID为35ca89798937,与构建时的输出一致。

在上面的构建过程中,我们要特别注意指令RUN的执行过程 ⑦ ⑧ ⑨。Docker会在启动的临时容器中执行操作,并通过commit保存为新的镜像。

2.查看镜像分层结构

ubuntu-with-vi-dockerfile是通过在base镜像的顶部添加一个新的镜像层而得到的,如图3-22所示。

图3-22

这个新镜像层的内容由RUN apt-get update && apt-get install -y vim生成。这一点我们可以通过docker history命令验证,如图3-23所示。

图3-23

docker history会显示镜像的构建历史,也就是Dockerfile的执行过程。

ubuntu-with-vi-dockerfile与Ubuntu镜像相比,确实只是多了顶部的一层35ca89798937,由apt-get命令创建,大小为97.07MB。docker history也向我们展示了镜像的分层结构,每一层由上至下排列。

注:missing表示无法获取IMAGE ID,通常从Docker Hub下载的镜像会有这个问题。

3.镜像的缓存特性

我们接下来看Docker镜像的缓存特性。

Docker会缓存已有镜像的镜像层,构建新镜像时,如果某镜像层已经存在,就直接使用,无须重新创建。

下面举例说明。

在前面的Dockerfile中添加一点新内容,往镜像中复制一个文件,如图3-24所示。

图3-24

    root@ubuntu:~# ls ①
    Dockerfile testfile
    root@ubuntu:~#
    root@ubuntu:~# docker build -t ubuntu-with-vi-dockerfile-2 .
    Sending build context to Docker daemon 32.77 kB
    Step 1 : FROM ubuntu
    ---> f753707788c5
    Step 2 : RUN apt-get update && apt-get install -y vim
    ---> Using cache ②
    ---> 35ca89798937
    Step 3 : COPY testfile / ③
    ---> 8d02784a78f4 Removing intermediate container bf2b4040f4e9
    Successfully built 8d02784a78f4

① 确保testfile已存在。

② 重点在这里:之前已经运行过相同的RUN指令,这次直接使用缓存中的镜像层35ca89798937。

③ 执行COPY指令。

其过程是启动临时容器,复制testfile,提交新的镜像层8d02784a78f4,删除临时容器。

在ubuntu-with-vi-dockerfile镜像上直接添加一层就得到了新的镜像ubuntu-with-vi-dockerfile-2,如图3-25所示。

图3-25

如果我们希望在构建镜像时不使用缓存,可以在docker build命令中加上--no-cache参数。

Dockerfile中每一个指令都会创建一个镜像层,上层是依赖于下层的。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。

也就是说,如果我们改变Dockerfile指令的执行顺序,或者修改或添加指令,都会使缓存失效。举例说明,比如交换前面RUN和COPY的顺序,如图3-26所示。

图3-26

虽然在逻辑上这种改动对镜像的内容没有影响,但由于分层的结构特性,Docker必须重建受影响的镜像层。

    root@ubuntu:~# docker build -t ubuntu-with-vi-dockerfile-3 . Sending
build context to Docker daemon 37.89 kB Step 1 : FROM ubuntu --->
f753707788c5 Step 2 : COPY testfile / ---> bc87c9710f40 Removing
intermediate container 04ff324d6af5 Step 3 : RUN apt-get update && apt-
get install -y vim ---> Running in 7f0fcb5ee373 Get:1
http://archive.ubuntu.com/ubuntu xenial InRelease [247 kB] ......

从上面的输出可以看到生成了新的镜像层bc87c9710f40,缓存已经失效。

除了构建时使用缓存,Docker在下载镜像时也会使用。例如我们下载httpd镜像,如图3-27所示。

图3-27

docker pull命令输出显示第一层(base镜像)已经存在,不需要下载。

由Dockerfile可知httpd的base镜像为debian,正好之前已经下载过debian镜像,所以有缓存可用。通过docker history可以进一步验证,如图3-28所示。

图3-28

4.调试Dockerfile

总结一下通过Dockerfile构建镜像的过程:

(1)从base镜像运行一个容器。

(2)执行一条指令,对容器做修改。

(3)执行类似docker commit的操作,生成一个新的镜像层。

(4)Docker再基于刚刚提交的镜像运行一个新容器。

(5)重复2~4步,直到Dockerfile中的所有指令执行完毕。

从这个过程可以看出,如果Dockerfile由于某种原因执行到某个指令失败了,我们也将能够得到前一个指令成功执行构建出的镜像,这对调试Dockerfile非常有帮助。我们可以运行最新的这个镜像定位指令失败的原因。

我们来看一个调试的例子。Dockerfile内容如图3-29所示。

图3-29

执行docker build,如图3-30所示。

图3-30

Dockerfile在执行第三步RUN指令时失败。我们可以利用第二步创建的镜像22d31cc52b3e进行调试,方法是通过docker run -it启动镜像的一个容器,如图3-31所示。

图3-31

手工执行RUN指令很容易定位失败的原因是busybox镜像中没有bash。虽然这是个极其简单的例子,但它很好地展示了调试Dockerfile的方法。

5. Dockerfile常用指令

是时候系统学习Dockerfile了。

下面列出了Dockerfile中最常用的指令,完整列表和说明可参看官方文档。

● FROM

指定base镜像。

● MAINTAINER

设置镜像的作者,可以是任意字符串。

● COPY

将文件从build context复制到镜像。

COPY支持两种形式: COPY src dest与COPY ["src", "dest"]。

注意:src只能指定build context中的文件或目录。

● ADD

与COPY类似,从build context复制文件到镜像。不同的是,如果src是归档文件(tar、zip、tgz、xz等),文件会被自动解压到dest。

● ENV

设置环境变量,环境变量可被后面的指令使用。例如:

    ENV MY_VERSION 1.3 RUN apt-get install -y mypackage=$MY_VERSION

● EXPOSE

指定容器中的进程会监听某个端口,Docker可以将该端口暴露出来。我们会在容器网络部分详细讨论。

● VOLUME

将文件或目录声明为volume。我们会在容器存储部分详细讨论。

● WORKDIR

为后面的RUN、CMD、ENTRYPOINT、ADD或COPY指令设置镜像中的当前工作目录。

● RUN

在容器中运行指定的命令。

● CMD

容器启动时运行指定的命令。

Dockerfile中可以有多个CMD指令,但只有最后一个生效。CMD可以被docker run之后的参数替换。

● ENTRYPOINT

设置容器启动时运行的命令。

Dockerfile中可以有多个ENTRYPOINT指令,但只有最后一个生效。CMD或docker run之后的参数会被当作参数传递给ENTRYPOINT。

下面我们来看一个较为全面的Dockerfile,如图3-32所示。

图3-32

注:Dockerfile支持以“#”开头的注释。

构建镜像,如图3-33所示。

图3-33

① 构建前确保build context中存在需要的文件。

② 依次执行Dockerfile指令,完成构建。

运行容器,验证镜像内容,如图3-34所示。

图3-34

① 进入容器,当前目录即为WORKDIR。

如果WORKDIR不存在,Docker会自动为我们创建。

② WORKDIR中保存了我们希望的文件和目录:

目录bunch:由ADD指令从build context复制的归档文件bunch.tar.gz,已经自动解压。

文件tmpfile1:由RUN指令创建。

文件tmpfile2:由COPY指令从build context复制。

③ ENV指令定义的环境变量已经生效。