Docker技术知识分享(三) - 镜像

镜像是容器技术的基础,容器是镜像的实例,下面我们就具体来看下镜像是如何组成和运行的。

一,镜像内部的结构

我们日常使用已打好的镜像则下载配置就可以,而如何我们涉及项目中的开发和自制“烧制”镜像则需要更加深入的了解镜像的构成。

通过镜像知识的学习,也可以进一步理解为什么镜像的运行仅需要占用相对较少的资源。

1.1 最小的镜像 - HelloWorld

类似于学习各门语言的课程中一样,我们从简单的HELLOWORLD示例来了解系统的运行机制,同样,我们通过下载和使用HELLOWORLD的镜像,运行容器来学习其运行机制。

HelloWorld是DOCKER官网提供的镜像,我们通过docker pull 从 docker hub上下载:

docker pull hello-world

下载镜像成功能:

使用docker images 命令查看镜像的信息:


我们可以看到非常有趣的参数:13.3kb, 也就是一个独立最小调度单位仅十几KB的大小,通过docker run 命令来查看该容器的运行结果:


上图是DOCKER的运行装态,我们可能更加关注容器如何编制,那我们可以查看Dockerfile是镜像的描述文件,定义如何构建Docker镜像,其语法相对比较简单且可读性强,我们从hello-world的dockerfile来初步验证如何生成镜像:

FROM scratch

COPY hello/

CMD ["/hello"]

三个命令的含义为:

(1)FROM sratch 是指镜像从0开始创建,无基础镜像。

(2)COPY hello / 将文件“hello”复制到镜像的根目录。

(3)CMD ["/hello"] 容器启动时,执行/hello。

首先在dev目录里准备Dockerfile文件,把可运行的命令hello也同步放在这个目录里,然后运行:

docker build -t hello-world-demo .

其中hello-world-demo为镜像的名称,. 为当前目录。

1.2 base镜像

base镜像的含义:(1)不依赖其他镜像,可以从零开始。(2)其他镜像可以基于base镜像进行扩展。

base镜像一般都是Linux发行版的的docker镜像,比如:Ubuntu, Debian, CentOS等。

下面我们通过CentOs镜像来查看镜像是如何构成的,下载镜像并查看镜像信息:


查看镜像信息:


镜像大小为231MB。

这个大小和我们传统的印象上的操作系统大小有很大差异,比如:我们常规安装操作系统WINDOWS,LINUX一般认为至少需求几GB左右大小的空间,为什么容器的操作镜像可以做到这么小?

一般初学者可能对于这个差异都会比较疑惑,后面我们就从LINUX的内核组成上来说明:


Linux操作系统的由内核空间和用户空间组成,如下图所示:

【图示参考链接:https://www.jianshu.com/p/30a140b63fbe】

rootfs

内核空间是kernel, Linux刚启动的时候会加载bootfs文件系统,之后bootfs文件会被卸载掉。

用户空间的文件系统是rootfs, 包含我们熟悉的/dev,/proc,/bin等目录。

对于BASE镜像而言,底层直接使用HOST的KERNEL,只需要使用ROOTFS就可以。

对于一个精简的OS,rootfs可以很小,只需要包括基本的命令、工具和程序库就行。相比其它的LINUX版本,CENTOS的ROOTFS已经较大,ALPINE版本还不到10MB。

用户正常使用的操作系统,除了ROOTFS还会装很多软件、服务和桌面支持的工具等,需要好几个GB就可以理解。

base镜像提供的最小安装的LINUX发行版

CentOs镜像的DOCKERFILE内容如下图所示:

https://github.com/CentOS/sig-cloud-instance-images/blob/b2d195220e1c5b181427c3172829c23ab9cd27eb/docker/Dockerfile

FROM scratch

ADD centos-7-x86_64-docker.tar.xz /

LABEL \

    org.label-schema.schema-version="1.0" \

    org.label-schema.name="CentOS Base Image" \

    org.label-schema.vendor="CentOS" \

    org.label-schema.license="GPLv2" \

    org.label-schema.build-date="20201113" \

    org.opencontainers.image.title="CentOS Base Image" \

    org.opencontainers.image.vendor="CentOS" \

    org.opencontainers.image.licenses="GPL-2.0-only" \

    org.opencontainers.image.created="2020-11-13 00:00:00+00:00"

CMD ["/bin/bash"]

