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指令定义的环境变量已经生效。