镜像是容器技术的基础,容器是镜像的实例,下面我们就具体来看下镜像是如何组成和运行的。
一,镜像内部的结构
我们日常使用已打好的镜像则下载配置就可以,而如何我们涉及项目中的开发和自制“烧制”镜像则需要更加深入的了解镜像的构成。
通过镜像知识的学习,也可以进一步理解为什么镜像的运行仅需要占用相对较少的资源。
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=
[--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
推荐使用第二种格式,执行过程比较明确。第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并且默认可执行文件是 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
以下示例设置 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格式:
例如:
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/