第二行ADD指令就是添加到镜像TAR包就是CENTOS7的ROOTFS。在制作镜像时,这个包会自动解压到/目录下,生成/DEV、/PROC、/BIN等目录。

可在DOCKER HUB里查看到DOCKERFILE的具体信息。

运行运行多种LINUXOS

不同的LINUX发行版本主要是ROOTFS不同。

比如UBUNTU 14.04使用的是UPSTART管理服务,APT管理软件包;CENTOS7使用的是SYSTEMD和YUM。两个系统仅是在用户空间上有所区别,实际内核LINUX KERNEL差异不大。

DOCKER可以同时支持多种LINUX镜像类型,模拟出多个操作系统的环境。


上图上DEBIAN和BUSYBOX(一种嵌入式LINUX系统)上层提供各自己的ROOTFS,底层共用DOCKER HOST的KERNEL。

这里有一个关键内容需要说明:

(1)BASE镜像只是在用户空间与发行版一致,KERNEL版本与发行版本是不同的。

例如:CENTOS 7使用的是3.X.X的KERNEL,如果DOCKER HOST是UBUNTU 16.04(比如:我们的实验环境),那么在CENTOS容器中使用的实际上是HOST 4.X.X的KERNEL,如图所示:


与mac本地的虚拟机版本保持一致:

通过查看DOCKER DESKTOP里以下的版本信息可以得到:


运行的LINUXKIT版本为:5.10.124


[MAC上学习K8S系列:进入DOCKER FOR MAC的宿主机:LINUXKIT]

简面言之是:MAC不是直接运行LINUX内核,而是在MAC上使用HYPERVISOR虚拟出一个主机,运行的LINUXKIT容器来模拟LINUX操作系统。

https://cloud.tencent.com/developer/article/2064832

以上实验可以看出:

(1)HOST KERNEL为:LINUXKIT - 5.10.124

(2)启动并进入CENTOS容器。

(3)验证容器是CENTOS7.

(4)容器的KERNEL版本与HOST一致。

第二重要的特性是:容器【只能】使用HOST的KERNEL,并且不能修改。

所有的容器都是共用HOST的KERNEL,在容器中没有办法对KERNEL进行升级。如果容器对于KERNEL版本有要求(比如:某个应用仅能在固定的KERNEL版本下运行),则不建议使用容器,这种场景虚拟机可能更合适。

1.3 镜像的分层结构

docker 支持扩展现有的镜像,创建新的镜像。

DOCKER HUB中99%的镜像都是在BASE镜像中安装和配置需要的应用构建出来。比如,我们构建一个新的镜像,DOCKERFILE如图所示:

图片链接:

https://blog.csdn.net/runner668/article/details/80955381?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-1-80955381-blog-126758604.pc_relevant_vip_default&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-1-80955381-blog-126758604.pc_relevant_vip_default&utm_relevant_index=1

# Version: 0.0.1

FROM debian                1.新镜像不再是从 scratch 开始,而是直接在 Debian base 镜像上构建。

RUN apt-get update && apt-get install -y emacs        2.安装 emacs 编辑器。

RUN apt-get install -y apache2            3.安装 apache2。

CMD ["/bin/bash"]              4.容器启动时运行 bash。


上述命令主要几个操作:

1)新镜像不再是从scratch开始,而是直接在Debian base镜像上构建。

2)安装emacs编辑器。

3)安装apache2。

4)容器启动时运行bash。

可以看到镜像就像叠罗汉一样一层层的叠在一起。每安装一个新的工具或者软件就是在现有的基础上增加一层。

大家可能有一个问题,为什么容器会使用这种模式来实现?主要的目的是为了共享资源;

比如:Docker Host上仅需求保存一份BASE的镜像,如:CENTOS7.0等,其它应用在使用的时候可以基于这个镜像进行构建,提供给所有的服务使用。往上叠加的新的一层可以存为新的镜像,例如:安装VIM服务的CENTOS7.0镜像,后续可以选择是基础的BASE镜像,还是有指定工具的镜像作为BASE镜像。

叠次多了之后可能会有一个新的问题,这么多容器使用同一样BASE镜像,那是不是会影响基础镜像的内容。例如:容器A使用的BASE镜像,容器B,C也使用BASE镜像,A调整了/BIN目录里文件,是否会影响B,C容器。

