对于Docker用户来说,最好的情况是不需要自己创建镜像。几乎所有常用的数据库、中间件、应用软件等都有现成的Docker官方镜像或其他人和组织创建的镜像,我们只需要稍作配置就可以直接使用.
使用现成镜像的好处除了省去自己做镜像的工作量外,更重要的是可以利用前人的经验。特别是使用那些官方镜像,因为Docker的工程师知道如何更好地在容器中运行软件。
当然,某些情况下我们也不得不自己构建镜像, 比如
(1)找不到现成的镜像,比如自己开发的应用程序。
(2)需要在镜像中加入特定的功能,比如官方镜像几乎都不提供ssh。
所以本节我们将介绍构建镜像的方法。
同时分析构建的过程也能够加深我们对前面镜像分层结构的理解。
Docker 提供了两种构建镜像的方法:docker commit命令与Dockerfile构建文件。
docker commit命令是创建新镜像最直观的方法,其过程包含三个步骤
• 运行容器。
• 修改容器。
• 将容器保存为新的镜像。
举个例子:在centos base镜像中安装vim并保存为新镜像。
第一步:运行容器,如图所示。
-it 参数的作用是以交互模式进入容器,并打开终端。9f937c7b4534 是容器的内部 ID。
第二步:修改容器,即安装vim
确认 vim 没有安装,如图所示。
安装vim, 如图所示。
第三步:保存为新镜像
在新窗口中查看当前运行的容器。
stoic_liskov是Docker为我们的容器随机分配的名字。
执行docker commit命令将容器保存为镜像, 如图所示。
新镜像命名为centos-with-vim。查看新镜像的属性,如图所示。
从size上看到镜像因为安装了软件而变大了。从新镜像启动容器,验证vim已经可以使用,如图所示。
以上演示了如何用 docker commit 创建新镜像。然而,Docker 并不建议用户通过这种方式构建镜像。原因如下:
(1)这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱。比如要在 centos base 镜像中也加入 vim,还得重复前面的所有步骤。
更重要的:使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患。
既然 docker commit 不是推荐的方法,我们干嘛还要花时间学习呢?
原因是:即便是用 Dockerfile(推荐方法)构建镜像,底层也 docker commit 一层一层构建新镜像的。学习 docker commit 能够帮助我们更加深入地理解构建过程和镜像的分层结构。
下一节我们学习如何通过 Dockerfile 构建镜像。
Dockerfile是一个文本文件,记录了镜像构建的所有步骤。
(1)第一个Dockefile
用Dockerfile创建新的镜像ubuntu-with-vim,其内容如图所示(以ubuntu base镜像为例)。
下面我们运行 docker build 命令构建镜像并详细分析每个细节。
[root@localhost ~]# pwd 1
/root
[root@localhost ~]# ls 2
Dockerfile
[root@localhost ~]# docker build -t ubuntu-with-vim-dockerfile . 3
Sending build context to Docker daemon 18.94kB 4
Step 1/2 : FROM ubuntu 5
---> 93fd78260bd1
Step 2/2 : RUN apt-get update && apt-get install -y vim 6
---> Running in a0a85b228957 7
Get:1 http://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
Get:2 http://security.ubuntu.com/ubuntu bionic-security/multiverse amd64 Packages [1367 B]
…………
Removing intermediate container a0a85b228957 8
---> a2ba3cbb7b97 9
Successfully built a2ba3cbb7b97 10
Successfully tagged ubuntu-with-vim-dockerfile:latest
① 当前目录为 /root。
② Dockerfile 准备就绪。
③ 运行 docker build 命令,-t 将新镜像命名为ubuntu-with-vim-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 为 93fd78260bd1。
⑥ Step 2:执行 RUN,安装 vim,具体步骤为 ⑦、⑧、⑨。
⑦ 启动 ID 为a0a85b228957的临时容器,在容器中通过 apt-get 安装 vim。
⑧ 删除临时容器 a0a85b228957。
⑨ 安装成功后,将容器保存为镜像,其 ID 为 a2ba3cbb7b97。这一步底层使用的是类似 docker commit 的命令。
⑩ 镜像构建成功。
通过 docker images 查看镜像信息。
镜像 ID 为a2ba3cbb7b97,与构建时的输出一致。
在上面的构建过程中,我们要特别注意指令RUN的执行过程 ⑦、⑧、⑨。Docker会在启动的临时容器中执行操作,并通过 commit 保存为新的镜像。
(2)查看镜像分层结构
ubuntu-with-vim-dockerfile 是通过在 base 镜像的顶部添加一个新的镜像层而得到的。
这个新镜像层的内容由 RUN apt-get update && apt-get install -y vim 生成。这一点我们可以通过 docker history 命令验证。
docker history 会显示镜像的构建历史,也就是 Dockerfile 的执行过程。
ubuntu-with-vim-dockerfile 与 ubuntu 镜像相比,确实只是多了顶部的一层a0a85b228957,由 apt-get 命令创建,大小为 83.7MB。docker history 也向我们展示了镜像的分层结构,每一层由上至下排列。
注:missing表示无法获取 IMAGE ID,通常从 Docker Hub 下载的镜像会有这个问题。
(3)镜像的缓存特性
我们接下来看Docker镜像的缓存特性。
Docker会缓存已有镜像的镜像层,构建新镜像时,如果某镜像层已经存在,就直接使用,无须重新创建。下面举例说明。
在前面的Dockerfile中添加一点新内容,往镜像中复制一个文件,如图所示。
[root@localhost ~]# pwd
/root
[root@localhost ~]# ls
Dockerfile testfile
[root@localhost ~]# docker build -t ubuntu-with-vim-dockerfile-2 .
Sending build context to Docker daemon 19.97kB
Step 1/3 : FROM ubuntu
---> 93fd78260bd1
Step 2/3 : RUN apt-get update && apt-get install -y vim
---> Using cache
---> a2ba3cbb7b97
Step 3/3 : COPY testfile /
---> 93902aee704e
Successfully built 93902aee704e
Successfully tagged ubuntu-with-vim-dockerfile-2:latest
① 确保 testfile 已存在。
② 重点在这里:之前已经运行过相同的 RUN 指令,这次直接使用缓存中的镜像层a2ba3cbb7b97。
③ 执行 COPY 指令。
其过程是启动临时容器,复制 testfile,提交新的镜像层93902aee704e,删除临时容器。
在 ubuntu-with-vim-dockerfile 镜像上直接添加一层就得到了新的镜像 ubuntu-with-vim-dockerfile-2。
如果我们希望在构建镜像时不使用缓存,可以在 docker build 命令中加上 --no-cache 参数。
Dockerfile 中每一个指令都会创建一个镜像层,上层是依赖于下层的。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。
也就是说,如果我们改变 Dockerfile 指令的执行顺序,或者修改或添加指令,都会使缓存失效。举例说明,比如交换前面 RUN 和 COPY 的顺序,如图所示:
虽然在逻辑上这种改动对镜像的内容没有影响,但由于分层的结构特性,Docker 必须重建受影响的镜像层。
# docker build -t ubuntu-with-vim-dockerfile-3 .
Sending build context to Docker daemon 21.5kB
Step 1/3 : FROM ubuntu
---> 93fd78260bd1
Step 2/3 : COPY testfile /
---> db6a42f8c8a0
Step 3/3 : RUN apt-get update && apt-get install -y vim
---> Running in 9029d097e2fb
Get:1 http://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
Get:2 http://archive.ubuntu.com/ubuntu bionic InRelease [242 kB]
…………
从上面的输出可以看到生成了新的镜像层db6a42f8c8a0,缓存已经失效。
除了构建时使用缓存,Docker 在下载镜像时也会使用。例如我们下载 httpd 镜像,如图所示。
docker pull 命令输出显示第一层(base 镜像)已经存在,不需要下载。
由 Dockerfile 可知 httpd 的 base 镜像为 debian,正好之前已经下载过 debian 镜像,所以有缓存可用。通过 docker history 可以进一步验证,如图所示。
(4)调试dockerfile文件
包括 Dockerfile 在内的任何脚本和程序都会出错。有错并不可怕,但必须有办法排查,所以本节讨论如何 debug Dockerfile。
先回顾一下通过 Dockerfile 构建镜像的过程:
① 从 base 镜像运行一个容器。
② 执行一条指令,对容器做修改。
③ 执行类似 docker commit 的操作,生成一个新的镜像层。
④ Docker 再基于刚刚提交的镜像运行一个新容器。
⑤ 重复 2-4 步,直到 Dockerfile 中的所有指令执行完毕。
从这个过程可以看出,如果 Dockerfile 由于某种原因执行到某个指令失败了,我们也将能够得到前一个指令成功执行构建出的镜像,这对调试 Dockerfile 非常有帮助。我们可以运行最新的这个镜像定位指令失败的原因。
我们来看一个调试的例子。Dockerfile 内容如图所示:
执行 docker build:
执行docker images查看镜像
Dockerfile在执行第三步 RUN 指令时失败。我们可以利用第二步创建的镜像737d6765cb5e进行调试,方式是通过 docker run -it 启动镜像的一个容器,如图所示。
手工执行 RUN 指令很容易定位失败的原因是 busybox 镜像中没有 bash。虽然这是个极其简单的例子,但它很好地展示了调试 Dockerfile 的方法。
用通俗一点的话来讲:dockerfile就是根据自己的需要自定义一个镜像,就像你写shell脚本一样,把一连串的过程或步骤全部写进dockerfile文件中,一步一步的执行dockerfile文件中你写的内容。
到这里相信大家对 Dockerfile 的功能和使用流程有了比较完整的印象.