docker基础学习之镜像篇

docker初探

  1. 容器技术原理
  2. 为什么使用容器

    与传统软件行业的开发、 运维相比,容器虚拟化可以更高效地构建应用,也更容易管理维护。举个简单的例子, 常见的LAMP组合开发网站,按照传统的做法自然是各种安装,然后配置,再然后测试,发布, 中间麻烦事一大堆,相信不少同行都深有体会。

    过了一段时间,用户群体增加,服务器需要搬迁到更合适的机房,往往需要再执行一次以前的部署步骤, 还包括数据的导出导人, 极大地花费了运维人员的时间。最可怕的是搬迁后因为一些不可预知的原因导致软件无法正常运行,只能一头扎进代码中找Bug

    如果使用容器技术,运维只需要一个简单的命令即可部署 整套LAMP环境,并且无需复杂的配置与测试,即便搬迁也只是打包传输即可,即使在另一台机器上,软件也不会出现 "水土不服" 的情况。这无疑节省了运维人员的大量时间。

    而对于开发来说,一处构建,到处运行大概是梦寐以求的事情,这也是很多跨平台语言的宣传标语之一,但是不管是怎样的跨平台语言在很多细节上都需要不少调整才能运行在另一个平台上。但容器技术则不 样, 开发者可以使用熟悉的编程语言 开发软件 ,之后用容器技术 打包构建,便可以一键运行在所有支持该容器技术的平台上。

     

  3. 容器原理

    容器的核心技术是Cgroup与Namespace,在此基础上还有一些其他工具共同构成容器技术。从本质上来说容器是宿主机上的进程,容器技术通过Namespace实现资源隔离, 通过Cgroup 实现资源控制, 通过rootfs实现文件系统隔离,再加上容器引擎自身的特性来管理容器的生命周期。

    简单地说,这里所说的Docker的早期 其实就相当于LXC的管理引擎,LXC是Cgroup的管理工具, Cgroup是Namespace的用户空间管理接口。Namespace是Linux内核在task_struct中对进程组管理的基础机制。

    1. Namespace

    想要实现资源隔离,第一个想到的就是chroot命令,通过它可以实现文件系统隔离,这也是最早的容器技术。但是在分布式的环境下,容器必须要有独立的IP、端口、路由等,自然就有了网络隔离。同时,也需要考虑进程通信隔离权限隔离等,因此一个容器基本上需要做到6项基本隔离,也就是Linux内核中提供的6种Namespace隔离.

    当然,完善的容器技术还需要处理很多工作。

    对Namespace的操作,主要是通过clone、setns、unshare这三个系统调用来完成的。

    clone可以用来创建新的Namespace。clone有一个flags参数,这些flags参数以CLONE NEW*为格式,包括CLONENEWNS、CLONE NEWIPC、CLONE NEWUTS、 CLONE NEWNET, CLONE NEWPID和CLONE NEWUSER,传入这些参数后,由clone创建出来的新进程就位于新的Namesapce之中。

    因为Mount Namespace是第一个实现的,Namesapce当初实现没有考虑到还有其他Namespace出现,因此用了CLONE_NEWNS的名字,而不是CLONE_NEWMNT之类的名字,其它CLONE_NEW*都可以看名字知道用途。

    那么,如何为已有的进程创建新的Namesapce呢,这就需要unshare。使用unshare调用的进程会被放到新的Namespace里。

    而set ns则是将进程放到已有的Namespace中,docker exe命令的实现就是set ns。

    事实上,开发Namespace的主要目的之一是实现轻量级的虚拟化服务,在同一个Namespace下的进程可以彼此响应,而对外进程隔离,这样在一个Namespace下进程仿佛处于一个独立的系统环境中,以达到容器的目的。

    先了解一下进程的Namespace

    这里的$$是指当前的进程的ID号,可以看到后面的ID,这表示当前进程指向的Namespace,当两个进程指向同一串数字时,表示它们处于同一个Namespace下。

     

    1. 认识Cgroup

    Cgroup是 controlgroups的缩写, 是Linux内核提供的一种可以限制、 记录、 隔离进程组(process groups)所使用的物理资源(如:CPU,内存,IO等等 )的机制。它最初由Google的工程师提出,后来被整合进Linux内核。 Cgroup也是LXC为实现虚拟化所使用的资源管理手段,因此可以说没有Cgroup就没有LXC 。

    目前,Cgroup有一套进程分组框架,不同资源由不同的子系统控制。一个子系统就是一个资源控制器, 比如CPU子系统就是控制CPU时间分配的一个控制器。 子系统必须附加(attach)到一个层级上才能起 作用 ,一个子系统附加到某个层级以后,这个层级上的所有控制族群(control group ) 都受这个子系统的控制。

    Cgroup各个子系统的作用如下。

    Blkio:为块设备 设定 输入/输出限制,比如物理设备(磁盘、 固态硬盘、 USB,等等)。

    Cpu:提供对CPU的Cgroup任务访问。

    Cpuacct:生成Cgroup中任务所使用的CPU报告。

    Cpuset:为Cgroup中的任务分配独立的CPU(在多核系统)和内存节点。

    Devices:允许或者拒绝Cgroup中的任务访问设备。

    Freezer:挂起或者恢复Cgroup中的任务。

    Memory:设定Cgroup中任务使用的内存限制,并自动生成由那些任务使用的内存资源报告。

    Net_cls:使用等级识别符(classid )标记网络数据包, 可允许Linux流量控制程序(tc)识别从具体Cgroup中生成的数据包。

    Net_prio:设置进程的网络流量优先级。

    Huge_tlb:限制HugeTLB的使用。

    Perf_event:允许 Perf工具基于Cgroup分组做性能监测。

     

    1. 容器的创建

    系统调用clone创建新进程,拥有自己的Namespace。

    该进程拥有自己的PID,mount,user,net,ipc,uts namespace

     

  4. docker基础

    Docker是一个重新定义了程序开发测试、交付和部署过程的开放平台。在Docker的世界里,容器就是集装箱,我们的代码都被打包到集装箱里;Docker就是集船坞、货轮、装卸、搬运于一体的平台,帮你把应用软件运输到世界各地,并迅速部署。

  5. docker架构

    我们知道Docker是一个构建、发布、运行分布式应用的平台,Docker平台整体可以看成由Docker 引擎(运行环境+打包工具)、DockerRegistry ( API +生态系统)两部分组成。 其中Docker引擎 可以分为守护进程和客户端两大部分。Docker引擎的底层是各种操作系统以及云计算基础设施,而上层则是各种应用程序和管理工具,每层之间都是通过API来通信的。

    1. docker client

    Docker引擎可以直观地理解为在某一台机器上运行的Docker程序,实际上它是一个C/S结构的软件,有一个后台守护进程在运行,每次我们运行Docker 命令的时候实际上都是通过RESTful Remote API来和守护进程进行交互的,即使是在同一台机器上也是如此。

     

    可以使用docker version查看版本,可以看到Client和server就是上图的Docker CLI和docker daemon。

    1. Docker Daemon

    daemon就是一个守护进程,实际上它就是驱动整个docker的核心引擎,在0.9版本前docker客户端和服务端是统一在一个二进制文件中,后来为了更好的管理,划分为四个二进制文件:docker,container,docker-container-shim和docker-runc.

     

    1. docker镜像

    Docker镜像是Docker系统中的构建模块(Build Component ),是启动一个Docker容器的基础。

    Docker镜像采用分层的结构构建,最底层是 bootfs,这是一个引导文件系统,一般用户很少会直接与其交互,在容器启动之后会自动卸载bootfs,bootfs之上是rootfs,rootfs是docker容器在启动时内部可见的文件系统,就是我们日常所见的"/"目录。

    Docker镜像使用了联合挂载技术和写时复制技术,关于这些内容会在下面章节详细介绍。利用这两项技术,Docker可以只在文件系统发生变化时才会把文件写到可读/写层,一层层叠加 ,不仅有利于版本管理,还有利于存储管理。

     

     

     

    1. docker容器

    在Docker 的世界中,容器是核心,是一个基于 Docker 镜像创建、包含为运行某一特定程序所有需要的OS,软件、配置文件和数据,是一个可移植的运行单元。不过从宿主机来看,它只是一个简单的用户进程而已。关于容器的知识,在稍后会有详细介绍,在这里读者只需要知道容器是从镜像创建的运行实例,它是一个独立的沙盒。

    容器很好地诠释了集装箱的理念,开发人员不用关心容器内部是什么应用,只管传输、运行即可,这是一种标准化的集装和运输方式,正因为 Docker 把容器技术进行了体验友好的封装,才使得容器技术迅速推广普及。

     

    1. docker仓库

    Github上有着海量的代码仓库,类似的,在Docker中,当开发者想要构建一个镜像或运行一个容器时,一般要先有 个现成的镜像才可以执行构建或者运行,而本地又没有该特定镜像时怎么办呢?Docker提出了Registry的概念,用户可以将自己的镜像上传到Registry上,如果是公开的,那么全世界的用户都可以拉取这个镜像来操作,可以说 Registry就是一个"软件商店"。Registry类似传统运输业中的船坞、中转站一样,是一个集中存放 "集装箱"(镜像)的地方。

    除了这两个官方的地址,用户还可以搭建自己私有的Registry,用来存储非公开的镜像。

     

  6. Docker镜像

    Docker最核心也是最基础的部分—镜像,docker镜像是docker整个体系中最基础的部分,docker镜像是容器的初始状态,docker镜像的构建,维护都对容器的运行有着极大的影。

  7. 认识镜像

    1.镜像结构

    上面命令中先是把镜像导出为tar归档文件,然后解压,最后可以看到hello-world这个镜像中一共有四个文件(夹),这些像乱码一样的文件夹其实是镜像的一个层(layer)。镜像包含着数据以及必要的元数据,这些数据就是层(layer),而元数据则是一些JSON文件,元数据是用来描述镜像的信息,包括数据之间的关系、容器配置信息等。

    上面解压的镜像所显示的每一个层(layer)文件夹意味着它是由一句Dockerfile命令生成的。在构建镜像的过程中,像Run、COPY、ADD、CMD等命令都会生成一个新的镜像层,一个镜像就是不断在上一个镜像层的基础上叠加上去的。为了更直观地了解一个镜像的历史,可以使用docker history看镜像的历史。

    可以看到镜像只有两句构建命令,第一句是copy可执行文件,第二句是cmd命令,负责在启动时执行的命令。构建过程由下到上,一层一层叠加,每一层的内容独立存储在镜像层中。

    镜像在本地存储目录为/var/lib/docker/,containers是存放容器的信息,image是镜像的信息,network是容器网络信息,plugins是插件信息,swarm是集群信息。volumes是数据卷信息。

    本地存储的镜像数据与层数据在image文件夹中是分开存储的,imagedb保存了本地全部镜像的元数据,而layerdb保存了本地镜像的全部镜像层。

     

    2.存储原理

    上面说到镜像内容与元数据是分开的,那么docker是如何把这些内容整合然后把一个完整镜像显示在用户眼前,以hello-world为例,通过docker inspect命令查看镜像的详细信息。

    注意rootfs中的信息,docker daemon首先通过image的元数据得知全部layer的ID,再根据layer的元数据梳理出顺序,最后使用联合挂载技术还原容器启动所需要的rootfs和基本配置信息。运行的容器实际上就像是在这些镜像层上新建一个动态的层。

    现在我们已经知道镜像是一种像"千层饼"一样的结构, 但问题是Docker是如何把这么多的镜像层统筹起来变为一个可运行的容器呢?这里就需要引入一项技术联合挂载。

    联合挂载会把多个目录挂载到同一个目录(甚至可能对应不同的文件系统)下, 并对外显示这些目录的整合形态。Docker中使用的AUFS ( AnotherUnionFS)就是一种联合文件系统。

    联合文件系统在日常使用的电脑中有一个地方经常会用到,那就是Linux系统的LiveCD,我们 使用发行版时一般都有一个LiveCD供用户体验。它的原理就是在原有的系统目录之上附加一层可读可写的文件层, 任何文件改动都会被写到这个文件层中, 这种技术就是写时复制。 关于写时复制的信息可以查看Overlay文件系统的资料。

    这里需要特别注意一点,因为不理解写时复制的特性,在以后构建镜像过程中,大部分新子都会有一个误区就是"删除一个文件必定会导致镜像体积变小" 。

    实际上并非如此, 举个简单的例子, 有一个镜像, 内部有一个 lOOMB 的文件,现在基于该镜像(FROM命令)构建一个新的镜像, 在构建过程中执行了删除那个 lOOMB 的文件的命令,那么现在镜像体积变小了吗?

    当然没有,因为根据联合挂载与写时复制的特点, 删除底层文件系统的文件或者目录时,会在上层建立一个同名的主次设备号都为0的字符设备,并不会删除底层文件系统的文件或者目录, 只是整合后的rootfs让用户看不到那些文件而已。

    所以正确的解决办法是从底层的文件系统着手,在最初的镜像层删掉 lOOMB 文件才是减少镜像体积的办法。

     

  8. 镜像操作
    1. 拉取镜像。

    拉取镜像的命令,通过docker pull不仅可以拉取docker hub的镜像,还可以通过指定仓库地址拉取私有仓库镜像。

     

    使用docker pull -a会把所有的标签都拉取到本地,使用—disable-content-trust=false会在拉取时校验镜像保证传输安全,默认是关闭。

    镜像拉取很简单,如果想获取其它用户的镜像,可以在docker hub上搜索。

     

    1. 镜像创建

    构建镜像

    docker bulid是构建镜像用到的重要命令。

    详情可以使用docker build –help查看。

    例子:构建一个镜像并打上标签,后面的"."是表示当前目录,docker构建镜像是要注意上下文的,dockerfile文件要放在知道的地方。

    镜像提交

    除了使用docker build构建镜像,还可以使用commit提交镜像。docker commit会把容器提交打包为镜像,这样提交的镜像会保存容器内的数据,而且第三方无法获取镜像的dockerfile,也就无法再构建一个一模一样的镜像出来,不推荐使用commit。

    但是在某些时候,我们需要使用docker commit来保存容器的状态,这个时候需要使用这个方法保存容器。下面举个例子。

    首先拉取一个镜像下来

    查看镜像列表

    使用这个镜像运行容器,并在容器里添加一个文本,注意使用docker run it进入容器退出后,容器也会停止。后面会说明不让其停止的办法。

    把上面创建的容器作为镜像提交并打上新的标签

    查看镜像,已有新的名字

    运行提交的镜像,并查看之前创建的文本,可以看到可以保存当时容器的状态。

    下图中可以看到随着退出容器,容器也停止了,执行start让它运行,可以看到已经是up状态,并可以进入。

     

    可以看到:提交的test容器保存了之前文件,docker commit参数如下:

    -a 添加作者信息

    -c 修改dockerfile指令

    -m 类似git commit -m提交修改信息

    -p 暂停正在commit操作。

     

    3.导入导出镜像。

    前面的使用过镜像导出的功能,现在来看具体的镜像导入导出用法,如果在两台主机之间需要传输镜像,一个办法就是把镜像推送到仓库,然后让另一台主机拉回来,但是这样有个中转,很麻烦,有时候我们不想把镜像发布到互联网中,而自己搭建私有镜像仓库需要诸多步骤,于是需要一组可以导入导出镜像的命令。

    导出镜像

    使用docker save可以导出镜像到本地系统;

    如下图所示:

    我们可以把这个文件解压,里面是一个基于Libcontainer标准的rootfs,使用runc也可以运行起来。如忘记了参数,还可以使用">"符号导出镜像。

    导入镜像

    使用docker load可以加载一个导出的镜像包到本地仓库。

    或者使用"<"符号

    4.删除镜像

    本地镜像多了,有些不要的,需要删掉,删除镜像的命令为docker rmi,删除镜像时不指定镜像的tag会删除镜像的latest标签。可以在命令后面接上多个镜像名称,删除多个镜像。

    使用docker rmi命令删除镜像时, 要确保没有容器使用该镜像,也就是说, 没有容器是使用该镜像启动的, 才可以删除,否则会报错。

    删除镜像时可以使用镜像的ID也可以使用镜像名称,docker rmi有一个参数-f, 该参数可以强制删除镜像, 即便有容器正在使用该镜像。 但是这样只会删除镜像标签, 不影响正在运行的容器,实际上只要容器还在运行, 镜像就不会被真正删除, 用户可以使用docker commit操作提交容器来恢复镜像。

    #删除一个镜像,默认会删除latest标签

    #删除一个标签

    镜像实际上是以ID为标准保存在Docker,中的,即使镜像没有使用标签, 镜像也是可以存在的,出现这种情况的原因有很多,例如强制删除了一个正在运行着容器的镜像,又或者构建的新镜像的tag覆盖了原来旧镜像的tag等。

    时间长了, 我们没有tag说明这些镜像是什么作用就会很难管理, 所以我们需要删除这些镜像,数量少时我们可以手动一条一条地删除, 数量多时我们可以配合Docker其他命令, 删除所有未打dangling标签的镜像:

    docker rmi $(docker image -q -f dangling=true)

    #删除所有镜像

    docker rmi $(docker image -q)

     

    5.发布镜像。

    因为docker hub是官方默认的仓库,镜像最多,一般会选择发布到这里。在推送之前需要登录docker hub。没有账号自己去申请一个。

    #登录docker hub

    #docker login

    查找镜像

    #docker search ubuntu

    #下拉镜像

    #docker pull ubuntu

    推送镜像

    #先打tag

    推送

    #docker push

    #查看

    docker search 17665439246/ubuntu

    发布到第三方仓库

    有时候需要国内的网络较快,会选择第三方仓库。

    这时候需要先给镜像打仓库tag

    如:docker tag ubuntu:latest reg.example.com/17665439246/ubuntu:18

    打上标签之后可以在docker images看到不同的镜像名称。虽然是两个镜像,但是空间依旧是一个镜像。

    然后就可以进行推送了。

     

  9. dockerfile详解

    多次提到可以使用Dockerfile构建镜像或者使用docker commit提交镜像, 这两种方法生成的镜像的最大区别在于前者可以通过一份简单的文本就把整个镜像慨括进去, 其他人只需要拿到Dockerfile就可以构建出一个 "一模一样" 的镜像, 而后者(使用docker commit)生成的镜像 ,其他人只能通过Registry或者导出导入的方式来传输镜像,非常不方便,而且其他人很难确定镜像内有什么,也无法构建一个"模样"的镜像出来,所以一般不推荐使用docker commit 的方式生成镜像。

    因此掌握使用Dockerfile构建镜像就很有必要了,Docker的内容并不多, 配合docker build使用,可以轻松地构建一个自己定制的镜像。本节内容将逐 解释Dockerfile的每条指令,然后扩展到镜像构建过程中的扩展,诸如 .dockerignore等。

    1. Dockerfile编写

    要写 一份Dockerfile,需要作者有一定的Linux命令行基础,因为从整体来看,Dockerfile就是一个自动化的Linux命令集。在写Dockerfile过程中,需要模拟一遍命令的运行过程,尽量减少因命令行写错而重新构建的情况,因为有时这很耗时。

    Dockerfile这个文件虽然可以命名为其他名字, 但是一般情况下不推荐修改Dockerfile这个文件名,除非同一个文件夹下存在多个Dockerfile文件。构建时加上-f指定该文件即可。如下:

    #docker build -t user/image:tag .

    #docker build -t user/image:new -f Dockerfile.new .

    上面的两句命令,第一句表示使用当前目录中的Dockerfile文件构建,第二句表示使用Dockerfile.new这个文件构建,两句命令都是使用当前目录作为构建的上下文跟目录。一般情况下我们使用Dockerfile这个默认的名字可以省去-f这个参数。

    一个标准的Dockerfile中应该包含命令,注释等,构建命令应该使镜像尽量干净,不留垃圾文件。Dockerfile的结构如下:

    #这是注释

    INSTRUCTION arguments

    如何写出一个构建不会出锚Dockerfile是新手首先要面对的问题。 因为Docker构建过程是无交互的,所以整个构建过程需要保证命令集能够 直持续不断地执行下去,完全的 "自动化"要求 Dockerfile编写者必须尽可能地考虑到所有可能的构建情况。

     

    因此在书写 Dockerfile 的过程中, 需要注意命令是否能够自动执行,遇到交互节点是否可以自动应答等。例如在使用apt-get install构建镜像时就会遇到 Y/n的提示,所以在写Dockerfile时必须加入-y参数。在安装依赖时不要执行软件升级操作, 比如apt-get upgrade等行为都是破坏镜像兼容性的做法, 可能会导致其他人构建时产生因为版本问题而构建失败的问题。

     

    此外,要注意Dockerfile指令书写的顺序,因为Dockerfile的构建过程是从上到下的,所以书写Dockerfile需要考虑到后面的命令执行情况,并适当调整命令的位置。比如在执行删除软件源命令之前一般要完成全部软件依赖的安装,不能发现缺少依赖再重新添加软件源进行安装(如果调整Dockerfile上面的命令,

    后面命令即使没有改动,构建时也会从改动处开始构建)。

     

    第三,要注意清理,这是很多用户在书写 Dockerfile 时没有注意到的地方,有时一个镜像在构建过程中会产生很多临时文件,很多时候在完成构建之后,临时文件也保留在了镜像之中,因此在 Dockerfile中般在最后写上清理系统的命令, 以保证镜像的体积。值得注意的是,并不是删除文件后镜像体积一定会减小,这要根据Docker的存储特点和Dockerfile文件来判断。

     

    最后是关于易读,有时候使用一条很长的命令,不要一行写到底,这样不容易阅读,遇到长命令可以使用"\"符号来连接,遇到几个命令连在一起,还可以使用"&&"的方式进行连接。

    #RUN echo 'hello world' \

    && echo 'hello'

    易读还包括适当地在 Dockerfile留下注释以及维护者的信息,这将有利于他人从中获得更全面的帮助。

    实际上一个复杂的Docker镜像,一次性写出执行成功的Dockerfile是不太现实的,往往需要编写者多次调试。幸好Docker构建过程有一个缓存,每一句命令(除了ADD和COPY)执行过后都会被缓存到本地中,直到镜像完全构建成功,也就是说,如果构建过程因为某一句命令构建失败,那么下一次构建时只需要从失败那一句命令开始构建,不需要从头到尾再构建一次。

    注意:Docker并不会去检查容器内的文件内容,比如RUN apt-get -y update, 每次执行时文件可能都不一样 ,但是Docker却认为命令一致,会继续使用缓存。 这样来,以后构建时都不会再重新运行apt-get -y update了。

    在Docker开始构建镜像时,Docker客户端会先在上下文目录寻找.dockerignore文件,根据.dockerignore文件排除上下文目录中的部分文件和目录,然后把剩下的文件和目录传递给Docker服务端开始构建。".dockerignore"语法与".gitignore"相同。

    对于大型项目在构建镜像时,一般不建议在项目的根目录放置Dockerfile 文件,因为上下文内容太过于庞杂,构建缓存会非常之大,解决办法有两种,一种是新建一个Docker相关的文件夹,改变构建上下文,构建时指定构建上下文, 以减少不必要的目录进入构建缓存。另一种办法就是使用 ". docker ignore" 文件,这里是一个使用" .dockerignore"文件的例子:

     

     

    #这是注释

    #一级子目录中排除其名称以temp开头的文件和目录。例如/dir/temp_text和/dir/temp_dir被排除去

    */temp*

    #同理,二级目录排除以temp开头的文件和目录

    */*/temp*

    #排除根目录中名称有temp字符的文件和目录

    temp?

    行开头!(感叹号)可用于排除例外。" .dockerignore"文件示例:

    * .md #上下文中排除所有md文件

    !README.md #不排除README.md文件

     

    特别指出文件:

    *.md #上下文中排除所有md文件

    !README * .md #包含README字符的md文件不排除

    README-password.md #特别指出README-password.md从上下文中排除

    注意:上面例子中最后两句不能写反, 因为Docker从上往下读,最后两句反过来会冲突,写反之后Docker会以最后一句为准。

    最后总结一下书写Dockerfile需要注意的几个问题:

    1. 镜像要轻量化,减少镜像的大小要从根本入手,基础镜像尽量选择简单而且稳定的发行版。减少软件依赖,仅安装需要的软件包以及最后要记得清理缓存。
    2. 使用.dockerignore排除无关文件。在大部分情况下,Dockerfile 会和构建所需的文件放在同一个目录中。为了提高构建的性能,应该使用.dockerignore来过滤掉不需要的文件和目录。
    3. 一个容器只做一件事,甚至一个容器只运行一个进程。例如一个动态网站容器,不应该把数据库,运行环境,负载器都放进容器中,这样容器就失去了意义。
    4. 减少构建命令。这样做的目的是减少镜像层数,这对于减少镜像体积有着极其重要的影响。例如整合多个Label、 ENV、 RUN标签,优化命令执行顺序等。
    5. 其使用反斜杠\连接跨行的命令,提高Dockerfile易读性。

     

    1. Dockerfile命令详解

    解析器命令

    解析器命令是可选的,它影响Dockerfile后续的处理方式。解析器命令不会向构建添加镜像层,不会显示构建步骤。解析器指令以注释形式写入Dockerfile中。单个命令只能使用一次,如果碰到注释,Dockerfile命令或空行,接下来出现的解析命令都无效,都被当做注释处理。还有,解析器命令不支持反斜杠跨行。

    解析命令以#开头,形式如下,虽然不区分大小写,但约定俗成使用小写:

    #directive=valuel

    #directive=value2

    FROM ImageName

    解析器命令形如注释,但不是注释,它必须写在Dockerfile 所有执行命令之前(也就是FROM命令前面),写在FROM命令下面的全部被认为是注释。

    当前只有一个解析命令:escape,用来设置转义或续行字符,这在Windows下是很有用的,例如下面的语句:

    COPY testfile.txt c:\\

    RUN dir c:\

    在windows下会被Docker解析成:

    COPY testfile.txt c:\RUN dir c:

    下面的例子就可以正常执行(转义字符为默认值,Windows下应为`):

    # escape=`

    FROM … …

    … …

    COPY testfile.txt c:\

    RUN dir c:\

    FROM

    FROM命令表示将来构建的镜像来自哪个镜像,也就是使用哪个镜像作为基础构建的,一般情况下Dockerfile都有基础镜像,FROM指令必须是整个Dockerfile的第一句有效命令。

    FROM的格式为:

    FROM <imagesName:tag>

    当同一个Dockerfile构建几个镜像时,可以写多个FROM命令,比如说同时用Ubuntu和Debian作为基础镜像构建一个系列的镜像,以最后一个镜像的ID为输出值。

     

    MAINTAINER

    这条命令主要是指定维护者信息,方便他人寻找作者。可以写邮箱或者名字,没有规定。

    MAINTAINER Name <Email>

    注意:这个标签已经弃用, 但现在还有很多Dockerfile使用这个标签, 所以短时间内不会删除。现在推荐使用更灵活的LABEL命令。

     

    RUN

    接下来是RUN命令。 这条命令用来在Docker的编译环境中运行指定的命令。RUN会在shell 或者 exec的环境下执行命令。

    shell格式:

    RUN echo Hello World

     

    RUN命令会在当前镜像的顶层执行任何命令, 并commit成新的(中间)镜像,提交的镜像会在后面继续用到。在shell格式中,可以使用反斜杠将单个RUN命令跨到下一行。

    RUN echo "Hello" &&\

    echo "World"\

    && echo "Docker"

     

    exec格式:

    RUN ["程序名", "参数1", "参数2"]

    以这种格式运行程序, 可以免除运行/bin/sh的消耗。这种格式是用Json 格式将程序名与所需参数组成一个字符串数组,所以如果参数中有引号等特殊字符,则需要进行转义。

     

     

    exec格式,不会触发shell,所以$HOME这样的环境变量无法使用,但它可以在没有 bash的镜像中执行,而且可以避免错误的解析命令 字符串。如果需要展开变量,可以这样 使用:RUN ["sh", "-c" ,"echo $HOME" ], exec格式被解析为json 数组, 所以使用双引号而不是单引号。

     

    RUN命令的构建缓存在下一次构建期间不会失效。诸如RUN apt-get update之类的构建缓存将在下 一次构建期间 被重用。但用户可以通过使用--no-cache标志来使 RUN命令的缓存无效,例如docker build –no-cache ...

     

    最后要注意 ,避免使用RUN apt-get upgrade或RUN apt-get dist-upgrade, 这会更新大量不必要的系统包,增加了镜像大小,破坏了镜像的兼容性。如果需要更新包,简单地使用RUN apt-get update && apt-get install -y package 就足够了,注意要把这两句写到一个RUN中,否则会变成两个镜像层,增加不必要的缓存。构建结束之后记得清理缓存,像apt clean之类的自然不必说,包括/var/lib/apt/lists、 /tmp/* 这些目录等都要删除(在同一个RUN命

    令中)。

     

     

    ENV

    ENV命令用来在执行docker run命令 运行镜像时指定自动设置的环境变量。这个环境变量可以在后续任何RUN命令中使用,并在容器运行时保持,这些环境变量可以通过docker run命令的-e参数来进行修改。

    语法如下:

    ENV <KEY> <VALUE>

    #或者

    ENV <KEY>=<VALUE>

     

    使用ENV命令类似Linux下的export命令,用户可以在后续的Dockerfile中使用这个变量,例如:

    ENV TARGET_DIR /app

    WORKDIR $TARGET_DIR

    在Dockerfile中,使用env命令来定义环境变量。环境变量有两种形式:$variable和${variable},推荐使用后者,因为后者可以使用复合值,如${fool}_bar,前者就无法做到;后者支持部分bash语法,支持环境变量,可以进行递归替换。

    ${variable:-password}:如果variable不存在,则使用password。

    ${variable:+password}: 如果variable存在,则使用password,如果variable不存在,则使用空字符串。

    最后, 尽量把多个ENV命令写为一个命令, 这样可以减少镜像层的数量, 因为每一句命令都是一个镜像层, 合并之后镜像结构会变得更加简单直观。 例如:

     

     

    ENV Name="Zuo Lan"\

    demo_var=hello \

    test_var=world

     

    ENV命令在构建完成之后,会一直保留在容器内,可以使用docker inspect查看相关的值, 也可以使用docker run --env <key> = <value>更改它们的值。

     

    ARG

    ARG命令定义了一个变量,用户可以在构建时使用,效果和dockerbuild --build-arg <varname>=<value>一样,可以在构建时设定参数,这个参数只会在构建时存在。格式为:

    ARG <name>[=default value>]

     

    ARG与ENV类似, 不同的是ENV会在镜像构建结束之后依旧存在镜像中, 而ARG会在镜像构建结束之后消失。 例如, 在构建过程中, 如果希望整个构建过程是无交互的,那么可以设置如下ARG命令(仅限Debian发行版):

    ARG DEBIAN_FRONTEND=noninteractive

     

     

    COPY

    COPY命令用来将本地的文件或文件夹复制到镜像的指定路径下。格式为:

    COPY /Local/Path/File /Images/Path/File

     

    ADD

    ADD和COPY作用相似,但实现不同,ADD命令可以从一个URL地址下载内容复制到容器的文件系统中,还可以将压缩打包格式的文件解开后复制到指定的位置。格式为:

    ADD File /Images/Path/File

    ADD latest.tar.gz /var/www/

     

    在相同的复制命令下, 使用ADD 构建的镜像比COPY命令构建的镜像体积要大, 所以如果只是复制文件请使用COPY命令。

     

    因为通过STDIN传递一个Dockerfile构建(docker build -<http://example.com/Dockerfile),没有构建上下文,所以Dockerfile只能使用基于URL的ADD命令,不能使用COPY。此外,还可以通过STDIN传递压缩归档文件(docker build -<archvet.tar.gz>),归档根目录下的Dockerfile和归档的其余部分将在构建的上下文中使用。不过,如果URL文件使用身份验证保护,那么只能使用RUN wget或者RUN curl等工具。

     

    注意:

    1. 不能对构建目录或上下文之外的文件进行ADD操作,即不能使用../pat这样的路径。
    2. 如果容器内部目标位置不存在,则会自动创建。
    3. ADD命令会使得构建缓存无效(当上下文变动时)。

    EXPORT

    EXPORT命令用于标明这个镜像中的应用将会侦听某个端口,并且能将这个端口映射到主机的网络界面上。但是,为了安全,docker run命令如果没有带上相应的端口映射参数,Docker并不会将端口映射出去。格式如下:

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

     

    EXPOSE只负责容器内部监昕端口, 如果 Docker不给容器分配端口映射,则外部将无法访问容 器EXPOSE设置的端口。

     

    CMD

    CMD提供了容器默认的执行命令。Dockerfile 只允许使用一次CMD命令。使用多个CMD会抵消之前所有的命令, 只有最后一个命令生效。一般来说, 这是 整个 Dockerfile脚本的最后一个命令。

    当Dockerfile已经完成了所有环境的安装与配置, 通过CMD命令来指示docker run命令运行镜像时要执行的命令。格式如下:

     

    CMD [ " executable ", "paraml", "param2" ]

    CMD command paraml param2

     

    值得注意的是,docker run命令可以覆盖CMD命令, CMD与ENTRYPOINT的功能极为相似, 区别在于如果docker run后面出现与CMD指定的相同命令,那么CMD会被覆盖;而ENTRYPOINT会把容器名后面的所有内容都当成参数传递给其指定的命令(不会对命令覆盖)。

    另外,CMD还可以单独作为ENTRYPOINT命令的可选参数,共同组合成一个完整的启动命令 ,例如下面这样的写法表示容器启动时执行: command paraml param2。

     

    ENTRYPOINT ["command"]

    CMD ["param1","param2"]

    下面以例子说明,实验的Dockerfile是:

    FROM ubuntu

    CMD ["echo", "hello ubuntu"]

    然后我们构建镜像并运行容器,运行时会返回:

    docker build -t user/test .

    运行一下看看

    改变输出

    当使用docker run user/test echo "Hello Docker"这个方式启动容器时echo "Hello Docker"命令会覆盖原有的CMD命令。也就是说,CMD命令可以通过docker run命令覆盖, 这一点也是CMD和ENTRYPOINT指令的最大区别。

     

    CMD与RUN的区别在于,RUN是在创建成镜像时就运行的, 先于CMD和ENTRYPOINT,

    CMD会在每次启动容器的时候运行, 而RUN只在创建镜像时执行 一 次, 固化在image中。

     

    ENTRYPOINT

    上面说到了ENTRYPOINT命令, 这个命令和CMD很相似,ENTRYPOINT相当于把镜像变成一个固定的命令工具 , ENTRYPOINT一般是不可以通过docker run来改变的,而CMD不同,CMD可以通过启动命令修改内容。主要区别通过实践来体会最清晰,实验的Dockerfile为:

    FROM ubuntu

    ENTRYPOINT ["echo"]

    可以看到EVTRYPOINT命令下,容器就像一个echo程序,docker run 后面的参数就成为了echo的参数了。

     

    shell格式:因为嵌套在shell中,PID不再为l,也接收不至UNIX信号,即在docker stop <container>时收不到SIGTERM信号,需要手动写脚本使用exec或gosu命令处理。

    ENTRYPOINT <command> <param1> <param2>

     

    exec格式为:

    ENTRYPOINT ["<executable>","<param1>","<param2>"]

    此时ENTRYPOINT进程的PID为1.

    CMD和ENTRYPOINT至少得使用一个。两个一起用时,ENTRYPOINT作为可执行程序,CMD则是ENTRYPOINT 的默认参数。

    注意:可以用docker run --entrypoint 来重置默认的ENTRYPOINT。

     

    VOLUME

     

    VOLUME用来向基于镜像创建的容器添加数据卷(在容器中设置一个挂载点,可以用来让其他容器挂载或让宿主机访问,以实现数据共享或对容器数据的备份、 恢复或迁移),数据卷可以在容器之间共享和重用,数据卷的修改是立刻生效的, 数据卷的修改不会对更新镜像产生影响,数据卷会一直存在直到没有任何容器使用它为止(没有使用它也会在宿主机存在,但就不是数据卷了,和普通文件无异)。

     

    VOLUME指令在后面还会详细介绍,这里只做简单的使用说明。格式为:

    VOLUME [ " /data " , " /data2 "]

    VOLUME /data

    VOLUME可以在docker run 中使用,如果run命令中没有使用,则默认不会在宿主机挂载这个数据卷。如果在Dockerfile中没有设置数据卷,在docker run中也是可以设置的,在Dockerfile 中声明数据卷有助于开发人员迅速定位需要保存数据的目录位置。

    下面用一个例子说明:

    dockerfile文件如下

     

     

     

     

     

     

    #结果

    运行这个容器

    可以看到hello返回。说明在容器内部有test.txt文件。

    在本地创建一个文件:再运行容器,这是设置一个数据卷,注意,上面Dockerfile 并没有设置/app为数据卷,但是在docker run中使用-v参数指定了/app目录,看cat的结果会发现目标文件并不是容器内部的test.txt文件,而是宿主机上的test.txt文件:

     

    USER

    USER命令指定运行容器时的用户名或UID(默认为root),后续的RUN也会使用指定用户。格式为:

    USER user

    USER user:group

    USER uid:gid

    USER命令可以在docker run命令中通过-u选项来覆盖,这个命令的应用场景在于当服务不需要管理员权限时,可以通过该命令指定运行用户。指定的用户需要在USER命令之前创建,例如:

    RUN groupadd -r newuser && useradd -r -g newuser newuser

    要临时获取管理员权限可以使用gosu ,而不推荐用sudo 。

    WORDDIR

     

    WORKDIR命令指定RUN、 CMD与ENTRYPOINT命令的工作目录。 语法如下:

    WORKDIR /path/to/workdir

    同样的, docker run可以通过-w标志在运行时覆盖命令指定的目录。此外, 可以使用多个WORKDIR命令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如:

    WORKDIR /a

    WORKDIR b

    WORKDIR c

    则最终路径为/a/b/c。WORKDIR还支持ENV 设置的环境变量。

     

    ONBUILD

     

    ONBUILD命令在镜像被用作另一个构建的基础时,要向镜像添加在以后执行的trigger命令。trigger将在下游构建的上下文中执行,简单地说,就是事先在下游Dockerfile中的FROM命令之后立即插入。

    如果你正在构建将用作构建其他镜像的基础图像,例如应用程序构建环境或可以使用用户特定配置自定义的后台驻留程序,这将非常有用。

    格式:

    ONBUILD [INSTRUCTION]

    ONBUILD指定的命令在构建镜像时并不执行,而是在它的子镜像中执行,下面用一个简单的例子来说明。

    Dockerfile

    FROM busybox

    ONBUILD RUN echo "you won't see me until later"

    构建:

    可以看到在构建的时候会读取ONBUILD命令,但是并不会执行。接下来使用上面的镜像构建子镜像。

    Dockerfile

    FROM me/no_echo_here

    构建

     

    可以看到,在这一次构建中,执行了ONBUILD命令。目前ONBUILD命令后面不能是FROM和MAINTAINER命令,当然也不能是ONBUILD自己。形如ONBUILD FROM这样的都是错误的命令。

     

    LABEL

    LABEL命令是指添加无数据到镜像。每个标签会生成一个layer,所以尽量使用一个LABEL标签,比如:

    LABEL multi.label1="valuel" multi.label2= "value2" other="value3 "

    #或者:

    LABEL multi.labell="valuel" \

    multi.label2="value2" \

    other="value3"

    标签信息会保存到镜像中,如果有某个值已经存在,新的标签元素会覆盖它。LABEL命令的值不是给Docker构建镜像用的,而是专门给人看的,也就是说,这里面的数据是留给别人理解这个镜像的关键信息,包含作者、联系方式,版本以及其他你想表达的数据。

     

    STOPSIGNAL

    STOPSIGNAL 指令允许用户定制化运行docker stop时的信号。例如

    STOPSIGNAL SIGKILL

    这样构建的镜像其启动的容器在停止时会发送SIGKILL信号,这个命令适用于一些不能接受正常退出信号的容器。

     

    HEALTHCHECK

    这是一个健康检查命令,用来检查容器启动运行时是否正常,若正常则会返回healthy,否则返回unhealthy。例如有时候服务器被卡在无限循环中并且无法处理新连接的情况,即使服务器进程仍在运行,但实际上问题已经产生了却不会报错 ,因为从Docker看来这个容器还在运行。添加这个心跳检查命令,可以隔一段时间检查容器是否在正常运行。

     

     

     

    格式为:

    #通过在容器中运行命令来检查容器运行状况

    HEALTHCHECK [OPTIONS] CMD command

    #禁用从基本映像继承的任何运行状况检查

    HEALTHCHECK NONE

    参数有以下3个。

    1.设置在容器启动多长时间后开始检查容器状态: --interval=DURATION(默认为30s)。

    2.设置超时时间,超过这个时间不返回信息表示容器异常:--timeout=DURATION(默认为30s)。

    3.设置重试次数:--retries=N(默认为3)。

    例如:

    HEALTHCHECK --interval=5m --timeout=3s \

    CMD curl -f http://localhost/ || exit 1

    这样可以在容器运行时检查运行是否正常,不需要用第三方工具检测容器心跳信号。在Dockerfile中只能有一个HEALTHCHECK命令。如果列出多个,则只有最后一个HEALTHCHECK

     

    SHELL

    在Docker构建过程中,会默认使用/bin/sh作为 shell环境 ,Windows下构建默认使用 cmd作为 shell环境 ,但是有时候我们需要其他shell环境来执行RUN的内容,这时候我们需要用SHELL命令提醒Docker更换shell环境。

     

    例如在Windows下将powershell更换为默认shell环境:

    SHELL ["powershell","-command"]

     

     

    3.自动化构建

    在使用Docker镜像的过程中,我们经常需要构建自己的镜像,而每一次docker build都需要漫长的等待,非常耗费时间,而且面对-些大型镜像的编译工作还需要服务器有足够的硬件性能,这对普通用户来说是个不小的门槛与负担。

    因此,我们可以利用Docker Hub来自动构建镜像,解放我们的双手,也节省了 笔服务器费用。在登录Docker Hub之后 ,首先在右上角头像的菜单中依次选择 "Settings> Linked Accounts & Services",这时候可以看到Github的图标, 单击认证,然后Docker Hub就与你的Github仓库连接了。目前自动化构建免费版貌似不支持了。要升级才行。国内的几家容器云提供商还提供免费的实时 镜像构建服务。除此之外,还可以使用著名的持续构建服务Travis CI来构建镜像。

  10. 镜像仓库
    1. 国内镜像加速

    事实上除了Docker Hub上的镜像,许多第三方仓库也是有着体量不小的镜像。由于国内的网络环境访问Docker Hub的速度不理想,使用国内镜像仓库就很有必要了,否则在国内直接拉取Docker Hub的镜像会非常慢。

    国内像Daocloud、阿里云、灵雀云、时速云、网易蜂巢等公司都有开放的第三万镜像仓库,不过本节推荐的是一个由中国科学技术大学(简称中科大)搭建的镜像仓库。中科大镜像仓库是官方仓库的镜像缓存,也就是说,它本身不只是一个仓库,还是一个仓库镜像。

    从中科大拉取镜像和从官方拉取镜像基本上是一样的。要使用这个镜像仓库替换官方仓库,非常简单,只需要一步。

    linux下在/etc/docker/daemon.json编辑加入以下内容,没有就创建一个,

    {

    "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn"]

    }

    然后重启docker既可。

     

    此外如果有海外的服务器,并且流量很多,可以打造成一个镜像加速器。如下

    docker run -d -p 5000:5000 \

    -e STANDALONE=false \

    -e MTRROR_SOURCE=https://registry-1.docker.io \

    -e MIRROR_SOURCE_INDEX=https://index.docker.io \

    registry[]

     

    一个简单的docker Hub镜像加速站点就搭建好了,然后在上面的daemon.json文件中配置即可。

     

     

     

     

    1. 搭建私有仓库

    如果你是个人开发者,没有公开仓库分享的需求,那么搭建一个自己的仓库就非常简单了,只需要运行一个容器就可以实现私有仓库的搭建:

    docker run -d -p 5000:5000 --restart=always --name myregistry registry:2

    没有问题的话,上面的私有仓库已经搭建起来了,现在你可以向私有仓库推送镜像了,但是在此之前,必须先使用docker tag来给即将推送的镜像打标签,这是因为在 docker images所显示的镜像默认是从Docker Hub拉下来的,推送时如果不指定仓库地址,Docker会默认推送到 Docker Hub中。

    举个例子,比如要把test:latest推送到刚才搭建的私有仓库中,需要先使用dcoker tag改变test:latest的镜像名称:

    docker tag test:latest localhost:5000/test:latest

    改变之后就可以推送私有仓库了,

    docker push localhost:5000/test:latest

    如果需要拉取这个镜像就使用pull即可。

    需要注意的是上面的registry容器没有提供数据参数,所以推送的容器只存在容器内部,如果registry容器删除后就会消失。所以为了存储镜像,实际上在启动的时候需要补充数据卷参数:-v<宿主机本地路径>:/var/lib/registry,默认为镜像存储位置在/var/lib/registry,用户可以自己调整参数改变路径。

    在这里只是简单的说一下仓库的创建使用,后面还会进行细致完整的仓库说明。

     

     

     

     

    1. 仓库原理

     

    上面在搭建私有仓库时,你可能注意到并没有账号管理的功能,这是因为在 registry镜像中并没有账号管理的功能。要完成这些功能,我们先要了解一个完整Docker Registry的结构。

     

    Docker Registry扮演三个角色, 分别是Index、Registry和Registry Client。Index.主要负责管理Docker Private Registry的用户信息以及认证权限、 保存记录和更新用户信息(包括操作记录),以及镜像校验信息。Index主要由控制单元、鉴权模块、数据库、健康检查模块和日志系统等组成,可见Index并不是 一个具体存在的事物而是-个概念。

     

    Registry是镜像的仓库。然而,它没有一个本地数据库,也不提供用户的身份认证,由S3、云文件和本地文件系统提供数据库支持。此外,它是通过Index Auth service(鉴权模块)的Token方式进行身份认证的。

     

    Docker充当RegistryClient来负责维护推送和拉取的任务,以及客户端的授权。

     

    了解了这三个角色,我们就大概了解了DockerRegistry的工作流程,如图是客户端发出pull(捡取)请求下载镜像时 Registry的工作流程,客户端向Index 请求拉取镜像,Index确认后返回Token,客户端拿到Token向Registry请求拉取镜像,Registry再向Index确认Token是否正确,确认无误后Registry允许用户拉取镜像。

     

     

     

     

     

    同样,当客户端提出要推送镜像至Registry时,需要Index认证,认证通过后会返回一个Token,拿着Token去找Registry, Registry再去问Index, Index说是这个口令、没错,然后Registry才向用户开放权限允许推送,如图所示。

     

    稍有不同的是删除镜像的时候,registry收到Client的删除镜像的请求时,会向Index确认,确认无误后,Index删除镜像元数据,并且通知Registry删除存储的镜像,如图所示:

     

     

    即便是一些第三方公开仓库,在匿名拉取镜像时都是通过鉴权机制发放匿名口令来实现拉取的。

    更详细的诸如Index的数据库设计等内容,可以参考-些开源的仓库项目,例如Harbor等的数据库设计。

posted @ 2021-12-24 21:10  头发重要  阅读(867)  评论(0)    收藏  举报