答案是不会:修改会变限制在单个容器内,这个就是容器中非常重要的“COPY-ON-WRITE”特性。笔者认为这一点是容器技术设计上比较巧妙的一点:通过这个特性,无需生成新容器都“复制”一份以前的镜像, 节约存储空间,而是使用关联与映射的方式利用现有的镜像资源。实际我们可能比较关注的是性能问题,通过共享如何保证底层的镜像能支撑上层的应用。

https://zhuanlan.zhihu.com/p/28678299

1.3.1 可写的容器

当容器启动时,新的可写层会被加到镜像的顶部。

这一层通常被称为“容器层”,“容器层”之下的都被称之为“镜像层”。


所有的容器的改动无、无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层中是可写的,容器层以下的镜像层都是只读的。下面我们简单聊下镜像层的细节:

镜像层次可以很多、所有镜像层会联合在一起形成统一的文件系统。如果不同层中有一个相同的路径的文件,比如/bin, 上层的/bin会覆盖下层的/bin, 也就是用户只能访问的文件/bin中。在容器层中,用户看到是叠加之后的文件系统。

1)添加文件:在文件创建时,新文件被添加到容器中。

2)读取文件:在容器上读取某个文件时,DOCKER会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。

3)修改文件:大容器中修改已存在的文件时,DOCKER会从上到下依次在各镜像层中找到此文件。一旦找到,立即将其复制到容器层,然后修改之。

4)删除文件:在容器中删除文件时,DOCKER也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。

只有当需要修改时才复制一份数据,这种特性被称作COPY-ON-WRITE。可见,容器层保存的是镜像变化的部分,不会对镜像本身作任何的修改。

以上功能就回答了前面所提出的问题:容器层记录对镜像的修改,所有的镜像都是只读的,不会被容器修改,所以镜像可以被多个容器共享。

二,构建镜像

对于DOCKER用户来说,使用现成的官网镜像是比较理想的方式。大部分的常用数据库、中间件、应用软件等都有现成的DOCKER官方镜像或他人和组织创建的镜像,我们只需要稍作配置就可以使用。

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

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

1)自己开发的应用程序,比如:各个微服务的组件、中心等。

2)需求镜像中加入特定的功能,比如:官方镜像几乎都不提供SSH(可能出于安全考虑等)。

后面我们将简单说明下镜像构建的几种方法,通过实验与验证,我们可以更清晰的理解分层的思路和结构。

DOCKER提供了两种构建镜像的方法:DOCKER COMMIT命令与DOCKERFILE构建文件。

2.1 docker commit

Docker Commit命令是创建新镜像最直观的方法,主要包含三个步骤:

1)运行容器。

2)修改容器。

3)将容器保存为新的镜像。

举个例子:在UBUNTU BASE镜像里安装VI并保存为新镜像。

(1)运行容器

docker run -it ubuntu


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

(2) 安装VI

确认VI没有安装,如下图所示:


在容器中安装VI:

运行命令

apt-get install -y vim


如果有E:Unable to locate package vim报错,则需要更新apt-get,运行:apt-get update

参考文章:

https://blog.csdn.net/mameng1988/article/details/83782831

确认后再安装VIM命令:

VIM安装成功后运行,实际运行情况如下:

(3)保存为新镜像

在新窗口中查看当前运行的容器,如下图所示:

容器为我们随机分配的名字为:“紧张的图灵”

执行docker commit命令将容器保存为镜像:

新的镜像名称为:ubuntu-with-vi

查看新的镜像属性信息:


从大小上可以看到新的镜像因为安装了工具而变大,以前为77.8MB,新的镜像大小为175MB。

从新的镜像直接创建容器:


可以看到新的镜像创建的容器可以直接运行VI。

以上演示了如何用COMMIT创建新的镜像。这种方式不是DOCKER官网推荐的构建镜像的方式,原因如下:

1,这是手工创建镜像的方式,容易出错,效率低且可重复性弱。比如如果需要在DEBIAN BASE镜像中加入VI,还是需要重复执行以上操作。

2,更重要的:使用者并不知道镜像是如何创建出来的,里面是否有恶意程序或者病毒,存在安全隐患。

为什么不推荐的方法咱们也需要发时间学习?原因是:推荐的方式DOCERFILE构建镜像,底层实际也是DOCKER COMMIT一层层构建起来的新镜像。学习DOCKER COMMIT能够帮助我们更加深入的理解构建过程中的分层结构。

