Docker 镜像
在之前的介绍中,我们知道镜像是 Docker 的三大组件之一。
Docker 运行容器前需要本地存在对应的镜像,如果本地不存在该镜像,Docker 会从镜像仓库下载该镜像。
本章将介绍更多关于镜像的内容,包括:
之前提到过, 上有大量的高质量的镜像可以用,这里我们就说一下怎么获取这些镜像。
从 Docker 镜像仓库获取镜像的命令是 。其命令格式为:
具体的选项可以通过 命令看到,这里我们说一下镜像名称的格式。
比如:
上面的命令中没有给出 Docker 镜像仓库地址,因此将会从 Docker Hub 获取镜像。而镜像名称是 ,因此将会获取官方镜像 仓库中标签为 的镜像。
从下载过程中可以看到我们之前提及的分层存储的概念,镜像是由多层存储所构成。下载也是一层层的去下载,并非单一文件。下载过程中给出了每一层的 ID 的前 12 位。并且下载结束后,给出该镜像完整的 的摘要,以确保下载一致性。
在使用上面命令的时候,你可能会发现,你所看到的层 ID 以及 的摘要和这里的不一样。这是因为官方镜像是一直在维护的,有任何新的 bug,或者版本更新,都会进行修复再以原来的标签发布,这样可以确保任何使用这个标签的用户可以获得更安全、更稳定的镜像。
如果从 Docker Hub 下载镜像非常缓慢,可以参照 一节配置加速器。
有了镜像后,我们就能够以这个镜像为基础启动并运行一个容器。以上面的 为例,如果我们打算启动里面的 并且进行交互式操作的话,可以执行下面的命令。
就是运行容器的命令,我们这里简要的说明一下上面用到的参数。
进入容器后,我们可以在 Shell 下操作,执行任何所需的命令。这里,我们执行了 ,这是 Linux 常用的查看当前系统版本的命令,从返回的结果可以看到容器内是 系统。
最后我们通过 退出了这个容器
要想列出已经下载下来的镜像,可以使用 命令。
列表包含了 、、、 以及 。
其中仓库名、标签在之前的基础概念章节已经介绍过了。镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个标签。因此,在上面的例子中,我们可以看到 和 拥有相同的 ID,因为它们对应的是同一个镜像。
如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如, 镜像大小,在这里是 ,但是在 显示的却是 。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。
另外一个需要注意的问题是, 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
你可以通过以下命令来便捷的查看镜像、容器、数据卷所占用的空间。
上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 。:
这个镜像原本是有镜像名和标签的,原来为 ,随着官方镜像维护,发布了新版本后,重新 时, 这个镜像名被转移到了新下载的镜像身上,而旧的镜像上的这个名称则被取消,从而成为了 。除了 可能导致这种情况, 也同样可以导致这种现象。由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 的镜像。这类无标签镜像也被称为 虚悬镜像(dangling image) ,可以用下面的命令专门显示这类镜像:
一般来说,虚悬镜像已经失去了存在的价值,是可以随意删除的,可以用下面的命令删除。
为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 参数。
这样会看到很多无标签的镜像,与之前的虚悬镜像不同,这些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。实际上,这些镜像也没必要删除,因为之前说过,相同的层只会存一遍,而这些镜像是别的镜像的依赖,因此并不会因为它们被列出来而多存了一份,无论如何你也会需要它们。只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。
不加任何参数的情况下, 会列出所有顶级镜像,但是有时候我们只希望列出部分镜像。 有好几个参数可以帮助做到这个事情。
根据仓库名列出镜像
列出特定的某个镜像,也就是说指定仓库名和标签
除此以外, 还支持强大的过滤器参数 ,或者简写 。之前我们已经看到了使用过滤器来列出虚悬镜像的用法,它还有更多的用法。比如,我们希望看到在 之后建立的镜像,可以用下面的命令:
想查看某个位置之前的镜像也可以,只需要把 换成 即可。
此外,如果镜像构建时,定义了 ,还可以通过 来过滤。
默认情况下, 会输出一个完整的表格,但是我们并非所有时候都会需要这些内容。比如,刚才删除虚悬镜像的时候,我们需要利用 把所有的虚悬镜像的 ID 列出来,然后才可以交给 命令作为参数来删除指定的这些镜像,这个时候就用到了 参数。
配合 产生出指定范围的 ID 列表,然后送给另一个 命令作为参数,从而针对这组实体成批的进行某种操作的做法在 Docker 命令行使用过程中非常常见,不仅仅是镜像,将来我们会在各个命令中看到这类搭配以完成很强大的功能。因此每次在文档看到过滤器后,可以多注意一下它们的用法。
另外一些时候,我们可能只是对表格的结构不满意,希望自己组织列;或者不希望有标题,这样方便其它程序解析结果等,这就用到了 。
比如,下面的命令会直接列出镜像结果,并且只包含镜像ID和仓库名:
或者打算以表格等距显示,并且有标题行,和默认一样,不过自己定义列:
如果要删除本地的镜像,可以使用 命令,其格式为:
其中, 可以是 、、 或者 。
比如我们有这么一些镜像:
我们可以用镜像的完整 ID,也称为 ,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 来删除镜像。 默认列出的就已经是短 ID 了,一般取前3个字符以上,只要足够区分于别的镜像就可以了。
比如这里,如果我们要删除 镜像,可以执行:
我们也可以用,也就是 ,来删除镜像。
当然,更精确的是使用 删除镜像。
如果观察上面这几个命令的运行输出信息的话,你会注意到删除行为分为两类,一类是 ,另一类是 。我们之前介绍过,镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。
因此当我们使用上面命令删除镜像的时候,实际上是在要求删除某个标签的镜像。所以首先需要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的 的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 行为就不会发生。所以并非所有的 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。
当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变动非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。这就是为什么,有时候会奇怪,为什么明明没有别的标签指向这个镜像,但是它还是存在的原因,也是为什么有时候会发现所删除的层数和自己 看到的层数不一样的源。
除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。之前讲过,容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。因此该镜像如果被这个容器所依赖的,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。
像其它可以承接多个实体的命令一样,可以使用 来配合使用 ,这样可以成批的删除希望删除的镜像。我们在“镜像列表”章节介绍过很多过滤镜像列表的方式都可以拿过来使用。
比如,我们需要删除所有仓库名为 的镜像:
或者删除所有在 之前的镜像:
充分利用你的想象力和 Linux 命令行的强大,你可以完成很多非常赞的功能。
在 Ubuntu/Debian 上有 可以使用,如 或者 ,而 CentOS 和 RHEL 的内核中没有相关驱动。因此对于这类系统,一般使用 驱动利用 LVM 的一些机制来模拟分层存储。这样的做法除了性能比较差外,稳定性一般也不好,而且配置相对复杂。Docker 安装在 CentOS/RHEL 上后,会默认选择 ,但是为了简化配置,其 是跑在一个稀疏文件模拟的块设备上,也被称为 。这样的选择是因为不需要额外配置就可以运行 Docker,这是自动配置唯一能做到的事情。但是 的做法非常不好,其稳定性、性能更差,无论是日志还是 中都会看到警告信息。官方文档有明确的文章讲解了如何配置块设备给 驱动做存储层的做法,这类做法也被称为配置 。
除了前面说到的问题外, + 还有一个缺陷,因为它是稀疏文件,所以它会不断增长。用户在使用过程中会注意到 不断增长,而且无法控制。很多人会希望删除镜像或者可以解决这个问题,结果发现效果并不明显。原因就是这个稀疏文件的空间释放后基本不进行垃圾回收的问题。因此往往会出现即使删除了文件内容,空间却无法回收,随着使用这个稀疏文件一直在不断增长。
所以对于 CentOS/RHEL 的用户来说,在没有办法使用 的情况下,一定要配置 给 ,无论是为了性能、稳定性还是空间利用率。
或许有人注意到了 CentOS 7 中存在被 backports 回来的 驱动,不过 CentOS 里的这个驱动达不到生产环境使用的稳定程度,所以不推荐使用。
注意: 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 定制镜像,定制镜像应该使用 来完成。如果你想要定制镜像请查看下一小节。
镜像是容器的基础,每次执行 的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,我们所使用的都是来自于 Docker Hub 的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。接下来的几节就将讲解如何定制镜像。
回顾一下之前我们学到的知识,镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
现在让我们以定制一个 Web 服务器为例子,来讲解镜像是如何构建的。
这条命令会用 镜像启动一个容器,命名为 ,并且映射了 80 端口,这样我们可以用浏览器去访问这个 服务器。
如果是在 Linux 本机运行的 Docker,或者如果使用的是 Docker for Mac、Docker for Windows,那么可以直接访问:;如果使用的是 Docker Toolbox,或者是在虚拟机、云服务器上安装的 Docker,则需要将 换为虚拟机地址或者实际云服务器地址。
直接用浏览器访问的话,我们会看到默认的 Nginx 欢迎页面。
现在,假设我们非常不喜欢这个欢迎页面,我们希望改成欢迎 Docker 的文字,我们可以使用 命令进入容器,修改其内容。
我们以交互式终端方式进入 容器,并执行了 命令,也就是获得一个可操作的 Shell。
然后,我们用 覆盖了 的内容。
现在我们再刷新浏览器的话,会发现内容被改变了。
我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 命令看到具体的改动。
现在我们定制好了变化,我们希望能将其保存下来形成镜像。
要知道,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
的语法格式为:
我们可以用下面的命令将容器保存为镜像:
其中 是指定修改的作者,而 则是记录本次修改的内容。这点和 版本控制相似,不过这里这些信息可以省略留空。
我们可以在 中看到这个新定制的镜像:
新的镜像定制好后,我们可以来运行这个镜像。
这里我们命名为新的服务为 ,并且映射到 端口。如果是 Docker for Mac/Windows 或 Linux 桌面的话,我们就可以直接访问 看到结果,其内容应该和之前修改后的 一样。
至此,我们第一次完成了定制镜像,使用的是 命令,手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。
使用 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。
首先,如果仔细观察之前的 的结果,你会发现除了真正想要修改的 文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。
此外,使用 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。虽然 或许可以告诉得到一些线索,但是远远不到可以确保生成一致镜像的地步。这种黑箱镜像的维护工作是非常痛苦的。
而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。
Dockerfile 定制镜像
从刚才的 的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
还以之前定制 镜像为例,这次我们使用 Dockerfile 来定制。
在一个空白目录中,建立一个文本文件,并命名为 :
其内容为:
这个 Dockerfile 很简单,一共就两行。涉及到了两条指令, 和 。
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 就是指定基础镜像,因此一个 中 是必备的指令,并且必须是第一条指令。
在 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 、 、 、 、 、 、 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 、 、 、 、 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 、 、 、 、 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
如果你以 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 、。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 会让镜像体积更加小巧。使用 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
指令是用来执行命令行命令的。由于命令行的强大能力, 指令在定制镜像时是最常用的指令之一。其格式有两种:
既然 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:
之前说过,Dockerfile 中每一个指令都会建立一层, 也不例外。每一个 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后, 这一层的修改,构成新的镜像。
而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。
Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。
上面的 正确的写法应该是这样:
从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 中,如同我们之前所说的那样, 指令启动了一个容器 ,执行了所要求的命令,并最后提交了这一层 ,随后删除了所用到的这个容器 。
这里我们使用了 命令进行镜像构建。其格式为:
在这里我们指定了最终镜像的名称 ,构建成功后,我们可以像之前运行 那样来运行这个镜像,其结果会和 一样。
如果注意,会看到 命令最后有一个 。 表示当前目录,而 就在当前目录,因此不少初学者以为这个路径是在指定 所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定上下文路径。那么什么是上下文呢?
首先我们要理解 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 ,而如 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
当我们进行镜像构建的时候,并非所有定制都会通过 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 指令、 指令等。而 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径, 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
如果在 中这么写:
这并不是要复制执行 命令所在的目录下的 ,也不是复制 所在目录下的 ,而是复制 上下文(context) 目录下的 。
因此, 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 或者 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。
现在就可以理解刚才的命令 中的这个 ,实际上是在指定上下文的目录, 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
如果观察 输出,我们其实已经看到了这个发送上下文的过程:
理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 不工作后,于是干脆将 放到了硬盘根目录去构建,结果发现 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 打包整个硬盘,这显然是使用错误。
一般来说,应该会将 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 一样的语法写一个 ,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
那么为什么会有人误以为 是指定 所在目录呢?这是因为在默认情况下,如果不额外指定 的话,会将上下文目录下的名为 的文件作为 Dockerfile。
这只是默认行为,实际上 的文件名并不要求必须为 ,而且并不要求必须位于上下文目录中,比如可以用 参数指定某个文件作为 。
当然,一般大家习惯性的会使用默认的文件名 ,以及会将其置于镜像构建上下文目录中。
直接用 Git repo 进行构建
或许你已经注意到了, 还支持从 URL 构建,比如可以直接从 Git repo 中构建:
这行命令指定了构建所需的 Git repo,并且指定默认的 分支,构建目录为 ,然后 Docker 就会自己去 这个项目、切换到指定分支、并进入到指定目录后开始构建。
用给定的 tar 压缩包构建
如果所给出的 URL 不是个 Git repo,而是个 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。
从标准输入中读取 Dockerfile 进行构建
或
如果标准输入传入的是文本文件,则将其视为 ,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 进镜像之类的事情。
从标准输入中读取上下文压缩包进行构建
如果发现标准输入的文件格式是 、 以及 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。
我们已经介绍了 ,,还提及了 , ,其实 功能很强大,它提供了十多个指令。下面我们继续讲解其他的指令。
格式:
和 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。
指令将从构建上下文目录中 的文件/目录复制到新的一层的镜像内的 位置。比如:
可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 规则,如:
可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外,还需要注意一点,使用 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
指令和 的格式和性质基本一致。但是在 基础上增加了一些功能。
比如 可以是一个 ,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 去。下载后的文件权限自动设置为 ,如果这并不是想要的权限,那么还需要增加额外的一层 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 指令进行解压缩。所以不如直接使用 指令,然后使用 或者 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。
如果 为一个 压缩文件的话,压缩格式为 , 以及 的情况下, 指令将会自动解压缩这个压缩文件到 去。
在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 中:
但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 命令了。
在 Docker 官方的 中要求,尽可能的使用 ,因为 的语义很明确,就是复制文件而已,而 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 的场合,就是所提及的需要自动解压缩的场合。
另外需要注意的是, 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。
因此在 和 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 指令,仅在需要自动解压缩的场合使用 。
指令的格式和 相似,也是两种格式:
之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。 指令就是用于指定默认的容器主进程的启动命令的。
在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如, 镜像默认的 是 ,如果我们直接 的话,会直接进入 。我们也可以在运行时指定运行别的命令,如 。这就是用 命令替换了默认的 命令了,输出了系统版本信息。
在指令格式上,一般推荐使用 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ,而不要使用单引号。
如果使用 格式的话,实际的命令会被包装为 的参数的形式进行执行。比如:
在实际执行中,会将其变更为:
这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。
提到 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。
Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。
一些初学者将 写为:
然后发现容器执行后就立即退出了。甚至在容器内去使用 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。
对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。
而使用 命令,则是希望 upstart 来以后台守护进程形式启动 服务。而刚才说了 会被理解为 ,因此主进程实际上是 。那么当 命令结束后, 也就结束了, 作为主进程退出了,自然就会令容器退出。
正确的做法是直接执行 可执行文件,并且要求以前台形式运行。比如:
的格式和 指令格式一样,分为 格式和 格式。
的目的和 一样,都是在指定容器启动程序及参数。 在运行时也可以替代,不过比 要略显繁琐,需要通过 的参数 来指定。
当指定了 后, 的含义就发生了改变,不再是直接的运行其命令,而是将 的内容作为参数传给 指令,换句话说实际执行时,将变为:
那么有了 后,为什么还要有 呢?这种 有什么好处么?让我们来看几个场景。
场景一:让镜像变成像命令一样使用
假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 来实现:
假如我们使用 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:
嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 中可以看到实质的命令是 ,那么如果我们希望显示 HTTP 头信息,就需要加上 参数。那么我们可以直接加 参数给 么?
我们可以看到可执行文件找不到的报错,。之前我们说过,跟在镜像名后面的是 ,运行时会替换 的默认值。因此这里的 替换了原来的 ,而不是添加在原来的 后面。而 根本不是命令,所以自然找不到。
那么如果我们希望加入 这参数,我们就必须重新完整的输入这个命令:
这显然不是很好的解决方案,而使用 就可以解决这个问题。现在我们重新用 来实现这个镜像:
这次我们再来尝试直接使用 :
当前 IP:61.148.226.66 来自:北京市 联通
可以看到,这次成功了。这是因为当存在 后, 的内容将会作为参数传给 ,而这里 就是新的 ,因此会作为参数传给 ,从而达到了我们预期的效果。
场景二:应用运行前的准备工作
启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。
比如 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。
此外,可能希望避免使用 用户去启动服务,从而提高安全性,而在启动服务前还需要以 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 身份执行,方便调试等。
这些准备工作是和容器 无关的,无论 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 中去执行,而这个脚本会将接到的参数(也就是 )作为命令,在脚本最后执行。比如官方镜像 中就是这么做的:
可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 为 脚本。
格式有两种:
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 ,还是运行时的应用,都可以直接使用这里定义的环境变量。
这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。
定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 镜像 中,就有类似这样的代码:
在这里先定义了环境变量 ,其后的 这层里,多次使用 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 即可, 构建维护变得更轻松了。
下列指令可以支持环境变量展开: 、、、、、、、、、。
可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 制作更多的镜像,只需使用不同的环境变量即可。
格式:
构建参数和 的效果一样,都是设置环境变量。所不同的是, 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 保存密码之类的信息,因为 还是可以看到所有值的。
中的 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 中用 来覆盖。
在 1.13 之前的版本,要求 中的参数名,必须在 中用 定义过了,换句话说,就是 指定的参数,必须在 中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的 的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。
格式为:
之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
这里的 目录就会在运行时自动挂载为匿名卷,任何向 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:
在这行命令中,就使用了 这个命名卷挂载到了 这个位置,替代了 中定义的匿名卷的挂载配置。
格式为 。
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 时,会自动随机映射 的端口。
此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个 Docker 引擎参数 ,当指定该参数后,容器间将默认无法互访,除非互相间使用了 参数的容器才可以互通,并且只有镜像中 所声明的端口才可以被访问。这个 的用法,在引入了 后已经基本不用了,通过自定义网络可以很轻松的实现容器间的互联与隔离。
要将 和在运行时使用 区分开来。,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
格式为 。
使用 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在, 会帮你建立目录。
之前提到一些初学者常犯的错误是把 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:
如果将这个 进行构建镜像运行后,会发现找不到 文件,或者其内容不是 。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 中,这两行 命令的执行环境根本不同,是两个完全不同的容器。这就是对 构建分层存储的概念不了解所导致的错误。
之前说过每一个 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。
因此如果需要改变以后各层的工作目录的位置,那么应该使用 指令。
格式:
指令和 相似,都是改变环境状态并影响以后的层。 是改变工作目录, 则是改变之后层的执行 , 以及 这类命令的身份。
当然,和 一样, 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
如果以 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 或者 ,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 。
设置 CMD,并以另外的用户执行 CMD [ “exec”, “gosu”, “redis”, “redis-server” ]
格式:
指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。
在没有 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。
而自 1.12 之后,Docker 提供了 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。
当在一个镜像指定了 指令后,用其启动容器,初始状态会为 ,在 指令检查成功后变为 ,如果连续一定次数失败,则会变为 。
支持下列选项:
和 , 一样, 只可以出现一次,如果写了多个,只有最后一个生效。
在 后面的命令,格式和 一样,分为 格式,和 格式。命令的返回值决定了该次健康检查的成功与否::成功;:失败;:保留,不要使用这个值。
假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 来帮助判断,其 的 可以这么写:
构建好了后,我们启动一个容器:
在等待几秒钟后,再次 ,就会看到健康状态变化为了 :
格式:。
是一个特殊的指令,它后面跟的是其它指令,比如 , 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
中的其它指令都是为了定制当前镜像而准备的,唯有 是为了帮助别人定制自己而准备的。
假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 进行包管理,所有依赖、配置、启动信息等会放到 文件里。在拿到程序代码后,需要先进行 才可以获得所有需要的依赖。然后就可以通过 来启动应用。因此,一般来说会这样写 :
这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 的话,各个项目内的自己的 就变为:
这次我们回到原始的 ,但是这次将项目相关的指令加上 ,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 就变成了简单地:
之前的做法
在 Docker 17.05 版本之前,我们构建 Docker 镜像时,通常会采用两种方式:
全部放入一个 Dockerfile
一种方式是将所有的构建过程编包含在一个 中,包括项目及其依赖库的编译、测试、打包等流程,这里可能会带来的一些问题:
例如
编写 文件,该程序输出
编写 文件
构建镜像
分散到多个 Dockerfile
另一种方式,就是我们事先在一个 将项目及其依赖库编译测试打包好后,再将其拷贝到运行环境中,这种方式需要我们编写两个 和一些编译脚本才能将其两个阶段自动整合起来,这种方式虽然可以很好地规避第一种方式存在的风险,但明显部署过程较复杂。
例如
编写 文件
编写 文件
新建
现在运行脚本即可构建镜像
对比两种方式生成的镜像大小
为解决以上问题,Docker v17.05 开始支持多阶段构建 ()。使用多阶段构建我们就可以很容易解决前面提到的问题,并且只需要编写一个 :
例如
编写 文件
构建镜像
对比三个镜像大小
很明显使用多阶段构建的镜像体积小,同时也完美解决了上边提到的问题。
只构建某一阶段的镜像
我们可以使用 来为某一阶段命名,例如
例如当我们只想构建 阶段的镜像时,我们可以在使用 命令时加上 参数即可
构建时从其他镜像复制文件
上面例子中我们使用 从上一阶段的镜像中复制文件,我们也可以复制任意镜像中的文件。
除了标准的使用 生成镜像的方法外,由于各种特殊需求和历史原因,还提供了一些其它方法用以生成镜像。
格式:
压缩包可以是本地文件、远程 Web 文件,甚至是从标准输入中得到。压缩包将会在镜像 目录展开,并直接作为镜像第一层提交。
比如我们想要创建一个 的 Ubuntu 14.04 的镜像:
这条命令自动下载了 文件,并且作为根文件系统展开导入,并保存为镜像 。
导入成功后,我们可以用 看到这个导入的镜像:
如果我们查看其历史的话,会看到描述中有导入的文件链接:
Docker 还提供了 和 命令,用以将镜像保存为一个 文件,然后传输到另一个位置上,再加载进来。这是在没有 Docker Registry 时的做法,现在已经不推荐,镜像迁移应该直接使用 Docker Registry,无论是直接使用 Docker Hub 还是使用内网私有 Registry 都可以。
保存镜像
使用 命令可以将镜像保存为归档文件。
比如我们希望保存这个 镜像。
保存镜像的命令为:
然后我们将 文件复制到了到了另一个机器上,可以用下面这个命令加载镜像:
如果我们结合这两个命令以及 甚至 的话,利用 Linux 强大的管道,我们可以写一个命令完成从一个机器将镜像迁移到另一个机器,并且带进度条的功能:
Docker 镜像是怎么实现增量的修改和维护的?
每个镜像都由很多层次构成,Docker 使用 将这些不同的层结合到一个镜像中去。
通常 Union FS 有两个用途, 一方面可以实现不借助 LVM、RAID 将多个 disk 挂到同一个目录下,另一个更常用的就是将一个只读的分支和一个可写的分支联合在一起,Live CD 正是基于此方法可以允许在镜像不变的基础上允许用户在其上进行一些写操作。
Docker 在 AUFS 上构建的容器也是利用了类似的原理。
到此这篇ubuntu镜像下载为压缩包(ubuntu 镜像下载)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/bcyy/68162.html