2.2 Dockerfile

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

1, 第一个DOCKERFILE

FROM ubuntu

RUN apt-get update && apt-get install -y vim


1)当前目录为:firstDockerfile

2)  通过vim写入Dockefile文件

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

4)从社步开始就是正式的镜像构建过程。首先DOCKER将BUILDER CONTEXT中所有的文件发送给DOCKER DAEMON。BUILD CONTEXT为镜像构建提供所需要的文件或目录。

DOCKERFILE中的ADD、COPY等命令可以将BUILD CONTEXT中的文件添加到镜像。此例中,BUILD CONTEXT为当前目录/firstDockerfile, 该目录下的所有文件和子目录都会被发送到DOCKER DAEMON。

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

5)step1: 执行FROM命令,将Ubuntu作为base镜像。

6)step2:执行RUN命令,安装VIM。

7)启动临时容器,在容器中通过apt-get安装vim.

8)安装成功后,将容器保存为镜像。这一步类似于前面演示的docker commit命令。

9)删除临时容器。

10)镜像构建成功。

通过docker images查看镜像信息,如下图所示:


在上面执行步骤中,需要特别注意RUN指令的运行效果。DOCKER会在启动的临时容器中执行操作,并通过COMMIT保存为新的镜像。

2,查看镜像分层结构

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


这个新镜像层由RUN apt-get udpate && apt-get install -y vim生成。这一点通过docker history命令可以查看,这个结果也是DOCKERFILE执行的过程。通过上述结果也可以看出前面提到的镜像分层的含义:即当前ubuntu-with-vi-dockerfile是通过共享ubuntu的镜像来创建一个新镜像,大小为97mb,每一层由上至下排列。

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

3,镜像的缓存特性

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

在前面的验证DOCKERFILE中添加一个新的语句,往镜像中复制一个文件,如下图所示:


FROM ubuntu

RUN apt-get update && apt-get install -y vim

COPY newfile /

1) 确认newfile目录已存在。

2)重点在于:之前运行过相同的RUN命令,这次直接使用缓存中的镜像层。

3)执行COPY指令。

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

在ubuntu-with-vi-dockefile镜像上直接添加一层就得到新的镜像ubuntu-with-vi-dockerfile-2

如果我们不希望使用缓存,则可以在DOCKER BUILD命令中加上--NO-CACHE参数。

DOCKERFILE每个指令都会创建一个镜像层,上层依赖于下层。只要某一层发生变化,上面所有层的缓存都会失效。

如果我们改变DOCKERFILE的执行顺序,或者修改添加指令,都会使缓存失效。举例说明,比如前面提到的RUN命令和COPY命令顺序调换。

FROM ubuntu

COPY newfile /

RUN apt-get update && apt-get install -y vim

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


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

除了构建时使用缓存,DOCKER在下载镜像时也会使用。例如我们下载HTTPD镜像,如下图所示:


第二次在docker pull命令运行的时候会显示已经存在,不需要再下载。对于分层结构中已有的镜像层,如果本地已完成下载则不需要重复下载。

4,调试DOCKERFILE

总结通过DOCKEFILE构建镜像的过程如下:

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

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

3)执行类似DOCKER COMMIT操作,生成一个新的镜像层。

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

5)重复2-4步骤,直到DOCKERFILE中所有的指令执行完毕。

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

我们来做一个小实验,DOCKERFILE内容如下图所示:

FROM busybox

RUN touch tmpfile

RUN /bin/bash -c echo "continue to build..."

COPY newfile /

执行docker build, 如下图所示:


通过docker build --progress=plain . 查看详细运行的步骤。MAC上运行的详细步骤与LINUX会有些差异,LINUX运行可以通过中间过程的镜像来运行容器,具体步骤与截图后续补充。


5, Dockerfile常用指令

5.1 COPY

复制指令,从上下文目录中复制文件或者目录到容器里指定路径。

格式:

COPY [--chown=:] <源路径1>...  <目标路径> COPY [--chown=:] ["<源路径1>",...  "<目标路径>"]

[--chown=:]:可选参数,用户改变复制到容器内文件的拥有者和属组。

<源路径>:源文件或者源目录,这里可以是通配符表达式,其通配符规则要满足 Go 的 filepath.Match 规则。例如:

COPY hom* /mydir/ COPY hom?.txt /mydir/

<目标路径>:容器内的指定路径,该路径不用事先建好,路径不存在的话,会自动创建。

5.2 ADD

ADD 指令和 COPY 的使用格类似(同样需求下,官方推荐使用 COPY)。功能也类似,不同之处如下:

ADD 的优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。

ADD 的缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定。

5.3 CMD

类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:

CMD 在docker run 时运行。

RUN 是在 docker build。

作用:为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。

注意:如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。

格式:

CMD   CMD ["<可执行文件或命令>","","",...]  CMD ["","",...]  # 该写法是为 ENTRYPOINT 指令指定的程序提供默认参数

推荐使用第二种格式,执行过程比较明确。第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并且默认可执行文件是 sh。

5.4 ENTRYPOINT

类似于 CMD 指令,但其不会被 docker run 的命令行参数指定的指令所覆盖,而且这些命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。

但是, 如果运行 docker run 时使用了 --entrypoint 选项,将覆盖 ENTRYPOINT 指令指定的程序。

优点:在执行 docker run 的时候可以指定 ENTRYPOINT 运行所需的参数。

注意:如果 Dockerfile 中如果存在多个 ENTRYPOINT 指令,仅最后一个生效。

格式:

ENTRYPOINT ["","","",...]

可以搭配 CMD 命令使用:一般是变参才会使用 CMD ,这里的 CMD 等于是在给 ENTRYPOINT 传参,以下示例会提到。

示例:

假设已通过 Dockerfile 构建了 nginx:test 镜像:

FROM nginx ENTRYPOINT ["nginx", "-c"] # 定参 CMD ["/etc/nginx/nginx.conf"] # 变参

1、不传参运行

$ docker run  nginx:test

容器内会默认运行以下命令,启动主进程。

nginx -c /etc/nginx/nginx.conf

2、传参运行

$ docker run  nginx:test -c /etc/nginx/new.conf

容器内会默认运行以下命令,启动主进程(/etc/nginx/new.conf:假设容器内已有此文件)

nginx -c /etc/nginx/new.conf

5.5 ENV

设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。

格式:

ENV ENV = =...

以下示例设置 NODE_VERSION = 7.2.0 , 在后续的指令中可以通过 $NODE_VERSION 引用:

ENV NODE_VERSION 7.2.0 RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc"

5.6 ARG

构建参数,与 ENV 作用一致。不过作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量。

构建命令 docker build 中可以用 --build-arg <参数名>=<值> 来覆盖。

格式:

ARG <参数名>[=<默认值>]

5.7 VOLUME

定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

作用:

避免重要的数据,因容器重启而丢失,这是非常致命的。

避免容器不断变大。

格式:

VOLUME ["<路径1>", "<路径2>"...] VOLUME <路径>

在启动容器 docker run 的时候,我们可以通过 -v 参数修改挂载点。

5.8 EXPOSE

仅仅只是声明端口。

作用:

帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射。

在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

格式:

EXPOSE <端口1> [<端口2>...]

5.9 WORKDIR

指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。(WORKDIR 指定的工作目录,必须是提前创建好的)。

docker build 构建镜像过程中的,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。

格式:

WORKDIR <工作目录路径>

5.10 USER

用于指定执行后续命令的用户和用户组,这边只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。

格式:

USER <用户名>[:<用户组>]

5.11 HEALTHCHECK

用于指定某个程序或者指令来监控 docker 容器服务的运行状态。

格式:

HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令 HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令 HEALTHCHECK [选项] CMD <命令> : 这边 CMD 后面跟随的命令使用,可以参考 CMD 的用法。

5.12 ONBUILD

用于延迟构建命令的执行。简单的说,就是 Dockerfile 里用 ONBUILD 指定的命令,在本次构建镜像的过程中不会执行(假设镜像为 test-build)。当有新的 Dockerfile 使用了之前构建的镜像 FROM test-build ,这时执行新镜像的 Dockerfile 构建时候,会执行 test-build 的 Dockerfile 里的 ONBUILD 指定的命令。

格式:

ONBUILD <其它指令>

5.13 LABEL

LABEL 指令用来给镜像添加一些元数据(metadata),以键值对的形式,语法格式如下:

LABEL = = = ...

比如我们可以添加镜像的作者:

LABEL org.opencontainers.image.authors="paulin"

参考文章:

https://mp.weixin.qq.com/s/nbl_sBC3fjRfBrzTq7u9SA

https://www.runoob.com/docker/docker-dockerfile.html

三,RUN vs CMD vs ENTRYPOINT

RUN、CMD和ENTRYPOINT三个指令看起来很类似,后面简单说明下它们的区别:

(1)RUN:执行命令并创建新的镜像层,RUN常用于安装软件包。

(2)CMD:设置容器启动后默认执行的命令及其参数,但是CMD能够被DOCKER RUN后面的命令行参数替换。

(3)ENTRYPOINT:配置容器启动时运行的命令。

3.1 Shell和Exec格式

我们可用两种方式指定RUN、CMD和ENTRYPOINT要运行的命令:Shello格式和Exec格式,二者在使用上有细微的区别。

Shell格式:

例如:

RUN apt-get install python3

CMD echo "Hello world"

ENTRYPOINT echo "Hello world"

当指令执行时,shell格式底层会调用/bin/sh -c [command]。例如下面的Dockerfile片段:

ENV name Paulin Cao ENTRYPOINT echo "Hello,$name"

执行docker run [image]将输出:

Hello, Paulin Cao

注意其中的变量name已经被值Paulin Cao替换。

下面来看下Exec格式:

["executable","param1","param2",...]

例如:

RUN ["apt-get","install","python3"]

CMD ["/bin/echo","hello world"]

ENTRYPOINT ["/bin/echo","Hello world"]

当指令执行时,会直接调用[command], 不会被shell解析。

例如下面的Dockerfile片段:

ENV name Paulin Cao ENTRYPOINT ["/bin/echo","Hello, $name"]

运行容器将输出:

Hello, $name

注意以上验证中name没有被替换。

如果希望使用环境变量,修改Dockerfile:

ENV name Paulin Cao ENTRYPOINT ["/bin/sh", "-c", "echo Hello, $name"]

运行容器将输出:

Hello, Paulin Cao

CMD和ENTRYPOINT推荐使用Exec格式,因为指令可读性更强。RUN 则可以使用两程方式都可以。

3.2 RUN

RUN指令通常用于安装应用和软件包。

RUN在当前镜像的顶部执行命令,并创建新的镜像层。Dockerfile中常常包含多个RUN命令。

Run有两种格式:

(1)Shell格式:RUN command

(2)Exec格式:RUN ["executable","param1","param2"]

下面是使用RUN安装多个包的例子:

RUN apt-get update && apt-get install-y\bzr\cvs\git\merucrial\subversion

注意:apt-get update和apt-get install被放在一个RUN指令中执行,这样能够保证每次安装的是最新的包。如果apt-get install在单独的RUN中执行,则会使用apt-get update创建镜像层,而这一层可能是以前的缓存镜像。

3.3 CMD

CMD指令允许用户指定容器的默认执行的命令。

此命令会在容器启动且docker run没有指定其他命令时运行。

如果docker run指定了其他命令,CMD指定的默认命令被忽略。

如果Dockerfile有多个CMD指令,只有最后一个CMD有效。

CMD有三种格式:

(1)Exce格式:CMD["executable", "param1","param2"]

这个是CMD的推荐格式。

(2)CMD ["param1","param2"]为ENTRYPOINT提供额外的参数,此时ENTRYPOINT必须使用Exec格式。

(3)Shell格式: CMD command param1 param2

Exec和Shell格式前面已介绍过。

第二种格式 CMD ["param1""param2"]需要与Exec格式的ENTRYPOINT指令配合使用,用途是为ENTRYPOINT设置默认参数,后续在讲ENTRYPOINT时举例细化说明。

下面看下CMD是如何工作的。Dockerfile片段如下:

CMD echo "Hello world"

运行容器docker run -it [image]将输出:

Hello world

但当后面加上一个命令,比如:docker run -it [image] /bin/bash, CMD会被忽略掉,命令bash将被执行:

root@xxxx:/#

3.4 ENTRYPOINT

ENTRYPOINT指令可以让容器以应用程序或服务的形式运行。

ENTRYPOINT看起来和CMD很像,它们都可以指定要执行的命令和参数。不同的地方在于ENTRYPOINT不会被忽略,一定会被执行,即使运行docker run时指定了其它命令。

ENTRYPOINT两种格式:

(1)Exec格式:ENTRYPOINT ["executable","param1","param2"] 这是ENTRYPOINT的推荐格式。

(2)Shell格式:ENTRYPOINT command param1 param2

为ENTRYPOINT选择格式时需要小心,两种格式的差别很大。

1,Exec格式

ENTRYPOINT的Exec格式用于设置执行的命令及其参数,同时可通过CMD提供额外的参数。

ENTRYPOINT中参数始终会被使用,而CMD的额外参数可以在容器启动时动态替换掉。

比如下面的Dockerfile片段:

ENTRYPOINT ["/bin/echo","Hello"] CMD ["world"]

当容器通过docker run -t [image]启动时,输出为:

Hello world

如果通过docker run -it [image] PaulinCao启动,则输出为:

Hello PaulinCao

2, Shell格式

ENTRYPOINT的Shell格式会忽略任务CMD或docker run提供的参数。

四,分发镜像

前面章节已经简要说明如何构建镜像,后面我们就来验证如何在不同的主机上运行镜像:

(1)使用相同的Dockerfile在其它的主机上构建镜像。

(2)将镜像上传至公共的REGISTRY(比如:DOCOKER HUB或者阿里云镜像仓库),其它主机直接下载使用。

(3)搭建私有化REGISTRY供本地主机使用。

第一种方式前面三节已演示比较多,第四章我们重点看第(2)、(3)两种方式如何推进使用。

4.1 为镜像命名

在分发镜像之前,首先都得给镜像命名。

当我们执行DOCOKER BUILD命令时已经为镜像取名,例如下面示例:

docker bukld -t ubuntu-with-vi

ubuntu-with-vi 就是镜像的名字。通过docker images 可以查看镜像的信息,如下图所示:


ubuntu-with-vi 对应的是RESPOSITORY, 还有一个叫lastest的TAG字段。

具体的一个镜像的名字由两个部分组成:repository和tag。

[image name] = [repository]:[tag]

如果执行docker build 时没有指定tag, 默认值lastest。效果相当于:

docker build -t ubuntu-with-vi:lastest

tag 常用于描述镜像的版本信息,当然也可以任意字符串,如下图所示:


1,小心lastest tag

千万别被lastest tag给误导,lastest 看起来是指的最新版的意思,实际没什么特别的含义。当没指定镜像tag 时, Docker 会默认值lastest。

虽然Docker Hub上很多repository将lastest作为最新稳定版本的别名,但实际只是一种约定,而不是强制规定。

所以我们在使用镜像时最好还是避免使用latest, 明确指定某个tag, 比如:httpd:2.3, ubuntu:xenial.

2, tag使用最佳实践

实际可以把容器从某种意义上理解为传统的光盘授权软件,不同的系统版本可以更好让用户理解版本内容。例如:WINDOWS XP, WINDOWS 7等。

高效的版本命名方案可以让用户清楚地知道当前使用镜像的版本,同时还可以保持一定的灵活性。

每个repository可以有多个tag, 而多个tag可能对应的是同一个镜像。下面通过例子为大家简要说明Docker 社区普遍使用的tag方案。

假设我们现在发布一个镜像paulinimage, 版本为v1.0.1, 那么我们可以给镜像打上4个tag: 1.0.0、1.0、1 和latest, 我们通过以下命令给镜像打tag。

docker tag paulinimage-v1.0.1 paulinimage:1 docker tag paulinimage-v1.0.1 paulinimage:1.0 docker tag paulinimage-v1.0.1 paulinimage:1.0.1 docker tag paulinimage-v1.0.1 paulinimage:latest

过了一段时间,我们发布v1.0.2。这时我们可以打上1.0.2的tag, 并将1.0、1和latest从v1.0.1移到v.1.0.2, 如下图所示:


下图为调整后的版本:


命令为:

docker tag paulinimage-v1.0.2 paulinimage:1 docker tag paulinimage-v1.0.2 paulinimage:1.0 docker tag paulinimage-v1.0.2 paulinimage:1.0.2 docker tag paulinimage-v1.0.2 paulinimage:latest

之后如果v2.0.0发布。这时可以打上2.0.0、2.0 和 2的tag, 并将latest 移到v2.0.0, 如下图所示:


命令为:

docker tag paulinimage-v2.0.0 paulinimage:2 docker tag paulinimage-v2.0.0 paulinimage:2.0 docker tag paulinimage-v2.0.0 paulinimage:2.0.0 docker tag paulinimage-v2.0.0 paulinimage:latest

这种tag方案使用镜像的版本很直观,用户在选择的时候比较灵活:

1)paulinimage:1始终指向1这个分支中最新的镜像。

2)paulinimage:1.0始终指向1.0.x 中最新镜像。

3)paulinimage:latest始终指向所有版本中最新的镜像。

4)如果使用特定的版本,可以选择使用paulinimage:1.0.1, paulinimage:1.0.2或paulinimage2.0.0。

Docker Hub上有很多repository 都采用这种方案。

4.2 使用公共Registry

保存和分发镜像最直接的方法就是使用Docker Hub。

Docker Hub是Docker公司维护的公共Registry。用户可以将自己的镜像保存到Docker Hub免费的repository中。如果不希望别人访问自己的镜像,可以购买云厂商的私有repository。

除了Docker Hub, quay.io 是另外一个公共Registry, 提供Docker Hub类似的服务。

下面介绍如何用Docker Hub存取我们的镜像。

1)首先得在Docker Hub上注册一个帐号。

2)在Docker Host上登录,如下图所示:


输出用户名和密码后登录成功。

3)修改镜像的repository, 使用与Docker Hub的帐号匹配。

Docker Hub为了区分不同的用户的同名镜像,镜像的registry中要包含用户名,完整格式为:[username]/xxx:tag。可以通过docker tag命令重命名镜像,如下图所示:


Docker官方自己维护的镜像没有用户名,比如:httpd。

通过docker push 将镜像上传到Docker Hub, 如下图所示:


Docker会上传镜像的每一层。因为paulincao/ubuntu:v1.0.1这个镜像实际上跟官方的ubuntu镜像一模一样,Docker Hub上已有了全部的镜像层,所以真正上传的数据很少。同样的,如果我们的镜像是基于base镜像的,也只有新增加的镜像层会被上传(如本实例上,原始镜像大小为175MB,实际上传大小仅为74MB左右)。如果想上传同一个repository的所有镜像,省略tag部分就可以。例如:

docker push paulincao/ubuntu

1) 登录https://hub.docker.com, 在Public Repository 中就可以看到上传的镜像,如下图所示


如果要删除上传的镜像,只能在Docker Hub界面上操作。

2)这个镜像可被其他Docker host下载使用,如下图所示:


4.3 搭建本地的Registry

Docker Hub虽然非常方便,但是有些限制,如:

1)需要公网连接,下载和上传速度受公网速度影响较大。

2)上传到Docker Hub的镜像任何人都能访问,虽然可以私有repository, 但是需要收费。

3)安全原因很多组织不允许将镜像放到外网。

解决方案就是搭建本地的Registry。

Docker已将Regsitry 开源,在Docker Hub上有官方的镜像registry。 下面我们就在Docker上运行自己的registry。

1,启动registry容器

我们使用的镜像是registry:2, 如下图所示:

docker run -d -p 5000:5000 -v /Users/paulincao/Documents/work/dev/DockerLearning/imageDebug/registry:/var/lib/registry registry:2


-d: 后台启动容器。

-p: 将容器的5000端口映射到host的5000端口。5000是registry服务端口。端口映射在后续网络章节会详细说明。

-v: 将容器/var/lib/registry 目录映射到host 的 本地目录,用于存放镜像数据。-v 的使用我们在容器存储章节详细讨论。

通过docker tag重命名镜像,使之与registry 匹配,如下图所示:

docker tag paulincao/ubuntu:v1.0.1 localhost:5000/paulincao/ubuntu:v1.0.1

我们在镜像的前面加上运行registry的主机名称和端口。


前面我们讨论了镜像名称由respository和tag 两部分组成。而repository的完整格式为:

[registry-host] :[port] / [username] /xxx

只有Docker Hub上的镜像可以省略 [registry-host] :[port] 。

2, 通过docker push 上传镜像

通过docker push上传镜像如图所示:

docker push localhost:5000/paulincao/ubuntu:v1.0.1


现在可以通过docker pull本地registry 下载镜像,如下图所示:

docker pull localhost:5000/paulincao/ubuntu:v1.0.1


除了镜像的名称长一些(包含registry host和port),其它使用方式和Docker Hub一样。

以上是搭建本地registry的简要步骤。如果需要生产级应用,如认证,https传输安全等,具体可以参考官方文档:

https://docs.docker.com/registry/configuration/

你可能感兴趣的:(Docker技术知识分享(三) - 镜像)