-------------------------------------------------------------------------------------------------------------------------------------

docker1

目录

docker(注意安装完毕后,一定不要重启防火墙,会照成容器失效)

  先了解一下容器的概念。到底容器是什么?这是从维基百科上摘下来的概念。

        容器是一种基础工具;泛指任何可以用于容纳其它物品的工具,可以部分或完全封闭,被用于容纳、储存、运输物品;物体可以被放置在容器中,而容器则可以保护内容物;( 注意容器是一种技术工具。泛指任何可以用于容纳其它物品的工具都叫做容器。这种物品本身可以是部分或完全封闭的。)
        人类使用容器的历史至少有十万年(所以容器并不是一个新概念),甚至可能有数百万年的历史;

        容器的类型(容器的类型有很多种。等等等等都是容器,但是它不是我们讲的重点。我们接下来讲的内容先从lxc开始。)
              瓶 - 指口部比腹部窄小、颈长的容器。
              罐 - 指那些开口较大、一般为近圆筒形的器皿
              箱 - 通常是立方体或圆柱体。形状固定。
              篮 - 以条状物编织而成。
              桶 - 一种圆柱形的容器。
              袋 - 柔性材料制成的容器,形状会受内容物而变化。
              瓮 - 通常是指陶制,口小肚大的容器。
              碗 - 用来盛载食物的容器。
              柜 - 指一个由盒组成的家俱
              鞘 - 用于装载刀刃的容器。

  LXC(LinuX Containers)
         在虚拟化技术当中,了解过所谓的虚拟化和容器间的关系。虚拟化的常用技术形式,实现形式,目前的主流的有两种
        
        主机级虚拟化:
              第一种我们称为叫做主级虚拟化。我们要虚拟化的是整个完整的物理硬件平台。比如经常使用的VMware workstation他可以让我们拿到一个虚拟机以后,就好像是一个裸的物理机设备一样,贴在上面自由的安装操作系统,去使用操作系统。安装的操作系统,甚至给我们底层宿主机,使用不同的系统。而且主机级虚拟化,他还有两种类型的实现。所谓Type-I类型1的虚拟化和Type-II类型2的虚拟化。这两种其实都是主机级虚拟化,他不是我们要描述的重点。但是要注意的是这两种虚拟化技术的实现机制就是,所谓我们首先刚才一直在讲,你得有底层硬件平台,对吧。先不管那个Vmm底下有没有一层HostOS,但是这个虚拟机的软件给我们虚拟出来的场景或者虚拟出的这个环境,应该是一个独立的硬件平台。因此,用户要使用这个虚拟机,就需要在虚拟机之上自己去部署一个完整意义上的操作系统。这个所谓完整意义上的操作系统,我们得自己在上面装一个内核,然后内核上又有用户空间,用户空间里边跑进程。运行内核是我们的主要目的吗?不是,内核的核心作用在于资源分配和管理,对不对。所以真正在用户空间跑的应用程序才是能产生生产力的,比如像我们web,我们要提供一个web服务,内核好像从来没有给我们提供web服务过。虽然有那么极个别,确实能工作,在内核空间的服务。但是像nginx,httpd都是工作在用户空间的。所以我说真正能产生生产力的应该是用户空间的应用进程,而不是出于通用目的而设计的资源管理平台,操作系统内核。但是这个内核又不得不有。因为在于我们要想在这个硬件平台上使用软件程序。现代软件基本都是针对于内核的系统调用和库调用研发的,对不对。你不安装内核,不安装库,不安装运行环境,你没法运行应用程序。这个内核虽然看上去没什么关系,但他必不可少。更何况,我们一旦有用户空间以后,它里边要运行很多进程。那么这些进程为了能够协调,也必须要用内核来实现。我们创建一个虚拟机的目的,就一个,单一的目的。就是为了运行一个web服务器,你想象一下,或者运行一个Tomcat这样的应用程序服务器,为此我们却不得不安装内核,安装用户空间,然后去爬Tomcat代价是不是有点大。      而且更重要的是,用各位更熟悉的二型虚拟化,我们来分析。如果一个进程为了运行,他需要实现两级调度和资源分派。      第一,自己虚拟机有没有内核呀?这个内核就已经实现了一次所谓的内核虚拟化,cpu调度,以及io的调度,io的管理等等。但是真正的虚拟机本身也是被宿主机内核管理的另一层,进程或者抽象层而已,因此还需要给我们的宿主机内核或者是Hyperviso,如果是一型虚拟化的话,再需要调度和管理一次。这中间资源额外的开销,也就不言而喻了。因此我们说这种所谓的传统的主机虚拟化技术。的的确确能够让我们在一组硬件平台之上实现所谓的跨系统环境的隔离和实验和调试等各种所谓的,当然也包括资源高效应用的各种需求。但事实上他带给我们的资源开销也是不容忽视的。当然很多时候我们很有可能创建虚拟机目的仅仅是为了运行有限的,一个或几个。负有生产责任的进程而已,而为此付出的开销代价稍微有点大。那既然如此,还是这句话,减少中间层,减少中间环节,很显然就是有效提高效率的方式,比如说如果我们现在要想提升效率的话,我能不能把这个虚拟机中间这层内核给他抽掉。把虚拟这层内核给他抽掉。只保留进程。但这么一来,你会发现有一个问题啊。我们用虚拟机的目的是为了什么?是为了环境隔离。别忘了。如果要运行两个nginx他俩都得监听80端口,请问。你一个主机上,有两个80的套接字吗?当然如果针对不同ip地址是可以实现对吧,假设ip地址只有一个。对外通信你运行两个nginx,甚至运行五个nginx。他们有办法使用同一套接字,都使用默认80吗?显然做不到。      我们有了虚拟机,可能就完全可以实现,这是没什么问题的。这是一种需求,更甭提其他需求。所以我们说创建虚拟之目的,假如只是为了运行进程,干嘛不在宿主机上运行,而非要放到虚拟机当中,我们目标是为了资源隔离使用,就像那个nginx这里边翻江倒海,他也不会影响其他进程。最多他损坏那个虚拟机,一个环境而已。所以这儿带来的隔离才是我们要追求的目标。      因此,我们虽然脱掉了一层内核,但又不能让他重新回到大家在同一个锅里吃饭那个环境,而需要让他们每一个相应的进程或者每一组进程彼此之间是互相不可达的。就好像运行了两个不同的虚拟机,甚至是物理机中一样,互不干扰。只不过碰巧他们共享同一组底层硬件资源而已。那因此我们这个所要追求的环境。就变成这种样子。也就意味着,第一我们有一组硬件平台。在这个硬件平台之上我们提供一个所谓的虚拟的隔离环境管理器,而后我们创建一个又一个的隔离环境,然后我们让要运行隔离出来运行的进程就跑在这个隔离环境内。我们应该知道进程是跑在用户空间的,因此内核提供的是内核空间。进程应该运行在用户空间,而我现在需要进程运行的一个隔离环境中,其实我们隔离的是什么?是用户空间。按道理来讲,我们用户空间只有一组各位明白的,对不对。我们在一个内核之上嘛,但是我现在可以期望实现的是将这个用户空间给它隔离成多组。彼此之间互相不干扰,一个用户空间内只运行一个或部分进程。但是需要明白,无论怎么隔离,这其中一定有一个就像我们此前说到的任何KVM一样,一定有一个甚至一般都是第一个用户空间应该是有特权的。我们通过他来管理其他的用户空间,对吧。ok      好,这里我们就有了用户空间了。随后我们启动进程时,让进程启动并运行在用户空间当中。那么在众多用户空间能共享底层同一个内核,但事实上是被同一个内核管理的。但是在自己运行时候能够看到的边界却是自己所属的用户空间的边界。那彼此之间也隔离了。但是各位应该能够明白,这种隔离显然没有主机级虚拟化所隔离的那么彻底,而且更重要的是,在这里有没发现这个用户空间拿来干什么。放进程的。给进程提供运行环境,并且还能够保护其内部的进程不受其他进程的干扰,所以他叫做什么。容器,ok      这就是我们在linux当中或者在计算机领域当中经常提到的容器技术。容器技术其实也不算什么新概念,最早是出现在FreeBSD上的当年叫Jail。其实目的就是为了能够运行进程,不受其他的干扰。他叫jail这是监禁监狱之意。重要的是我们提供一个实验环境让他在里边运行好像是沙箱一样。就算这个进程自己出现大的有了故障,他有异常行为,也不至于影响自己所属这个容器的外围的,就是容器边界之外的其他的进程。那这种隔离带给我们是一种安全运行这样一个初衷。当然一般来讲,进程自身突然间像脱缰的野马一样去随意发疯的可能性不存在,但一个服务进程被远程的客户端所劫持,进而搞起破坏的可能性是存在的。所以他这样破坏的时候,他所能够到达的边界仅仅是一个容器,以及仅是这么一个Jail的边界。所以最初出现Jail的目的是为了应用的安全运行。      后来就有人把Jail这种技术给他复刻到或者复制到这个所谓的linux平台上。到这个平台上之后,这个产品叫vserver。这个vserver在一定程度上也能实现出Jail的效果来。其实vserver背后所用到的当时主流能实行的功能,各位恐怕在就在此之前,很多情况下都用过叫chroot。无非就是实现chroot,我们发现真正的根应该是我们整个文件系统的根对不对。那么实现切分以后,假如我们在一个子目录下也创建一个FHS定义的发行版应该具有的根下的子目录结构以后使用chroot就能够把那个子目录当根用来使用,所以说在里面运行进程,也以为他就是根了,对不对。从这个角度来讲,他不就是个单独的Jail吗?      不过呢这并不能真正实现他与我们宿主机就是真正的特权用户空间或其他用户空间彻底隔离,因为chroot他所隔离的仅仅是看上去的这种空间,想象一下他们底层可用同一个内核的,你给他隔离出来以后,这个用户,这个进程到底运行为特权模式还是非特权模式呢?他如果需要访问某些特权资源的时候,我们该怎么去指派?所以表面上看上去只是chroot,但背后其实是一堆技术的支撑,真正要实现他们隔离至少比如说在一个单独的用户空间当中,刚讲过一个所谓的用户空间,它的主要目标是实现隔离环境的,而后任何进程运行在这个用户空间当中,他就以为自己是唯一运行在当前内核之上,用户空间中的进程,而且自己所能看到的其他进程应该也都是这个系统下的所有进程了,对不对。但是各位应该明白,一个用户空间,他应该看到有这些组件,第一主机名和域名要有吧。第二根文件系统是不是要有啊。之前说过,chroot切完根以后,他的确能看到什么bin,sbin什么之类的,这确实是叫根文件系统没问题,对不对。好,那么每一个用户空间都要有自己独立的根文件系统。第3个每一个用户空间也都应该有自己的IPC叫进程间通信的专用通道。各位试想一下,如果两个用户空间的进程之间通过IPC可以互相通信,那这隔离还有什么意义呢?IPC之前讲过,我们可以使用共享内存啊,什么的,应该知道这个概念。说白了就是共享内存嘛,我一个进程把数据丢到一个内存空间中,另外进程可以从里面获取。比如我们之前讲到的那种基于unix Socket的文件通信,这种东西压根就没有经过别的什么东西。他其实是通过内存来实现的。请问这两个用户空间中的进程,其实在同一个内存上运行,对吧?这是没问题的,因为物理硬件只有一组。那他们俩之间能不能正常使用IPC通信呢?按道理来讲,如果能那么隔离意义就不存在了。所以我们必须要确保他们的IPC是独立的,但你的IPC在同一个用户空间内的进程之间是可以的。但跨用户空间就不行。而事实上,在底层内核本身管理时,但是期望各进程之间任意任何进程之间可直接使用IPC通信的。但现在不得不把他隔离开,给他分开,只要有这个用户空间,有独立的用户空间,要独立的容器,我们就能把这个资源给他分割开来。      这3个分别是,第一主机名和域名,通常简称叫UTS,第二文件系统我们通常把它称叫挂载树Mount,第三IPC我们也要隔离。除此之外还有没有别的呢。比如在每一个用户空间中,每一个进程它应该会从属于某个进程。因为进程都应该是有他的父进程所创建,对不对。如果一个进程没有父进程,他应该是这个用户空间的init。我们说过一个系统运行其实无非就是两棵树,所谓进程树和文件系统树。很显然对当前用户空间来讲既然认为自己所属的这个用户空间是当前系统上唯一的,你要给他弄一个假象,要给他弄一个虚拟或者幻象,那么他就应该知道,要么自己是init要么自己从属于某个init。这样才对,否则你这个用户空间中的进程将无法被管理,我们说过所有的在linux的进程管理界,从来都是白发人送黑发人,所谓子进程有他的父进程所创建,而子进程终止,并回收也是有他的父进程来实现的,而如果init结束了。在init结束之前,他要把他的子子孙孙都要送走,然后他才能放心离开,关机的,这是正常逻辑的。所以我们说在每一个用户空间当中,既然要自己独立管理,你想象一下在每一个用户空间是不是都应该有自己的init才对,ok,但是init有几个。事实上,对我们系统init只能有一个。那我们于是就得给每一个用户空间做一个假的init出来,要么就叫init,就是init。要么我们在这个用户空间只能运行一个进程。只要这个进程一终止,这个用户空间也就消失了,这样也成。否则刚才我们说了,只要有多个进程,那么他应该同属于某一个上帝的使者来管理对不对。我们通常把它称作叫init这应该容易理解。      由此从这个角度来讲,我们的进程树使得每一个进程所看到,要么自己ID号为1,要么自己从属某个id号是1的进程,所以这就意味着他要有一个独立的进程数他的PID要是互相隔离的。各位应该明白,1号进程一定是指init的。我们又讲了,在一个内核之上,真正id是1的只能有1个,但现在我们又不得不为每一个用户空间的进程伪装一个,这样子我们就不得不把他们隔离开来。就是把这个init这种机制给它创造个假的一个幻象,在内核中能识别他们,但却需要给他伪装出这么一个出来这是pid。      那除了pid之外,还有没有呢?当然还有。一样的逻辑,我们运行一个进程。他都应该以某个用户的身份来运行。请问,第一个用户空间的新用户和第二个用户空间的用户有没有可能id号是一样的,但名字不一样啊。我觉得应该是存在的,因为我们会认为说每一个用户空间,每一个主机上它的id号为0的叫root,id号为1到999的以centos7为例。他应该是系统用户,1000以上的应该是我们的普通的登录用户。那很显然每一个用户空间所看到的,他应不应该看到root呢?每一个用户空间要不要有一个root?但是真正的root可以每一个用户空间在一个内核上都有吗?在一个内核上只能有一个root,这是显然的。而每一个用户空间也有都有root那这事就麻烦了。我可以随意穿到别人的用户空间了。我可以随意管理别人的用户空间了。这隔离还有什么意义?所以这就意味着什么?我们必须给每一个用户空间给他伪装,出一个root。这个root可能实现这样的效果。在真正的系统上,它只是一个普通用户。但是一旦对这个用户空间来讲,我们可以把它伪装为ID为0,在这个用户空间内为所欲为,我把这个用空间的所有权限都给他。并给他伪装成这个用户空间id为0。其他的其实真正回到宿主机的系统上内核上他依然是一个普通用户。这个我们容易理解,我们把一个子目录他的属主属组都改成某一个用户,然后chroot过去,那这个用户对这个目录本来他就有所有权限。但是你需要让进程看起来是root才行,所以从这个角度来讲,我们(USER)用户,组等相关信息也需要做隔离,做虚拟,做抽象一般来讲我们叫做隔离。最后一个也是最麻烦的一个,请问每一个用户空间,都以为自己是这个系统上的唯一的用户空间,那请问他们可不可以都应该有自己的IP地址?能不能监听一个80端口?假如说运行了nginx的话,不然的话隔离还有什么意义呢?所以从这个角度来讲,每一个用户空间就像一个虚拟机一样,他应该能看到自己的专用的网卡网络接口,有自己专有的 TCP\IP协议栈。有自己专用的,套接字平面,就是所谓的整个端口空间是0到65535这里面所有都可以用,另外一个用户空间进程也是,他看到的那个端口空间是0到65535的,如果没有被其他同一用户空间的其他进程占用,那么他就一定可以用,要实现这种效果。而且更重要的是两个容器之间,可能还需要互相通信,每一个用户空间都以为自己是这个网络中独立运行的计算机。那么很显然,两个容器就是两个独立的计算机。他们俩之间应该可以通过网络正常通信。还要达到这个目的才行。而在内核级tcp\ip协议栈只有一个。但我现在需要给每一个用户空间创建一个Nat。因此从这个角度来讲,我们的网络。你也要想办法在内核级别这些资源都是独立只有一组。因为我们本身内核在最初设计时,只是为了支撑单个用户空间的运行。那后来有这种所谓要运行Jail或者vserver的需要,于是他就在内核级把这些资源。开始能够在内核级直接进行虚拟化。我们这里叫虚拟化不合适。事实上这每一种资源在内核级只要可以切分为多个互相隔离的环境的话,我们就把它称为叫做名称空间。在内核当中UTS是可以以名称空间为单位并进行隔离的,也就意味着我们可以把在同一个内核之上创建出多个名称空间来,这是没问题的,然后在这些名称空间 之上,我可以把UTS资源,让每一个名称空间和另外一个名称空间,彼此之间互相隔离,所以每一个名称空间都可以有自己独有的名称,也就意味着主机名和域名是内核级的,所以一个主机上主机名只能有一个。但是现在我们有多个用户空间,就必须要给每一个用户空间当做是这个系统上唯一,而且跟其他互不干扰的用户空间一样。每一个用户空间都应该有自己的主机名。但是他们都去到内核上,内核就混乱了。所以内核在内核级隔离开来。然后他们可以分别各自使用,而不影响主的,特权的,真正的一个用户空间。      事实上Mount也是一个名称空间。我们可以把所谓挂载文件系统切分为多个,每一个好像都有一个,大家互相不干扰。一样的逻辑,内核级实现。IPC内核级管理的资源可以在内核就直接切分为多个。每一个里面的进程互相可以IPC通信,但是跨边界不可以,为了支撑所谓的容器机制的实现。linux内核到今天为止,在内核级一共对这6种需要被隔离的资源在内核级已经通过一个所谓叫做名称空间的机制原生支持就叫namespaces他并且把这种功能直接通过系统调用向外进行输出。系统调用,比如我们创建进程一般使用clonc()这个系统调用。如果把这个进程创建完以后,给它放到某个名称空间里面去。我们使用setns()就是设置名称空间,这样系统调用。所以要想使用容器要靠内核级的内核资源用于支撑用户空间的内核级的内核资源的名称空间隔离机制来实现。所以到今天为止,整个linux领域的所谓的容器化技术,就是靠namespaces内核级的6个spaces加上chroot来实现的。这个事儿其实听起来容易,可没那么简单。因为你来看这6种名称空间。有一个在内核3.8的时候才被加进来。所以要想很好的使用容器技术,那么内核版本应该是在3.8以后才行。centos6天然就被排除在外因为他用的是2.6的那个内核,当然了,没有哟着,这个并不是不能用,他只不过在某些地方是有缺失的而已,余下的几个基本上在2.6,2.4内核级别就一定被添加了,所以在centos6上我们可以很轻松的可以使用chroot的功能,是没有问题的。


              Type-I:而类型一的虚拟化技术,他们是直接在硬件平台上装一个虚拟机管理器叫Hypervisor,也就意味着在硬件之上,是不用再装宿主机操作系统,而是直接上Hypervisor。然后在Hypervisor之上去安装使用虚拟机,也就意味着没有任何主机是直接跑在硬件之上的,所有的操作系统通通都是跑在虚拟机内部。这就叫一型虚拟化技术。
              Type-II:对于类型2的虚拟化应该是最熟悉的,像我们VMware workstation,VirtualBox,kvm但是kvm是奇葩。所谓类型二的虚拟化,简单来讲无非就是。首先我们有宿主机,有物理机设备,在宿主机上物理机设备上,我们在先装一个主机操作系统。我们把他们叫hostOS,也称作叫宿主机操作系统。在宿主机之上装一个Vmm叫Virtual machine manager我们称作叫虚拟机管理器,像VMware workstation软件,在这个软件之上,我们在创建使用虚拟机。这种我们叫类型二的虚拟化技术。

Linux NameSpaces(这是用户空间,实现时,我们要靠Linux的Namespaces来实现,6种名称空间)

namespace 系统调用参数 隔商内容 内核版本
UTS CLONE_NEWUTS 主机名和域名 2.6.19
IPC CLONE_NEWIPC 信号量、消息队列和共享内存 2.6.19 (IPC 进程间通信)
PID CLONE_NEWPID 进程编号 2.6.24 (PID进程编号要确保每一个用户空间的进程编号都是隔离的,各名称空间中进程编号可以一样,也可以不一样互相不影响)
Network CLONE_NEWNET 网络设备、网络栈、端口等 2.6.29 (Network网络也需要隔离,都有自己独立的)
Mount CLONE_NEWNS 挂载点(文件系统) 2.4.19
User CLONE_NEWUSER 用户和用户组 3.8
        容器级虚拟化:
              有了主机及虚拟化技术之后,是不是就万事大吉了呢?肯定不是。试想一种场景我们现在不再使用主机级虚拟化技术了,我们开始抽调主机级虚拟化技术当中每一个虚拟的内核,让所有用户空间不再分属于一个独立的内核,而从属于同一内核,所以这种技术我们称作叫容器级虚拟化技术。但是这种虚拟化技术有一个问题在于,其实主机级虚拟化也存在,只不过我们在创建时就指派好了,我们在创建虚拟机时可以指定虚拟机CPU核心,多大的内存,这东西我们可以在创建虚拟机时就设计好。假如你物理机是32核,我创建虚拟机指定能使用2核。也就意味着他是无法使用余下30核的。这是可以在天然创建虚拟机时就做了资源限制的。各位需要想一下,这儿的名称空间,大家是工作在同一组内核之上的,如果这里边进程像脱缰了一样,他在里边因为内存泄漏,开始不断的吞噬内存,最终把整个系统内存全部吃掉,导致系统崩掉,会影响其他用户空间, CPU也是一样的逻辑。 CPU还好一些,因为CPU属于可压缩型资源,无非就是你得不到CPU,一直挂着就行了,但是内存就不行了。如果你一个进程把内存全吃了,其他进程运行时需要分配内存,一申请没有,结果就是OOM('Out Of Memory',内存用完了)。,会因为内存耗尽而被kill,因为内存属于非可压缩性资源。要就必须有,没有就挂了。不像CPU,要,没有,没有就等一会儿。就是没有问题的,他是可压缩性的,所以内核级还必须实现一种功能。来限制每一个用户空间中的进程的所有可用资源总量,比如我们可以按CPU来分配,一共有3个用户空间,且不说有多少核,我们定义说cpu的比例,在这3个用户空间中是1比2比1,这种方法就很有弹性,听清楚啊,叫1比2比1,就是把CPU当4份,一共只有4份。那么第一个用户空间只能用25%,第2个是50%,第3个是25%。但是这种好处在于,如果第二个和第三个不用,第一个他现在需要大量的计算,它可以干什么?吃掉整个CPU都没有问题。但是一旦到第二个和第三个用,大家就只能干什么,而且用的也很多。大家只能按比例分配。我们确保第1个25%,第2个50%,第3个25%,就按照这个比例来分配。所以我如果再加一个,如果又加了一个用户空间也做了分配,1比2比2比1。第1个20%,第2个40%,第3个40%,第4个20%。他是按这种方式来分配,因为这是可压缩性资源刚才一直在讲这种概念。我们可以用非常弹性的方式,来进行分配。这是一种分配方式,还有一种我们也可以限制一个用户空间中的进程,无论你有多少个进程,最多只能使用几核。假如说我物理机有32核,无论你有多少进程,最多只给你2核用,多一点就不能用,这也可以。所以我们可以在整体资源上做比例性分配,也可以在单一用户空间上实现做核心绑定,你只能使用几个核。调度的时候,任何核都可以调动,但是同时使用的时候不能超过指定数量,这也是一种限制,我们要达到这种目的才行,内存也是一样的逻辑。假如说一共是64g,你最多只能使用4g内存,那么你的这个空间内所有进程加起来不能超过4个g,如果要超过4G怎么办?这个用户空间里面哪个进程最耗内存。OOM,kill掉一个,因为一共就这么多,你不能吃别人的。因为内存属于不可压缩的资源,你多一点都不能超过,一超过回头收不回来的,因为他不可压缩,他绝不允许越界的。但CPU没有问题,而这种功能必须要在内核上针对每一种名称空间来实现,当然这个功能在内核级,靠的是一种机制叫做,全称叫做Control Groups我们称作叫控制组,也称之为CGroups。对cgroup来讲,它无非就是把系统级的资源分成多个组,然后把每一个组内的资源量给他指派或者分配到特定的用户空间的进程,实现方式包括如下

              Control Groups(cgroups)
                    cgroups简单来讲,你可以这么来理解。假如大家都处于同一个锅里吃饭的孩子但是呢。有些孩子根红苗正,他们属于一组。另外有些孩子属于黑五类,他们他们属于一组。那么贫下中农他们属于一组,臭老九们属于一组。对这些组来讲,我们可以以不同的配额方式,来分配粮票,比如有100份粮票,根红苗正组给他90份,黑五类组给他1份,臭老九组给他1份,贫下中农组给他8份。我们可以实现对一个系统之上主要运行的进程,给他分门别类的分分类。每一类就叫一个组叫控制组。那而后把有限的这些资源对每一组来讲,我们有限的去匹配,比如cpu,一个分90,一个分1份,一个分1份,一个分8份,毕竟黑五类,让他活着就不错了。等等类似这一种方式。普天之下,莫非王土,大家从属于什么,同一个组。叫什么组?这就叫所谓叫Control Groups划分成组以后,在不同的组内,来实现资源分配,而且组还可以被再次划分,比如像黑五类可以划分成另外两组,比如叫成年黑五类和黑崽子们,而根红苗正组可以划分为红二,红三,红四,红五等等都可以。而后一旦我们把一个资源分配给某一组以后,这个组内的子组,可以自动使用这个资源,除非我们给他单独做分配,否则,他将自动拥有分配过来的资源的使用权限。      假如把一个用户空间就当做是一个组,然后在这个组上指派不同的资源,那么是不是就可以限制,他们的资源使用能力。这是一种方式。然后我们把资源扔给了一个名称空间以后,这个名称空间内部的进程就自动拥有了使用者得到分配的所有资源的能力。       好,有了Control Groups,有了Namespaces终于我们就可以愉快的使用容器了。      但事实上容器的隔离能力大家想象一下,比起来主机及虚拟化来说,是不是要差很多,从想象各位应该明白,他一定会差很多的原因在于大家毕竟是什么?属于同一个内核,他只不过在内核级强行设置的边界,而不像主机虚拟化,大家本来就不属于同一个内核,内核本身就是天然隔离一个平台了。所以他的隔离性远不如主机级虚拟化那么好,到今天为止,这个问题依然存在。因此,为了加强这种安全性,为了避免一个用户空间的进程,绕过漏洞,绕过边界把手伸到别的用户空间中里边去。所以人们后来又通过所谓的叫selinux等等这种各种安全加强机制试图去,加固,我们的这个所谓的用户空间的边界或者容器的边界。所以我们为了能够支撑容器技术做的更加完善,我们还应该可能要需要,一般需要启用selinux等机制,不过对于我们来讲selinux不是一个好玩的东西,对吧。但大多数情况下是不会启用他的,那你就意味着我们用的容器技术应该就是,刚好说这三种,chroot,Namespaces,CGroups,这三个,最核心的东西,而这些在内核级本身就已经实现了。那他跟docker有什么关系?其实刚说过,容器技术本来是靠当年的Jail启发,有了vserver,再往后为了能够把这种容器技术做的更加易用,像刚才我们要创建容器怎么办?刚才说过你要自己写代码去调系统,系统调用,克隆,setns()什么之类的,来实现创建内核的,你有多少用户有这个能力啊。是不是,所以我们最好把需要使用容器技术的工具,这种功能给它做好,做成一组工具。做成一组工具它能够极大的简化用户的使用麻烦程度,对不对。于是就有了这样一个,所谓的解决方案叫LXC,叫LinuX Contalner,它就叫Linux容器。因为它确实是最早,除了vserver以外,是最早一批真正把完整的容器技术,用一组简易使用的工具和模板,来极大的简化了容器技术使用的一个方案。所以他把自己称为叫做LinuX Contalner我们通常把它简称为叫LXC。那么他有一组工具刚刚说过,比如像lxc-create我们可以使用这个命令,去快速创建1个容器我通常把它称叫一个用户空间。然后创建完用户空间以后,这个用户空间里边得应该有最基本的什么?bin,sbin这样的目录结构对不对。是不是还应该装上一个基本的应用程序呢?ls呀什么之类的,那这种东西怎么创建呢?你可以从宿主级copy对吧。但是如果说我们要创建的目标,这个用户空间,底层是内核,根用户空间是centos,我想创建个新的用户空间里边儿跑的是Ubuntu的用户空间行不行,那如果是Ubuntu的话,你有没有想过,我布置宿主机的能行吗?因此接下来我刚刚说过,他们需要基于模板。这个模板其实就是一组脚本,这个脚本就是创建完一个名称空间以后,这个脚本自动执行以后,他会首先给他一个脚本执行环境。简单的,这个脚本执行的时候,会自动的去实现,安装过程,这个安装就是指向了你所打算创建的那一类的名称空间的系统发行版所属的仓库,从仓库中拉下来下来安装。生成这个新的名称空间。于是这个名称空间就像虚拟机一样可以被使用。这边一个复杂过程,我们在利用什么?模板,访问到这个模板中所定义的,能够访问到那个发行版的仓库,利用仓库中的程序包下载至本地来完成安装过程。这是我们的系统,你应该有一个根的是我们所谓的特权的名称空间或者用户空间。特权用户空间在这个用户空间当中呢,找一个子目录,在这个子目录上执行一个脚本,这个脚本能够噼里啪啦的把一个发行版的程序包把它当根都装进来。装进来以后chroot进去,那因此就好像我们有了一个用户空间,就好像一个新的虚拟机一样。那所有的名称空间都这么来实现。而LXC就靠这一组工具帮我们快速的实现了创建空间,利用模板完成内部所需要各种文件的安装。同时还有一些工具能够自动帮我们完成chroot切换过去。于是我们就可以愉快的使用多个并行的用户空间,而每一个用户空间就像我们此前所使用的虚拟机,几乎没有两样。它是一个独立的系统里边应该有自己所需的各种文件,用户账号,主机名,ip地址什么之类的,通通都有。就跟你在第1个主机上安装一个vmwar,或者kvm甚至很多虚拟机是一样的。以后你也可以拿到来安装其他程序包,修改配置文件,启动服务,监听端口等等都可以。ixc在容器技术的推广上,绝对算得上是功不可没,对吧。但是你依然会有很高的门槛。第一你得理解lxc得学好lxc的各种工具。第二,我们还要去理解,必要的时候还要去定制模板,更重要的是。每一个用户空间都是安装生成的,对吧,我们在里边后来运行过程中生成了很多文件。比如我在里边这个运行程序,编辑配置文件,而后比如在运行redis或者mariadb里边很肯定会生成数据的。那么这些数据将来在这个宿主机出现故障以后,我想迁移到其他的宿主机上去,该如何解决?或者即便是不故障的,我想把它迁移到其他宿主机上去,其实也不是一件容易的事儿,对不对,如果我现在希望大量的去创建这样的容器。你是我需要在一个主机上进行5个,现在一共有20个主机,需要创建100个这种容器,想象是一件容易的事吗?批量创建也不容易。所以从这个角度来讲lxc它虽然极大的简化了容器技术的使用。但事实上比起了我们过去使用虚拟机来讲。他的这个复杂程度其实在很大程度上来讲是没有多大降低的,更何况他的隔离性。也没有虚拟机那么好。当然了,好处在于,他能够让每一个用户空间中的进程直接使用宿主机的性能,中间没有额外开销了基本上。这么好处就是性能方面或者资源方面的节约,这是没问题的。但除此之外,好像在分发上,大规模使用上看他依然没有找到很好的突破口,于是,后来就出现了docker,所以从这个角度来讲docker是什么东西呢?他是lxc的增强版,自己其实也不是什么容器,而只是容器技术的什么?利用工具,前端工具而已。容器是linux内核中的技术。他只是把这种技术的使用,用它的简化得以普及了而已。那docker怎么简化的呢?来看docker是什么?

                    cgroups
                          blkio:块设备IO
                          cpu: CPU 
                          cpuacct: CPU资源使用报告
                          cpusct: 多处理器平台上的CPU集合      ( CPU核心的分派,一般来说有两种分配方式,一种是按比例,一种是按核分配。)
                          devices:设备访问      (比如我们现在只有一个光驱这个东西我给用户空间一使用,用户空间二,用户空间三就不可见了。类似这一种叫资源分配。)
                          frcczer:挂起或恢复任务
                          memory: 内存用量及报告
                          perf_event:对cgroup中的任务进行统一性能测试
                          net_cls: cgroup中的任务创建的数据报文的类别标识符

              Moby, CNCF
                    nmp
                    machine + swarm + compose
                    mesos+marathon
                    kubernetes -> k8s
                    libcontainerf -> runC

                          (docker是什么和docker的简化)刚说了lxc,比如他大规模创建容器很难,我想在另外一个主机上复刻一个,一模一样的容器也很难,对不对。那docker就开始在这方面着手去找解决方案了。所以docker早期的版本,其核心就是一个lxc,他是lxc的二次封装发行版。功能上怎么实现呢?利用lxc做容器管理引擎,但是在创建容器时,就是创建用户空间时,它不再是用模板去什么,现场安装生成的,就像我们安装程序包一样安装生成。不是这样生成的,而是说他事先通过一种所谓叫镜像技术,各位在kvm中应该用过的,我们可以把一个操作系统打包成一个镜像,然后把这个镜像文件复制过去,创建成一个虚拟机,基于他启动就可以,就类似于这种方式。我们可以尝试着把一个操作系统,用户空间所需要用到的所有组件,事先准备编排好,编排好以后,给他整体打包成一个文件。这个文件我们把它称作叫镜像文件,叫image。这个镜像文件,是放在一个集中统一的仓库中的,互联网上有那么一个位置,大家都能访问,我把大家都会用到,比如一个最纯净的最小化的centos,最小化的Ubuntu,就举例子这样子。分别打包成镜像,放在这个这个仓库上或者放在这个仓库内。另外有很多同学也可能会经常在这个centos上安装一个nginx,那好,找一个最小化的centos在里边装好nginx再打包成一个镜像也放在这个仓库中。以后当你想启动创建容器,这怎么办呢?docker去运行容器,创建容器,启动容器,销毁容器,靠的还是lxc这组工具,但在此之上,docker实现他另外一个功能,当你使用lxc去create创建一个容器的时候它。不会激活模板去让你安装,而是干什么?自己连到服务器上,镜像服务器上,下载一个匹配你的创建容器需要的镜像。把镜像拖到本地,并基于镜像来启动容器。所以docker极大的简化了。容器的使用难度,以后我们想使用docker启动一个服务器怎么办?docker run结束。他自动连到互联网上docker的镜像仓库上下载下来,所有东西都准备的好好的。你想要的能够用到的大多数镜像在docker的仓库里边都有。 更有意思的是,为了使得整个容器使用更加易于管理。docker还想了另外一种方式。刚才我们讲过,在一个用户空间当中。我们可以尝试着。运行一组进程或者只运行一个进程。因为我的目的就是为了运行一个能有生产功能的软件程序的,对不对。那干脆docker采用一种更精巧机制,在一个容器内只运行一个进程。比如果我们要在主机上安装一个nginx还要安装tomcat,那么nginx运行在nginx的容器中,tomcat则运行在tomcat的容器中,二者用容器间的通信逻辑来进行通信。所以以后一个容器只运行一个进程,这是docker的目的。而lxc是把一个容器,当一个用户空间使用,它可以当虚拟机一样用,里边可以运行多个进程的。这样就使得我们将来到容器内去管理的时候就极为不便。而docker利用这种限制性的方式在一个容器中只运行一个进程的方式使得什么好处?第一,我们以前运行进程应该是这样子,这是我们主机。在这个主机上,我们有一个内核,在内核上我们运行了很多进程,这些进程大家是处于同一个锅里的,对不对。假如我们没有使用容器技术的时候,在一个内核之上,我们所运行的各种各样的进程大家其实属于同一个用户空间共享同一组,什么主机名啊,用户,IPC,mount,pid,都在同一个可视的空间范围内,彼此之间不隔离。那从而也会导致说如果一个黑客,劫持了一个进程,那他借由这个进程做跳板,有可能会威胁到其他进程,对吧。而docker所实现的功能是什么呢? 这个硬件还是那个硬件,内核还是那个内核。但随后对每一个进程是怎么运行的?没错,给他圈起来了。每一个进程只运在自己单独的空间之中。嗯,比如这是nginx,你有nginx自己的容器。name再启动一个比如这里是我们tomcat。tomcat有tomcat的容器,大家彼此之间是不可见的,而且这个容器只为运行这一个进程。那因此这个容器内部的所有环境,就是为了运行这一个进程而准备的,这个进程需要用到bin目录就给他整个bin目录,需要用到etc目录就给他整个etc目录,用不着user目录就没有user目录,尽最小化给他定义的,但应该明白,其实我们此前之所以把它们运在一个空间中有一好处。共享的文件只放一份是不就行了。那现在有什么害处?是不是要有多份呀,但这也是好处啊,你别忘了。因为毕竟你要删了哪个,你的其实不影响别人的。但是坏处在于他们占了更多的空间了。另外还有一个巨大的坏处。如果我们在系统之上运行一个进程出现故障了,我们通常会使用一组跟踪工具来看这个进程到底运行怎么了。比如什么ps啊比较入门级的,low的工具,ps呀,top呀什么之类的等等。你有没有想过?这每一个进程都运行他自己的空间当中。那么他的调试工具是不是应该是这个空间自带的才行啊。那这些工具也都待干什么?准备多份。所以你想瘦身不是那么容易的事,以前在同一个名称空间当中,这些调试工具我只准备一份能针对多个进程调试的。但以后呢,每一个工具都在自己准备什么?自带调试工具,因为大家是互相隔离的。进程你运行在哪个名称空间中,就只能对这一个名称空间中的进程做调试。我们目的就是为了让一个名称空间只运行一个进程的。你要在里面在运行个调试进程那就违反这个法则了。你要不运行,你怎么调试呢?所以,运行调试进程是必然的,一般来讲,但是他不是主要目的,你用到时在启动。平时是不用启动,启动只是那一个容器内的唯一,这个我们称为叫容器的Main process叫主进程。或者称作核心进程都可以。当然这个进程的子进程也运行在这儿,比如像nginx,我们知道nginx应该有他的mster和work,对吧,这些其实都运行在同一个名称空间中。所以说同一个进程可不是单一,而是指这个进程下的所有子进程。那他的子进程本来就受管理,对吧。那如果这个主进程终止了。就相当于,这个名称空间还有存下来的意义没有,没有。这个容器就终止了。所以他带给我们另外一个问题在于。本来我们去调试一个进程是非常简单的。而现在第一,你要调试这个进程可能没有工具,这是第一个问题。第二,要想调试,你还要干什么?,得先进入他的,得先突破这个边界才行。以前大家都没有边界,有什么好突破的,直接调整就行了,所以容器这种技术给运维带来了极大的不便利,但是他给我们开发人员带来了极大的便利之处,分发容易了,能做出来一次变形,到处运行。真正做到了真正意义上的到处运行,我们应该明白现在的环境通常都是异构,centos567并行,还有Ubuntu,还有openSUSE,还有windows,各种unix。你要开发一个软件,你想象一下,适用多种环境怎么搞。第一,我们要开发适用于每一种平台的软件。第二,每一种平台上配置文件路径,放置路径是不是也都不一样。所以你看我们程序员就为了每一种环境可能需要单独各自组织一个团队来研发,以后呢?一个团队就够了,我只需要组织一组环境,只需要打包成一个镜像就ok,无论是centos,windows,还是nuix你只要有docker执行docker run就完了,他跟你底层内核是没有关系,跟你这个操作系统原有的用户空间没有关系,但是自己独立的用户空间。他有自己独立的文件放置路径。所以以后使得我们的程序员突破分发上的极大的难题。更重要的是。部署也很难呀。其实早些时候。部署想象一下,我们要装一个tomcat到windows上你要先安装,安装完以后,找到配置文件改一改什么的,改完之后再启动服务。openSUSE是一种玩法,Ubuntu是另外一种玩法,centos又是另外一种新玩法,以后不用了,镜像做好配置设置好,docker run结束。所以说他极大的方便了程序员,以后我们程序团队,使用一组,ok了。那部署以前我们靠谁来实现?像什么发布,变更,故障处理是谁的核心职能?发布以后不用了,我们有镜像docker run就OK了。比如我现在发一个新版本,网站新版本,以前的话应该是找到特定路径下什么把它发出去,还要灰度呢,还要改链接呢,对不对。现在你只需要做一个镜像docker run起来,结束。当然,我们前面还需要调入器,对吧,这是没问题的,你run起来以后,可能还需要把它接入到我们的调度器上去。那如果我们有一个容器编排工具,这个过程统统都省去了,直接run结束。run的过程可能都不需要你手动执行。 你想象一下为什么docker这么有冲击力?为什么受到这么多人的关注,其实就是这个原因。以前的容器像java容器,他只能支撑java一种编程语言。以后你无论使用任何编程语言,使用任何操作系统,大家都可以统一在同一个平台上。docker容器,你用Ruby写,用python写,最后都可以打包成镜像。只要是镜像,我都可以docker run这么简单。是不是好玩很多?以往吧,我们使用这种技术,在分发和部署时,尤其是我们程序员做多版本,多环境下开发的时候是极其麻烦和困难的。但正是这种事情。正是这种容器技术的发展和大规模使用。极大的降低了软件开发的成本,和难度。在维护上,我们用一个分支足以解决问题。但是随之而来带给我们运维的问题就在于。第一,发布操作,我们以后可以使用编排工具来实现。其实docker是必须要靠编排工具来编排的就是容器。如果你没有编排的话,我们手动管理,其实真的手动管理好容器啊,比我们直接管理这个应用程序更麻烦。所以他增加了我们运维环境管理的复杂度。但是确实是,降低了,程序员开发时的复杂度。      第二个。真正出现问题,我们知道运维的核心工作在维稳,对吧。 那如果应用程序出现故障,我们以往调试是很很容易的。但以后调试很有可能那个容器内就没有调试工具。这么一来,就意味着我们以后做镜像需要为每一个镜像自带什么?调试工具,这样会使得我们系统很庞大的,因为你运行10个容器,每个容器自带的一堆东西,其实这东西本来是可以共享的,但现在不能了。但换来的好处就在于,他们确实是隔离的。而隔离在docker还有一个好处。docker是指docker使用容器的一种优势。刚刚说过docker在实现运行容器是通过在仓库当中下载镜像来实现的,对不对。于是,他还实现另外一个好处,如果想批量创建容器怎么办?如果你有十台服务器,每个服务器上运行10个10个容器,那么每一台服务器就docker下载一次就ok了,因为他运行容器采用一种非常有意思的方式,这是给他的镜像构建有关。docker的镜像的实现构建底层是通过所谓分层构建联合挂载的机制来实现的。我先简述一下,后面我们有一节专门来讲这个话题。叫分层构建,联合挂载来实现。意思是说,当我们做镜像是这样做的,你先做一个最基础的底层的centos镜像里面什么都没有纯净的,所以说我们想要使用一个nginx怎么办?我们不必要从头去建一个nginx,基于这个centos装一个nginx就行了,在这个centos之上装一个nginx。因此就成了一个nginx镜像,但是你的nginx镜像只包含nginx本身,不包含centos,他们是两层,底层是centos,上面一层是nginx,他俩叠在一块,就是一个运行在centos之上的nginx。所以这叫分层构建你一个功能,在一层上实现。随后你把它们叠在一起。形成一个统一视图,就相当于在一centos上有一个nginx,做联合挂载。这种好处就使得我们以后镜像分发,就没那么庞大了。比如说在这一个系统之上,需要运行三个容器nginx,tomcat,mariadb都是基于底层centos构建。所以在上面你只用一个centos基础镜像。再有三个不同的层,上层只有ngixn层,只有tomcat层,只有mariadb层,当我们需要运行nginx时,把centos这个底层镜像跟nginx联合挂载,第一个容器底层是centos,上面是nginx,第二个容器底层是centos,上面是tomcat,第三个容器底层是centos,上面是mariadb,底层那个镜像是共享的,因为每一个镜像都是只读的。注意,每一层镜像都是只读的。甚至比如说你想运行两个nginx没问题。假如我们要运行两个nginx容器,同一个centos底层的同一个nginx镜像。所以你就这一联合。我们创建两个容器,分别都挂在这两个镜像,联合挂载,那么他就运为,两个nginx容器。那么还有同学说到,如果在第一个容器我改了文件怎么办呢?那是不是也同为会影响第二个容器,那隔离还有什么意义呢?是不是这种疑惑?好,这就是docker所实现的另外一个功能。刚说了他们是只读的,你改什么改?改不了的。我们要想改只有一个办法,在每一层挂载的这个联合挂载的镜像栈的最顶层。额外附加一个新层。对呀,专门附加一个层,这个层才是一个能读能写的层,而且是容器自己专有的。所以第一个容器读写只能在自己的层中实现。那同学马上就想到了,我要删掉文件怎么办?假如这个文件正好属于以下这种情景。删不了。那就要删怎么办?就在这一层标记为不可见。那这么来实现。我要改呢。写时复制,把底层先复制上来到这一层,最上面一层在里边再改一改。我看的时候只看上面这层的,大家记得我们此前学到lvm快照,没错是不是类似这种效果。但这样一个性能就会很差了。确实性能会有影响。但他的确用那种很精巧的方式来构建。ok,但这么一来还有一个问题。什么问题呢?迁移依然很困难。你想我要把这个容器迁到其他属主机上去。那你底层这样有流数据的。是不是?这是叫有状态的。以有状态为耻,无状态为荣。因此,现在我们在真正使用容器时,我们不会在容器本地保存有效数据。所以你要打算持久使用的数据,我们干什么?在容器外部,在这个文件系统上面挂载一个外部的共享存储。一般我们需要一个共享的持久存储,比如像GlusterFS或者别的等等。我们通过网络把它挂出来,以后比如你是mariadb,用户需要保存数据,对不对,保存在哪儿啊?外存上。将来我们一不小心。这个服务器down了,或者是一不小心把这个mariadb给删了。没关系。你甚至在找一个主机。重新起mariadb。启mariadb无非就是把mariadb的镜像加载进来而已,定向在仓库里面,pull过来就可以了。启动以后。干什么,没错,挂载外存中的数据直接访问,数据还在。所以脱离数据,脱离容器而持久,容器你可以用在任何一主机上。那叫什么?云里雾里,或者叫云计算,是吧。因为他在哪儿,你不知道云里雾里的感觉,所以叫云计算了。但存储我们需要有外置的持久性存储,来确保数据在。大家有没有看到,这其实就是又是一个很好玩的功能。特别好玩的功能在什么地方?一个容器好像就变成一个进程。我们经常讲程序是由指令加数据组成对吧,指令放哪啊?程序文件中。数据放哪儿?文件或者放在外部的存储中,对不对。然后如果我把进程启动起来,比如vim我编辑了一个文件,然后把vim进程关了,vim编辑过的文件是不是会被删掉?不会,编辑一个文件,我们要靠vim进程来实现,不是什么去对吧?请问vim进程终止以后,文件是不是随着进程终止而就消失了,不会的。这儿容器是不是也实现这种功能。      启动一个容器跑一个进程,进程终止了,数据消失了没有,没有,在哪啊?外部存储上。那因此当这个容器终止时,你甚至可以把它干什么?删了,直接把容器当进程用,起一个容器,就是为了运行一个进程,进程一终止,容器也停,不要了,你下次再重新创建,再重新挂载上不就是了吗。容器不需要持久,所以容器有了生命周期,像进程一样,从创建而开始,从停止而结束。更重要的是,他跟我们的主机甚至都没有密切关联关系,可以运行在任何一主机上。所以随后我们就可以实现这种功能,假如你底层准备了十个主机,而后如果有一种功能给他们抹上这么一层,大家都有docker是吧,而后在docker之上面在建构一个层次。就行了,利用docker的功能,比如我在上面启动一容器,在这个中间层,统一层说我要启动一个容器,由他来决定找哪个主机可能更闲,更空闲一点。比如cpu更闲,或者内存更闲,或者io更闲。根据你的调度法则来决定,在其中任何一主机上,来启动一个容器。这个容器如果需要持久数据给他分配一个外部的存储空间,然后挂载上去,在里边存数据就行了。而后一旦那任务结束了怎么办?没错,容器一删,结束。因此这个组件能帮我们把要启动容器调度与整个底层集群环境中的,某一个docker host,我们称为叫docker主机之上,还有,当我们需要构建一个nmp的时候,那么n先启动,还是m先启动,还是p先启动。他们之间可能会有依赖关系,对吧,那这种依赖关系我们也需要事先定义好,最好让他们能按次序执行。而docker就没有这功能。那我们就需要一个在docker基础之上,能够把这种应用程序之间的依赖关系。从属关系、隶属关系等等,反映在启动,关闭时的次序和管理的逻辑中。这种功能叫容器编排。明白了吧,所以我们需要一款容器编排工具。没错,docker出现以后迅速出现了各种容器编排工具。比如像docker自己的swarm加compose,compose是单机编排的,我只能编排一个docker服务器之上的。比如nmp有可能就在一个主机上运行,但照样他需要编排compose实现这个功能。那如果把n个docker主机当一个主机来管理,swarm实现这个功能。所以我们可以使用swarm加compose。其实还有一个machine,包括使用这三个工具组合起来,当一个编排工具用,这是docker的。第二个是asf的,asf有个著名的数据中心操作系统,统一资源,编排编排工具叫mesos,但mesos不是用来专门编排容器的,它是实现统一资源调度和分配的。如果我们要实现能够编排容器,还需要加一个中间层叫marathon,第3个,就是kubernetes简称为k8s,好的,而更有意思的是,google这家公司秘而不宣的悄悄的。使用容器已经有十几年的历史。只是docker内部,据说每一周需要新建和销毁的容器就就多达几十亿的,他已经使用了十几年了。docker既然因缘巧合之间摸到这个门道。并且把它做成开源软件,google就坐不住了。你想,知道什么概念?自己本来是作为独门武器使用的,对不对?没想到那小子居然找了一种方法还公开给所有人使用。本来我应该是最有话语权的,对吧,但是我秘而不宣,他还藏起来,不让别人知道。这样才能成为独门武器嘛,必要时能成为必杀技,对不对,但后来发现这事藏不住。但是docker已然独霸了的这个话语权了。google现在就是说我有这个技术,我待公开出来。他也不敢呀,因为自己生产环境中使用的不可能公开出来,他在研发一个,但是docker已经占据人的心智了。好在docker阵营也不是铁板一块。docker自己阵营据说差点分裂,Docker最早的支持者之一硅谷初创公司CoreOS公布了一个开源项目Rocket, 就是另外一个公司叫CoreOS,他也有自己的容器引擎叫Rocket,那google就在后面。咱们美国经常这么干,对吧,叫扶持他的反对派。那在后面大力扶植CoreOS,但是后来发现CoreOS实在是难以跟docker抗衡,上不了台面,你就算给他提供武器,他也打不倒那个站台的。那你说怎么办?那你说怎么办?换道, 在容器这个领域。你已经成了事实上的这个标准。那容器刚才说我不会单独使用,要干什么。要做容器编排工具,而容器编排工具在google内部有一个重要的系统叫伯格(Borg),已经跑了十几年了。你想象一下,人家跑十几年在中间踩的坑早就踩够了,docker因缘巧合,找了一个小玩具来玩玩,没问题。那你要玩更大的这种规模没有经验,踩的坑肯定又少不了。别人十几年,作为一个大的公司需要十几年才能到今天,作为docker来讲三两年就做成可能吗?不可能,所以google就看到一个机会K8S横空出世,一出世就几乎横扫一切。你应该知道为什么原因很简单,十几年的经验就直接融合进来了。你do   cker绝对没有任何招架之力。K8S占据着80%左右使用份额。成了事实上的标准你没办法。在此基础之上,google还主导成立了cncf就是容器那个,所谓的标准组织。他做了什么呢?docker有个问题在于什么地方?有人说docker技术永生但docker公司已死,有人这么去形容。docker一手好牌打的稀烂。就是因为docker的编排这个领域中一无建树不说,他很多的这个所谓的。据说那我们作为一个后来者,事后诸葛亮去评判什么都没问题,对吧。你一旦站台你要成一个主持者说能不能带领这个docker一定要走向更好的这个方向也不一定。所以我们说在这个基础之上。dcoker有一个地方它是怎么做的呢,因为dcoker技术他一直没走好,没有找到一个变现的定位,为了让自己的这个估值能够继续做大,吸引更多的土豪来投资,或者将来上市做独角兽,那肯定要不断的去扩大自己的领域和份额,甚至能扩大自己的收益上的这些土豪们看到未来才行啊。而dcoker这个关键词在互联网火的不要不要的,但始终他没办法变现。所以后来dcoker决定。把docker的开源版。他做了社区版和企业版。后来他把这个社区版改名了,不在叫dcoker。而把docker的这个关键词的所有流量引入他们企业版的站点页面。所以对于后来有一段时间,社区为此这个闹得沸沸扬扬的。觉得docter这个过河拆桥啊什么之类的,就这种做法对不起社区啊。那叫后来他又把它改名叫Moby。把社区版那个标识符,把这个项目迁移到githup上,重新命名成叫做Moby。而后把dcoker的所有关键词全引向他的企业。所以让大家觉得docker使用了一个昏招。以至于后来CEO不得出面去解释。我们的目的不是这样子啊,我们是为了更好的去服务社区版,变得更加壮大之类。当然信你才怪着,大家就这拿出这副表情。而之所以能够做成这种结果,原因在于dcoker是一家商业公司,这个dcoker公司拥有了docker技术,拥有了dcoker这个代码往何处走的主导权。所以使得后来人们在用dcoker时,就因为dcoker这种做法对他产生疑虑。google在做kubernetes的时候,就为了避免走向这么,或者为了向大家表明,我是没有任何自己作为一家公司去控制k8ss走向的这种意图,所以他就成立了cncf,成立了cncf以后,而后就把k8s代码捐给cncf。而cncf是有很多组织和公司联合起来,成立了一个公共组织。那你去理解为,什么开源委员会就类似这种,不过它是容器管理的。所以google把k8s就交给他了,以后主导权不再属于google一家公司,而是有像微软,google,ibm这样的公司联合起来成立的组织的cncf委员会,所以你就不用再担心说google哪一天会把它私有化。那google这手做的使得大家对k8s简直算得上是趋之若鹜。所以这是使得k8s后来这个社区茁壮成长起来的原因。一年现在为止k8s还没有到一个稳定的阶段,一年四个版本的发布。当然原因还在于k8s还没有远没有到成熟阶段。还没有成熟,指的是很多功能还在不断的加进来。它使用go语言研发的,这就是容器历史上的悲欢离合、尔虞我诈,卿卿我我、耳鬓厮磨的故事。 
 
                          dcoker和kubernetes关系我已经说清楚了。不过呢dcoker在过河拆桥这条路上越走的还算是一路狂奔的。因为他早期是基于lxc构建的。当他后来成熟以后,就是略具实力以后就抛弃了lxc自建了一个容器引擎。当然这也不能说是他忘恩负义什么之类的。毕竟谁愿意背这个包袱呢,如果是他跟不上你的步伐。是吧,你俩大学同学,卿卿我我 谈恋爱了,3年以后,一个成长为是吧。一个原地踏步。如果不能共同成长,这是任何关系承受不住的打击。在dcoker和lxc也不例外。所以你想学linux运维,学云计算,我还能更好的去改变自己的生活理念,对吧,ok,所以你发现道理都是相通的是吧,看你会不会扯是吧,       好的,后来docker就开始研发一个容器引擎,叫libcontainer,替换lxc,所以现在版本的docker中已经没有lxc,但后来说过dcoker已经被cncf挟持了。为了dcoker为了占据一点话语权,在容器领域当中,不然了你看cncf自己去玩了。人家有一个大的组织。另起炉灶找一个组织,就是把docker排除在外,docker后来呢,cncf另起炉灶以后,他们已经纠集起了除了dcoker以外的容器领域的几乎全部力量。后来呢,看到一个概念说,如果以后容器要发展下去,肯定要走向标准化,肯定要开源,而且不能受控一家公司,对不对。那么谁来制作这个容器的标准化?因为容器运行起来,容器引擎要标准化,容器底层镜像文件格式要标准化,谁来负责做标准化呢?cncf自己走,有没有什么问题呢?没有任何问题。人要定义标准没有任何问题,但是这样子去做的话,明摆着呢欺负docker,所以给docker一机会,你来定义标准化。同时作为一款软件开源出来。所以后来libcontainer转到下一步叫rnc,现在我们在用的新版的docker他的容器引擎就叫runC,这也是一个所谓的容器运行时的环境标准。当然镜像文件也有标准。就他们遵循OCF叫做开放容器格式标准。这个标准成了事实上的工业标准。大家以后谁做容器就应该遵循这个标准。那么为此docker提供了一个所谓的参考实现也是大家经常用的实现,。当然了,不管我们怎么认为说docker被人欺负,但到今天为止docker确实是大家心知中事实上的容器技术。 所以到今天为止,k8s无论认为自己在编排上多么一家独大的。但他依然无法摆docker,大家可能做的k8s用的底层容器用的都是docker,虽然k8s支持很多种容器技术,docker只是其中的一种,所以我们先去学docker他的基本用法和基本使用逻辑。


        OCI(OCI叫做Open Container Initiative)
              Open Container Initiative      (开放容器倡议)
                    由Linux叁金会主导于2015年6月创立
                    (目标在于)旨在围绕容器格式和运行时制定一个开放的工业化标准(也就意味着大家只要用都应该遵循的标准)
                    contains two specifications(包含两个规格)
                          the Runtime Specification (runtime-spee)(运行时规范(runtime-spee)容器运行时,如何运行指定的 文件系统上的包)
                          the image Soccificaton (image-spcc)(镜像格式标准,如何创建一个 OCI 运行时可运行的文件系统上的包)
                    The Runtime Specification outlines how to run a "filesysten bundle" that is unpackedon disk(运行时规范概述了如何运行在磁盘上解压缩的“文件系统包”)

                    At a high-level an OCI implementation would download an OCI Image then unpack that image into an OCI Runtimc filesysten bundle(在较高级别上,OCI实施将下载OCI映像,然后将该映像解压缩为OCI Runtimc文件系统包)

        

        runC(后来就有了OCF格式)
              OCF: Open Container Formht(开放的容器格式)

              runC is a CLI tool for spawning and running containers according to the OCI specification(runC是一个CLI工具,用于根据OCI规范生成和运行容器(所以后来就有了OCF这个格式叫做开放的容器格式runC是他的主要实现之一。而现在runC就是较新版本的docker中,事实上所使用的容器引擎。)

                    Containers are started as a child process of runC and cau be cmbedded into various other svsteus without baving to run a dacnon(容器是作为runC的子进程启动的,并且可以嵌入其他各种svsteus中,而无需运行dacnon)

                    runC is built on libcontainer, the same container technology powering millions of Docker Engine installations(runC基于libcontainer构建,libcontainer是为数百万个Docker Engine安装提供支持的相同容器技术)
                          上面的学习让我们知道,要想使用linux容器,至少在linux内核级要支持两种技术,一种是NameSpaces我们称作叫名称空间。二是cgroups。接着我们可以借助于在用户空间组织一些工具,利用内核级所提供这些技术,从而实现容器运行之目的。而后docker在容器,运行使用简化的道路上又进一步他们提供了所谓镜像,而且是分层构建的镜像的方式,使得容器技术的使用更加被简化。后来呢在docker主导下就有了OCI和OCF这样的所谓的格式或者叫标准。      容器镜像是遵循所谓的容器镜像格式标准,所制作了很多镜像。而对于docker来讲,他们专门提供了一个容纳容器镜像的站点dockerhub.com这里可以找到大多数的容器镜像。比如说如果我们想使用对应的nginx镜像,搜索nginx关键词。对于nginx而言official表示的是官方给的镜像。所以说我们要想运行一个nginx容器,直接docker run nginx就ok。      



        docker architecture(码头建筑)

              The Docker daemon(Docker守护程序)
                    The Docker dacmon (dockerd) listens for Docker API requests and manages Docker objects such as images, containers, networks, and volumes(Docker dacmon(dockerd)侦听Docker API请求并管理Docker对象,例如图像,容器,网络和卷)
              The Docker client(Docker客户端)
                    The Docker clicnt (docker) is the primary way that many Docker users interact with Docker.(Docker客户端(docker)是许多Docker用户与Docker交互的主要方式。)

                    The docker command uses the Docker API(docker命令使用Docker API)
              Docker registries(Docker注册表)
                    A Docker registry stores Docker images.(Docker注册表存储Docker映像。)

                    Docker Hub and Docker Cloud are public registries that anyone can usc, and Docker is configured to look for inages on Docker Hub by default(Docker Hub和Docker Cloud是任何人都可以使用的公共注册表,并且Docker配置为默认情况下在Docker Hub上查找信息)

                    You can even run your own private registry.(您甚至可以运行自己的私人注册表。)
                          那他们到底是怎么下载,怎么使用的,我们来解一下docker的架构,整个docker架构有这样几部分组成。第一我们称作叫client端,第二叫server端,我们称作叫docker_HOST所以整个docker可以理解为它是一个c/s架构的应用程序。事实上,无论是c端还是s端,都有docker一个程序提供。这个程序有很多子程序,其中有一个子程序他的子命令叫build,就表示运行为守护进程,所以我们运行docker build,就表示把这台主机变成了一个所谓的守护进程服务器,他可以监听在某个所谓的套接字之上,这个套接字呢,为了安全起见,它默认只提供本地的unix socker文件的套接字,他支持三种类型套接字第一ipv4套接字,就是ipv4加端口,第二ipv6套接字,就是ipv6加端口,第三就是unix socker file或者称为unix socker套接字,他事实上监听在本地的文件上。这样子另外两个ipv4 和 ipv6是没有监听。那导致结果是什么?就像我们的mysql只监听127.0.0.1是一样的的逻辑,他只允许客户端是本地的,但是我们只需关心它是C/S架构的应用程序即可。      第二个,再看docker_HOST是真正运行容器的主机,他有什么特征呢?他底下关键字是两个,第一个叫Containers叫容器,而images叫做镜像,他们两个是docker主机上非常重要的组成部分。镜像来自于何处,我们才讲过,来至Registry翻译过来叫注册表,但是此处我们可以称为叫docker的镜像仓库,默认就是dockerhub。就到dockerhub去下载镜像,默认本地是没有的,刚才给大家解释过。镜像是分层构建的,所以下载到本地以后。可以共享,尤其是很多基础镜像可以共享给多个上层镜像使用。而且我也讲过,所有镜像都是只读的,对吧。所以启动容器时就是基于镜像来启动,在镜像基础之上,为一个容器创建一个专用的可写层,从而就等于一个容器,这就是我们称作什么叫容器。记住这是最关键的两部分,所以镜像也需要在docker主机的本地存储。但是我们到底在这个主机会运行哪些容器事先无法评估,因此这儿有个专门的仓库来放镜像,dockerhub上放的镜像估计有几十万个之多。所以你要把每一个都放在本地是不是空间就太大了。因此这也是为什么他放在公共存储上你用到哪一个下哪一个就行了啊,而没有放到本地的原因。但我们只要用,必须得把它先加载到本地,这里的协议是http,或https默认必须使用https为了安全,只有你自己确认,不要安全的,而且明确把它定为不安全,才能使用,同样的client端和docker_HOST端之间也是用http或https。因为docker的api也是一种标准的restful风格的api。当然我们这里是基于我们本地的socker文件通信,那就没必要考虑这么复杂,但你也要知道他的应用层协议也是http的。就可以了。好的,所以我们要使用docker,只需要把docker应用程序包下载下来安装即可,但是docker运行过程当中,尤其是创建容器,其实可能会有一点慢,尤其是第一次,原因在于他先去下载镜像了。这个下载镜像,所有要取决于你的互联网连接速度,更何况,docker的服务器还在国外。所以为了能够实现加速访问,docker在国内,就在大陆这边。做一个docker镜像服务器,我们称作叫dockercn,但是他们镜像加速好像效果不太好。当然你也可以使用。阿里云所提供的加速,那个速度绝对是很不错的。中国科技大,也有docker的加速镜像,大家可以自行选择。不过呢dockercn镜像是公开的,任何人都可以直接使用。而如果要使用阿里云的加速,一般而言,每一个人都有自己的私有账号。你需要到阿里云的开发者平台上注册一条,在阿里云要注册账号打开一个开发者平台,他会告诉你,你的专用加速链接是什么。      docker有所谓的企业版和社区版两个版本对吧,他们分别被叫做docker-ee和docker-ce。      那这就是我们所谓的docker的整体架构。那我们要想能够正常使用,很显然你的主机必须能够访问到互联网上去还行。


        镜像:静态(他不会运行)
        容器:动态,生命周期(容器是动态的,他有声明周期)
              这里需要简单解释一下仓库。Docker镜像是分层构建的,一个docker registries之上,它拥有两重功能。第一,它提供镜像存储的仓库。第二,他还提供用户来获取镜像时的认证等功能,同时还提供了当前服务下所有可用镜像的索引,搜索索引,那这种额外功能,它是一个应用程序。另外在一个一个docker registries之上仓库会有很多。但是仓库是这么搞的,一个docker镜像仓库有仓库的名称,就是一个仓库叫一个我们此前在yum中经常用的叫repository,我们通常把它简称叫repo而一个仓库通常只用来放一个应用程序的镜像。什么意思呢?各位知道我们的nginx假如作为镜像,对于nginx来讲,你做的镜像nginx也有程序版本假如是1.10的nginx,后来nginx升级到1.12了,你要不要做新镜像?明白我在说什么吧,所以镜像是不是也会有,应用到不同的应用程序版本上的镜像。所以为了能够使得镜像和应用程序的版本这些有一定意义上的关联关系。那么在docker上,通常一个仓库只放一个应用程序镜像。比如说这个仓库名就是应用程序名,我们这里是用来放nginx的,那么因此它的仓库源也就叫做nginx。仓库名就是你的应用程序名,,这个仓库内只放着一种应用程序,随后nginx是肯定会有很多版本,对不对,那1.10放一个镜像,后来nginx发展了有1.12,然后就有了1.14到今天,甚至还有他的开发板1.15。那怎么去标记这每一个镜像呢?他通过给每一个镜像额外添加一个组件叫标签,来标示着每一个镜像。比如第一个他就叫1.10版的nginx,那干脆我们就把这个镜像叫nginx冒号1.10(nginx:1.10)这叫做镜像名。所以用仓库名加标签才能用来唯一标识一个镜像。所以这个应用程序实际上是仓库名,而不是镜像名。镜像的标识是标签。那如果有人访问镜像的时候,我们只给了仓库名,而没给标签时候应该给他哪一个。那有默认的是吧,默认通常应该是最新版本,一个镜像可以有多个标签是没有问题的,所以说第一个镜像来讲,我可以给他取名叫什么。取个标签名1.15,同时我们还给他取一个标签叫latest叫最新版(nginx:1.15 nginx:latest)随后等1.6出现以后,那么这个最新版就不是1.15的引用了,我们可以让它引用到哪儿啊,1.6上去。所以nginx:latest这个标签会经常改的。latest通常会指向那个仓库中的最新版本,但也不一定有可能是最新的稳定版本,有的不同的组织方式不一样。你可以不过呢如果是这些稳定吧,比如在nginx来讲次版本号为基数是开发版对吧。因此我们要使用最新的稳定版,他同样应该是1.14的。而对1.14来讲,他这样来标记首先1.14有个标签(nginx:1.14)其次,他们还有stable(nginx:stable)叫稳定版。而稳定版通常是指向最新的稳定版,就是这个标签。。所以这么一来,大家明白在dockerhup上仓库是不是就非常多了。因为每一个应用程序都有一个仓库,比如redis有redis仓库,它里边有标签,什么3.0,3.2,3.4,4.0什么之类的,当然也应该有latest。      同时我们还有基础标准镜像标签就是基础操作系统。比如我就是一个独立的操作系统,centos的镜像它里面没有任何一个应用程序,只是centos自身有一个bash。最小化安装的centos,但对于centos我们也知道他有什么5.10,5.11,6.10什么之类的版本,对7来讲他有7.10,2,3,4,5,都有,都有这6个版本都有,只不过当前去访问linux:latest,应该使用,那当你访问centos7应该是这样7.5,大家注意这一点,所以你保证centos7和默认是七这个系列最新的。那你访问centos6呢,6系列最新的,除非您再给一个标签,比如叫centos6.4。,这就特定指定某一版本了。如果你不指,比如像centos冒号6(centos:6),那应该就是6系列中的最新版本。所以有些有些镜像仓库有了版本号就更乱就是更多更多组织形式。好,这是我们所听到的镜像仓库,而镜像仓库当中就是用来放镜像的。镜像是静态的,有点类似于我们的程序文件。镜像和容器的的关系,就行ls命令一样,比如有一个ls(/bin/ls),然后把ls拿来启动一个进程(ls /etc),再启动一个进程(ls /var)这俩进程是不是都是来自于同一个执行程序文件的,但他却属于两个进程,而且进程从创建开始到执行完了就结束了,对不对?那这个程序文件,只要你不删,他就始终存在。你看这个镜像和容器是不是有这种关系。镜像下载完以后就放在那里的,我们可以把它启动为一个容器,容器关闭容器就终止了,但镜像文件将始终存在,只要你不删除他。所以镜像和容器的关系就是程序和进程的关系,但以后一定要建立概念。很特别类似于程序,而且容器本身就是用来运行程序,就跟你运行进程一样一样。      我们要想运行容器,就像要想运行程序ls要先有程序文件。同样的,我们去运行ls时,通常不会给路径,对不对。只需要指明ls就行了。同样我们先启动容器时,您也不用指到哪儿去加载容器镜像文件。他有默认的加载路径。



        Docker objects(Docker对象)(Docker是restful风格的接口。所以在Docker上每一组件都会被当做资源或者对象来管理。)
              When vou use Docker, vou are creating and using images, containers,networks, volumes, plugins, and other objects(当您使用Docker时,您正在创建和使用映像,容器,网络,卷,插件和其他对象)
                    IMAGES(镜像)
                          An image is a read-only template with instructions for creating a D)ocker container.(映像是一个只读模板,其中包含创建Docker容器的说明。)
                          Often, an image is based on another inaze, with some additional customization(通常,图像是基于另一种图像,并进行一些其他自定义)
                          You might create your own images or you might only use those created by others and published in a registry(您可以创建自己的图像,也可以仅使用其他人创建并在注册表中发布的图像)
                    CONTAINERS(容器)
                          A container is a runnable instance of an image.(容器是图像的可运行实例。)
                          You can create. run, stop, move, or delete a container using the Docker APl or CLI(您可以创建。 使用Docker APl或CLI运行,停止,移动或删除容器)
                          You can connect a container to one or more networks, attach storage to it. or even create a new image based on its current state.(您可以将容器连接到一个或多个网络,并在其上附加存储。 甚至根据其当前状态创建一个新图像)
                                Docker是restful风格的接口。所以在Docker上每一组件都会被当做资源或者对象来管理。首先我们用到的资源和对象最主要的一个images(映像)。作为一个单独的对象,他可以增删改查。注意任何一个restful风格中的资源对象是可以支持增删改查的这些操作的。那因此镜像我们可以增删改查,containers容器我们可以增删改查,因为他也是对象,能理解吗。同样的networks网络也是一个独立的对象,可单独执行增删改查操作。另外还有volumes存储卷,卷着是为了能够给容器提供持久存储的存储空间,他通常是在外部的网络存储之上设备之上,那他也可以单独增删改查。另外还有plugins插件,增删改查,等等等等。那这些我们统统称为叫做objects对象。      那这些对象就可以使用标准的http协议的方法get,post,delete。那么对象也可以有对象结合,比如镜像。可以指向一个单独的镜像文件,也可以把镜像文件中的一组当做一个集合来进行操作。一组他也是个对象,也支持增删改查。      好,镜像文件是只读的这个概念一定要知道,是不允许做任何额外的修改操作的,除非你重构这个镜像。

安装及使用docker

  依赖的基础环境
        64bits CPU(      要使用64位的CPU)
        Linux Kerncl 3.10+      (目前来讲,为了更好的支持要求(比如user这个3.1之后才添加进内核),我们至少应该是有3.10及其以上版本的内核,centos7刚好符合规定)
        Linux Kernel cgroups and namespaces

  CentOS 7      (如果我们想安装docker有两种方式。CentOS 7在他的Extras当中有个repository,这里需要交代一下,centos6也支持安装使用docker,但centos6是2.6.32的内核是吧。那他为什么支持呢?是红帽把这些补丁给他补进去了。他只不过版本号没改,但它用起来。有其他问题,有很多不稳定因素,所以你要想跑docker一定要跑在centos7以后的版本上。      对于centos7来讲他的"Extras" repository默认就启用的,在这个仓库中有docker)
        "Extras" repository

  Docker Daemon
        systemctl start docker.scrvice

  Docker Client
        docker |OPTIONS] COMMAND [arg...]

配置yum源

# 查看Extras源是否启用
[root@beijing-crm-local-dev-004 ~]# yum repolist
!extras/7/x86_64                                   CentOS-7 - Extras - mirrors.aliyun.com                                                   413

# 查看Extras源中的docker版本(这个源里的版本可能比较老)
[root@beijing-crm-local-dev-004 ~]# yum list docker
docker.x86_64                                             2:1.13.1-162.git64e9980.el7.centos                                             extras

# 使用docker镜像文件

## 使用阿里镜像
# step 1: 安装必要的一些系统工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# Step 2: 添加软件源信息,如果yum-config-manager不能用,语法错误,可以使用curl -o /etc/yum.repos.d/docker-ce.repo  https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# Step 3: 更新并安装Docker-CE
sudo yum makecache fast
sudo yum -y install docker-ce
# Step 4: 开启Docker服务
sudo service docker start

# 注意:
# 官方软件源默认启用了最新的软件,您可以通过编辑软件源的方式获取各个版本的软件包。例如官方并没有将测试版本的软件源置为可用,您可以通过以下方式开启。同理可以开启各种测试版本等。
# vim /etc/yum.repos.d/docker-ee.repo
#   将[docker-ce-test]下方的enabled=0修改为enabled=1
#
# 安装指定版本的Docker-CE:
# Step 1: 查找Docker-CE的版本:
# yum list docker-ce.x86_64 --showduplicates | sort -r
#   Loading mirror speeds from cached hostfile
#   Loaded plugins: branch, fastestmirror, langpacks
#   docker-ce.x86_64            17.03.1.ce-1.el7.centos            docker-ce-stable
#   docker-ce.x86_64            17.03.1.ce-1.el7.centos            @docker-ce-stable
#   docker-ce.x86_64            17.03.0.ce-1.el7.centos            docker-ce-stable
#   Available Packages
# Step2: 安装指定版本的Docker-CE: (VERSION例如上面的17.03.0.ce.1-1.el7.centos)
# sudo yum -y install docker-ce-[VERSION]



## 使用清华大学镜像站(这个repo文件是清华大学镜像下载下来的但他不一定是镜像,查看一下这个文件可以看到这是一个下载连接,从https://download.docker.com下载的在官方站点下载是非常慢的,因此还要去改他。)
[root@localhost ~]# cd /etc/yum.repos.d/
[root@localhost yum.repos.d]# wget https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo
baseurl=https://download.docker.com/linux/centos/$releasever/$basearch/stable

# 修改清华大学镜像站的源(:%搜索全文s@查找https://download.docker.com/linux/@查找替换为https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/)
[root@localhost yum.repos.d]# vi docker-ce.repo 
:%s@https://download.docker.com/linux/@https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/@
18 substitutions on 18 lines(18行18换人)

# 查看仓库(可以看到82,这表示里面已经有程序包了)
[root@localhost yum.repos.d]# yum repolist
docker-ce-stable/7/x86_64                                 Docker CE Stable - x86_64                                                          82

使用yum安装docker

  安装完成以后呢,不需要做任何修改,你可以直接去启动docker的服务,但是对于docker-ce来讲,他的主配置文件,默认加载的配置文件是一个所谓的json格式的配置文件位于/etc/docker目录下,整个目录默认还不存在,还要自己创建叫daemon.json它是一个json格式的数组,里边定义各需要定义的参数。这个目录默认还不存在,当你第一次启动完以后,他有可能会自动创建一个,它里面会生成一个文件。但第一次启动之前,我们要自己定义的话,你要自己创建。而我们的目的是什么呢?就刚才说过,为了期望我们下载镜像快一点,应该给它定义镜像加速器。      镜像加速器在国内有docker cn但是加速可能效果一般。除此之外,网易应该也有docker加速。国内还有很多云计算公司,阿里云加速器,中国科技大学,他们都是建构的容器之上的。
# 安装docker
[root@localhost yum.repos.d]# yum -y install docker-ce

# 添加镜像加速器(添加这个json格式的数组,意思是我们有一个key叫"registry-mirrors"(注册表镜像)后面跟上数组,里面添加地址,地址可以逗号隔开,添加多个)
[root@localhost yum.repos.d]# mkdir /etc/docker
[root@localhost yum.repos.d]# vi /etc/docker/daemon.json
{
        "registry-mirrors": ["https://registry.docker-cn.com"]
}

# 启动服务
[root@localhost yum.repos.d]# systemctl start docker.service

查看帮助

# 常用操作(这种写法还都是最原始的写法,他没有分组的格式。)
docker search      : Search the Docker Hub for inages      (在Docker Hub中搜索图像(根据我们自己所指的关键词去搜索镜像))
docker pull      : Pull an image or a repository from a registry      (从注册表中提取图像或存储库(搜索镜像以后可以使用docker pull下载到本地))
docker images      : List images      (列出本地所有镜像,(从这里以上这命令,都可以替换为 docker image pull, docker image ls这两个命令      ))
docker create      : Create a new container      (创建一个新的容器)
docker start      : Start one or more stopped containers      (启动一个或多个已停止的容器)
docker run      : Run a command in a new container      (在新容器中运行命令)
docker attach      : Attach to a running container      (附加到正在运行的容器)
docker ps      : List containers      (列出容器)
docker logs      : Fetch the logs of a container      (提取容器的日志)
docker restart      : Restart a container     (重新启动容器)
docker stop      : Stop one or more running containers      (停止一个或多个运行中的容器)
docker kill      : Kill one or morc running containers      (杀死一个或一个正在运行的容器)
docker rm      : Renove one or more containers      (取消一个或多个容器)

# 查看使用帮助
[root@localhost yum.repos.d]# docker
Usage:  docker [OPTIONS] COMMAND      (docker用法,docker关键字,加[OPTIONS]选项,是可选的,加COMMAND命令,这叫子命令)
Management Commands:(子命令又分为好几类,叫Management Commands管理命令,就是管理配置的,早期的docker是没有那么好的分类,每一个子命令都是单独的。所以底下为了背包袱前行,他把这些老的也都兼容了。这里会有一个比较有意思的地方。比如我们要去创建一个容器,你可以使用docker create,但除此之外,这里还有container,在container下还有子命令,可以使用docker container查看,可以看到container下面也有create。所以我们以后创建容器有两种用法,第一,docker create。第二 ,docker container create)
builder     Manage builds      (管理构建)
config      Manage Docker configs      (管理Docker配置(管理配置的))
container   Manage containers      (管理容器)
context     Manage contexts      (管理上下文)
engine      Manage the docker engine      (管理Docker引擎)
image       Manage images      (管理镜像)
network     Manage networks      (管理网络)
node        Manage Swarm nodes      (管理节点)
plugin      Manage plugins      (管理插件)
secret      Manage Docker secrets      (管理Docker机密)
service     Manage services      (管理服务)
stack       Manage Docker stacks      (管理Docker堆栈)
swarm       Manage Swarm      (管理群)
system      Manage Docker      (管理Docker)
trust       Manage trust on Docker images      (管理对Docker映像的信任)
volume      Manage volumes      (管理卷)

Commands:
inspect     Return low-level information on Docker objects      (返回有关Docker对象的低级信息)
rm          Remove one or more containers      (删除一个或多个容器)
rmi         Remove one or more images      (删除一个或多个镜像)

# 查看管理镜像帮助
[root@localhost yum.repos.d]# docker container
Commands:
  attach      Attach local standard input, output, and error streams to a running container      (将本地标准输入,输出和错误流附加到正在运行的容器)
  commit      Create a new image from a container's changes      (根据容器的更改创建新图像)
  cp          Copy files/folders between a container and the local filesystem      (在容器和本地文件系统之间复制文件/文件夹)
  create      Create a new container      (创建一个新的容器(不会启动))
  diff        Inspect changes to files or directories on a container's filesystem      (检查容器文件系统上文件或目录的更改)
  exec        Run a command in a running container      (在正在运行的容器中运行外部命令,他是允许的,但要指明了在这个指定的容器中执行的)
  export      Export a container's filesystem as a tar archive      (将容器的文件系统导出为tar存档)
  inspect     Display detailed information on one or more containers      (显示一个或多个容器的详细信息)
  kill        Kill one or more running containers      (杀死一个或多个正在运行的容器(杀死,强行停止))    
  logs        Fetch the logs of a container      (提取容器的日志)
  ls          List containers      (列出容器)
  pause       Pause all processes within one or more containers      (暂停一个或多个容器中的所有进程)
  port        List port mappings or a specific mapping for the container      (列出端口映射或容器的特定映射)
  prune       Remove all stopped containers      (暂停容器)
  rename      Rename a container      (重命名容器)
  restart     Restart one or more containers      (重新启动一个或多个容器)
  rm          Remove one or more containers      (删除一个或多个容器)
  run         Run a command in a new container      (直接创建容器,并把他启动起来)
  start       Start one or more stopped containers      (启动一个或多个已停止的容器)
  stats       Display a live stream of container(s) resource usage statistics      (显示实时的容器资源使用情况统计流)
  stop        Stop one or more running containers      (停止一个或多个运行中的容器)
  top         Display the running processes of a container      (显示容器的运行过程(哪些容器更耗资源,就像看进程一样,可根据资源消耗比例来进行排序))
  unpause     Unpause all processes within one or more containers      (取消暂停一个或多个容器中的所有进程(继续容器))
  update      Update configuration of one or more containers      (更新一个或多个容器的配置)
  wait        Block until one or more containers stop, then print their exit codes      (阻塞直到一个或多个容器停止,然后打印其退出代码)


# 查看rum子命令帮助
[root@localhost yum.repos.d]# docker run --help

Usage:  docker run [OPTIONS] IMAGE [COMMAND] [ARG...](注意IMAGE镜像是不可省的,必须要指明run那个镜像,默认如果不指这个镜像启动起来,启动为容器时运行什么命令,它将运行这个镜像的默认命令,当也可以COMMAND自己给他传递一个命令,加上ARG参数 。启动容器时还可以加上OPTIONS选项)

Run a command in a new container

Options:
      --add-host list                  Add a custom host-to-IP mapping (host:ip)(添加自定义主机到IP的映射(host:ip))
  -a, --attach list                    Attach to STDIN, STDOUT or STDERR
      --blkio-weight uint16            Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)
      --blkio-weight-device list       Block IO weight (relative device weight) (default [])
      --cap-add list                   Add Linux capabilities
      --cap-drop list                  Drop Linux capabilities
      --cgroup-parent string           Optional parent cgroup for the container
      --cidfile string                 Write the container ID to the file
      --cpu-period int                 Limit CPU CFS (Completely Fair Scheduler) period
      --cpu-quota int                  Limit CPU CFS (Completely Fair Scheduler) quota
      --cpu-rt-period int              Limit CPU real-time period in microseconds
      --cpu-rt-runtime int             Limit CPU real-time runtime in microseconds
  -c, --cpu-shares int                 CPU shares (relative weight)
      --cpus decimal                   Number of CPUs
      --cpuset-cpus string             CPUs in which to allow execution (0-3, 0,1)
      --cpuset-mems string             MEMs in which to allow execution (0-3, 0,1)
  -d, --detach                         Run container in background and print container ID      (在后台运行容器并打印容器ID(表示剥离当前终端的关系,否则他将一直占据的这个终端))
      --detach-keys string             Override the key sequence for detaching a container
      --device list                    Add a host device to the container
      --device-cgroup-rule list        Add a rule to the cgroup allowed devices list
      --device-read-bps list           Limit read rate (bytes per second) from a device (default [])
      --device-read-iops list          Limit read rate (IO per second) from a device (default [])
      --device-write-bps list          Limit write rate (bytes per second) to a device (default [])
      --device-write-iops list         Limit write rate (IO per second) to a device (default [])
      --disable-content-trust          Skip image verification (default true)
      --dns list                       Set custom DNS servers      (设置自定义DNS服务器)
      --dns-option list                Set DNS options
      --dns-search list                Set custom DNS search domains      (设置自定义DNS搜索域)
      --domainname string              Container NIS domain name
      --entrypoint string              Overwrite the default ENTRYPOINT of the image
  -e, --env list                       Set environment variables      (设置环境变量)
      --env-file list                  Read in a file of environment variables
      --expose list                    Expose a port or a range of ports
      --gpus gpu-request               GPU devices to add to the container ('all' to pass all GPUs)
      --group-add list                 Add additional groups to join
      --health-cmd string              Command to run to check health
      --health-interval duration       Time between running the check (ms|s|m|h) (default 0s)
      --health-retries int             Consecutive failures needed to report unhealthy
      --health-start-period duration   Start period for the container to initialize before starting health-retries countdown (ms|s|m|h)
                                       (default 0s)
      --health-timeout duration        Maximum time to allow one check to run (ms|s|m|h) (default 0s)
      --help                           Print usage
  -h, --hostname string                Container host name(容器主机名)
      --init                           Run an init inside the container that forwards signals and reaps processes
  -i, --interactive                    Keep STDIN open even if not attached      (即使未连接STDIN也保持打开状态)
      --ip string                      IPv4 address (e.g., 172.30.100.104)
      --ip6 string                     IPv6 address (e.g., 2001:db8::33)
      --ipc string                     IPC mode to use
      --isolation string               Container isolation technology
      --kernel-memory bytes            Kernel memory limit
  -l, --label list                     Set meta data on a container
      --label-file list                Read in a line delimited file of labels
      --link list                      Add link to another container
      --link-local-ip list             Container IPv4/IPv6 link-local addresses
      --log-driver string              Logging driver for the container
      --log-opt list                   Log driver options
      --mac-address string             Container MAC address (e.g., 92:d0:c6:0a:29:33)
  -m, --memory bytes                   Memory limit
      --memory-reservation bytes       Memory soft limit
      --memory-swap bytes              Swap limit equal to memory plus swap: '-1' to enable unlimited swap
      --memory-swappiness int          Tune container memory swappiness (0 to 100) (default -1)
      --mount mount                    Attach a filesystem mount to the container
      --name string                    Assign a name to the container      (为容器分配一个名称)
      --network network                Connect a container to a network      (将容器连接到网络)
      --network-alias list             Add network-scoped alias for the container
      --no-healthcheck                 Disable any container-specified HEALTHCHECK
      --oom-kill-disable               Disable OOM Killer
      --oom-score-adj int              Tune host's OOM preferences (-1000 to 1000)
      --pid string                     PID namespace to use
      --pids-limit int                 Tune container pids limit (set -1 for unlimited)
      --platform string                Set platform if server is multi-platform capable
      --privileged                     Give extended privileges to this container
  -p, --publish list                   Publish a container's port(s) to the host      (将容器的端口发布到主机)
  -P, --publish-all                    Publish all exposed ports to random ports      (将所有公开的端口发布到随机端口)
      --read-only                      Mount the container's root filesystem as read only
      --restart string                 Restart policy to apply when a container exits (default "no")
      --rm                             Automatically remove the container when it exits      (退出时自动删除容器)
      --runtime string                 Runtime to use for this container
      --security-opt list              Security Options
      --shm-size bytes                 Size of /dev/shm
      --sig-proxy                      Proxy received signals to the process (default true)
      --stop-signal string             Signal to stop a container (default "SIGTERM")
      --stop-timeout int               Timeout (in seconds) to stop a container
      --storage-opt list               Storage driver options for the container
      --sysctl map                     Sysctl options (default map[])
      --tmpfs list                     Mount a tmpfs directory
  -t, --tty                            Allocate a pseudo-TTY      (分配伪TTY)
      --ulimit ulimit                  Ulimit options (default [])
  -u, --user string                    Username or UID (format: <name|uid>[:<group|gid>])
      --userns string                  User namespace to use
      --uts string                     UTS namespace to use
  -v, --volume list                    Bind mount a volume
      --volume-driver string           Optional volume driver for the container
      --volumes-from list              Mount volumes from the specified container(s)
  -w, --workdir string                 Working directory inside the container





# 显示Docker版本信息()
[root@localhost yum.repos.d]# docker version
Client: Docker Engine - Community
 Version:           19.03.13      (可以查看docker作为Client端,和Server端他们各自的版本)
 API version:       1.40      (他的API版本)
 Go version:        go1.13.15      (go版本,编译的时候使用的go)
 Git commit:        4484c46d9d
 Built:             Wed Sep 16 17:03:45 2020
 OS/Arch:           linux/amd64      (适用的平台版本)
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.13      (可以查看docker作为Client端,和Server端他们各自的版本)
  API version:      1.40 (minimum version 1.12)      (他的API版本)
  Go version:       go1.13.15      (go版本,编译的时候使用的go)
  Git commit:       4484c46d9d
  Built:            Wed Sep 16 17:02:21 2020
  OS/Arch:          linux/amd64      (适用的平台版本)
  Experimental:     false
 containerd:
  Version:          1.3.7
  GitCommit:        8fba4e9a7d01810a393d5d25a3621dc101981175
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

# 查看docker详细的环境信息
[root@localhost yum.repos.d]# docker info
Client:
 Debug Mode: false

Server:
 Containers: 0      (当前系统上容器有多少个)
  Running: 0      (处于运行状态的有多少个)
  Paused: 0      (处于暂停状态的有多少个)
  Stopped: 0      (处于停止状态的有多少个)
 Images: 0      (有多少个镜像)
 Server Version: 19.03.13      (服务器版本)
 Storage Driver: overlay2      (存储驱动后端(获取镜像以后,先存储在本地一个能够专门存储这种所谓的镜像的存储空间中。这个存储镜像的存储空间要求则是特殊而且专用的文件系统,这里1.18的docker版本他使用的是overlay2这种所谓的存储驱动。),overlay2这项特别重要,docker 镜像是分层构建,联合挂载,而分层构建和联合挂载要求必须使用特殊的文件系统才能实现。我们的什么xfs,什么EXT4之类的不支持的,他必需要使用专用的存储格式或者叫文件驱动也行)
  Backing Filesystem: xfs
  Supports d_type: true
  Native Overlay Diff: true
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Plugins:      (有哪些插件)
  Volume: local      (存储插件,支持哪些种)
  Network: bridge host ipvlan macvlan null overlay      (网络插件,支持哪些种)
  Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog      (日志插件,支持哪些种)
 Swarm: inactive
 Runtimes: runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 8fba4e9a7d01810a393d5d25a3621dc101981175
 runc version: dc9208a3303feef5b3839f4323d9beb36df0a9dd
 init version: fec3683
 Security Options:      (安全选项)
  seccomp      (这里支持seccomp就类似于selinux一样的东西)
   Profile: default
 Kernel Version: 3.10.0-693.el7.x86_64
 Operating System: CentOS Linux 7 (Core)
 OSType: linux
 Architecture: x86_64
 CPUs: 1
 Total Memory: 1.143GiB
 Name: localhost.localdomain
 ID: QLTR:DNS6:4YND:E3EU:AEUU:TAE3:724H:HYNF:HH3T:6CCW:MOTY:SEJC
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Registry Mirrors:      (注册表镜像)
  https://registry.docker-cn.com/      (这是我们配置进来的)
 Live Restore Enabled: false

WARNING: bridge-nf-call-iptables is disabled      (警告:bridge-nf-call-iptables已禁用)
WARNING: bridge-nf-call-ip6tables is disabled      (警告:bridge-nf-call-ip6tables已禁用)

搜索镜像

  跟我们打开dockerhub搜索结果很相识,name没有分隔符的是顶级仓库,一般都是dockerhub官方的,有分隔符的是用户仓库或项目仓库,这是这个人,他注册一账号,这个家伙单独自己建的。      如果我们发现这个镜像没问题了,我们就可以把它pull下来,直接使用了。要注意是nginx镜像默认应该是基于centos或者乌班图构建的,比较大。可能甚至接近100多兆的样子。所以pull下来会比较慢,因此nginx镜像其实它有很多种构建方式,甚至有较小格式的镜像。      我们仍然去打开他的web页面去搜nginx,搜到我们自己中意的镜像以后点击,打开二级页面,有一个Tags标签会告诉你有什么标签可以用比如stable大概有50MB大小,还有alpine这个很小了,才9兆。这是什么意思呢。alpine是一个专门用于构建非常小的镜像文件的。一个微型发行版。他能够给你的程序运行提供基础环境,但体积非常小。这可能会有什么问题里边缺少我们将来调试工具。因此,在生产环境中使用,相当不建议使用alpine版。尽量自己做镜像,而且里边带调试工具的。就算你从nginx官方下载,他也不过是建构在centos最小化安装之上里面也缺少各种各样的调试工具。
[root@localhost yum.repos.d]# docker search nginx
NAME                               DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
nginx                              Official build of Nginx.                        13912               [OK]                
jwilder/nginx-proxy                Automated Nginx reverse proxy for docker con…   1900                                    [OK]
richarvey/nginx-php-fpm            Container running Nginx + PHP-FPM capable of…   791                                     [OK]
linuxserver/nginx                  An Nginx container, brought to you by LinuxS…   127                                     
jc21/nginx-proxy-manager           Docker container for managing Nginx proxy ho…   101                                     
tiangolo/nginx-rtmp                Docker image with Nginx using the nginx-rtmp…   99                                      [OK]
bitnami/nginx                      Bitnami nginx Docker Image                      90                                      [OK]
alfg/nginx-rtmp                    NGINX, nginx-rtmp-module and FFmpeg from sou…   77                                      [OK]
jlesage/nginx-proxy-manager        Docker container for Nginx Proxy Manager        64                                      [OK]
nginxdemos/hello                   NGINX webserver that serves a simple page co…   62                                      [OK]
nginx/nginx-ingress                NGINX Ingress Controller for Kubernetes         42                                      
privatebin/nginx-fpm-alpine        PrivateBin running on an Nginx, php-fpm & Al…   39                                      [OK]
schmunk42/nginx-redirect           A very simple container to redirect HTTP tra…   19                                      [OK]
nginxinc/nginx-unprivileged        Unprivileged NGINX Dockerfiles                  19                                      
nginx/nginx-prometheus-exporter    NGINX Prometheus Exporter                       15                                      
centos/nginx-112-centos7           Platform for running nginx 1.12 or building …   15                                      
staticfloat/nginx-certbot          Opinionated setup for automatic TLS certs lo…   13                                      [OK]
raulr/nginx-wordpress              Nginx front-end for the official wordpress:f…   13                                      [OK]
centos/nginx-18-centos7            Platform for running nginx 1.8 or building n…   13                                      
bitwarden/nginx                    The Bitwarden nginx web server acting as a r…   7                                       
mailu/nginx                        Mailu nginx frontend                            7                                       [OK]
bitnami/nginx-ingress-controller   Bitnami Docker Image for NGINX Ingress Contr…   6                                       [OK]
flashspys/nginx-static             Super Lightweight Nginx Image                   6                                       [OK]
ansibleplaybookbundle/nginx-apb    An APB to deploy NGINX                          1                                       [OK]
wodby/nginx                        Generic nginx                                   1                                       [OK]

下载镜像

# 这是稳定版的alpine
[root@localhost yum.repos.d]# docker image pull nginx:1.14-alpine

# 有很多小的发行版,比如busybox表示一种很忙的盒子,busybox是一个程序一共也只有2MB的大小,但是他有很独特的性能,你可以在创建符号链接,把它链接成叫ls。它就像ls一样子在工作,跟ls命令一样。你把它链接成叫cat,他就像cat一样工作。链接能gz2,就像gz2一样可以压缩,听懂意思吗?一个程序能实现百来种,linux上最常用的命令。一个命令程序,链接成什么名字,他就能够扮演他那个程序的角色在工作。你说他是不是很忙,busybox。更奇葩的是,他完全可以实现进入一个busybox作一个用户空间出来,内核是内核加上busybox完全就是一个完整linux就够了。那安卓系统就是linux Kernel加上busybox,在busybox环境上又组装了一个所谓的jvm(java虚拟机)
[root@localhost yum.repos.d]# docker pull busybox

列出本地镜像

[root@localhost yum.repos.d]# docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              f35646e83998        12 days ago         133MB
nginx               1.14-alpine         8a2fb25a19f5        18 months ago       16MB
# REPOSITORY(仓库名称)
# TAG(标签)
# IMAGE ID(镜像自己的唯一标识)
# CREATED      (什么时间创建的)
# SIZE      (镜像大小)

# 显示完整的相关信息(会显示完整的sha256格式的id,默认情况下只会显示前12位)
[root@localhost yum.repos.d]# docker image ls --no-trunc
REPOSITORY          TAG                 IMAGE ID                                                                  CREATED             SIZE
busybox             latest              sha256:f0b02e9d092d905d0d87a8455a1ae3e9bb47b4aa3dc125125ca5cd10d6441c9f   11 days ago         1.23MB
nginx               latest              sha256:f35646e83998b844c3f067e5a2cff84cdf0967627031aeda3042d78996b68d35   12 days ago         133MB
nginx               1.14-alpine         sha256:8a2fb25a19f5dc1528b7a3fabe8b3145ff57fe10e4f1edac6c718a3cf4aa4b73   18 months ago       16MB


删除镜像

  被容器正在使用的镜像,不能删除
[root@localhost yum.repos.d]# docker image rm nginx:latest

列出容器

[root@localhost yum.repos.d]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
# 或
[root@localhost yum.repos.d]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
13cca67b41d4        busybox:latest      "sh"                14 minutes ago      Exited (0) 2 minutes ago                       b1

#或

[root@localhost yum.repos.d]# docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
# 或
[root@localhost yum.repos.d]# docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
13cca67b41d4        busybox:latest      "sh"                14 minutes ago      Exited (0) 2 minutes ago                       b1


# CONTAINER ID(容器id)
# IMAGE(用哪个镜像启动的容器)
# COMMAND(在容器中运行了什么命令。docker容器只是为了运行单个程序的,运行单个进程的。每个镜像都有他自己定义默认要运行的程序,你基于这个镜像启动时,它就启动。如果没指程序,就表示运行镜像自己指定的程序。但是我们也可以改,就是就不运行这个镜像默认的程序,换成别的也行。他里边还有其他进程。)
# CREATED(容器在什么时间创建的)
# STATUS(当前处于什么状态)
# PORTS(监听在什么端口)
# NAMES(容器也可以有自己的名字)
#  -a 显示所有容器(不加-a默认显示为正在运行不显示处于停滞状态的容器)

run跑一busybox个容器

  如果要运行为交互式接口,像使用一个虚拟机来使用它,那我应该给他附加上终端,没有终端是没办法打开shell的。交互式访问要使用-i,一般来讲启动容器时还可以给他个名字,那应该使用--name。如果有必要,你也可以启动容器时给他加入网络叫--network,如果不指默认加入bridge网络。另外容器停了,就是关闭,也可以让他自动默认就删除了,可以使用--rm。
# 基于busybox启用一个很小很小的这个用户空间,那里边可能只有几k几兆的大小,但是是有完整的linux,我们来领略一下,这个镜像是默认运行shell进程的
[root@localhost yum.repos.d]# docker run --name b1 -it busybox:latest
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var

/ # ls /bin      (ls一下/bin它里面有很多命令,这些命令他们都是busybox的别名)
[                  df                 hd                 lpr                paste              setlogcons         ts
[[                 dhcprelay          hdparm             ls                 patch              setpriv            tty
acpid              diff               head               lsattr             pgrep              setserial          ttysize
add-shell          dirname            hexdump            lsmod              pidof              setsid             tunctl
addgroup           dmesg              hexedit            lsof               ping               setuidgid          ubiattach
adduser            dnsd               hostid             lspci              ping6              sh                 ubidetach
adjtimex           dnsdomainname      hostname           lsscsi             pipe_progress      sha1sum            ubimkvol

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 sh      (sh的id号为1,表示整个用户空间的总管进程,因为他down了整个用户空间就down了,用户空间down了,相当于容器就关闭了。所以以后在docker中运行容器id为1的进程不是init,有可能就是你要运行的那一个程序,现在你要把sh一退出,整个容器就停止了)
    8 root      0:00 ps

# 比如在busybox种运行其他进程,他就是我们bash的子进程而已,比如httpd
/ # httpd -h
        -f              Don t daemonize      (不加-f就是后台,加-f就是前台)
        -p [IP:]PORT    Bind to IP:PORT (default *:80)      (指明监听在什么地址的,什么端口上)
        -h HOME         Home directory (default .)      (把哪个目录当做网页文件的根目录)

/ # mkdir -p /data/html
/ # vi /data/html/index.html
Busybox httpd server.

# 启动httpd(这个网页也建好了,-f让他运行在前台,现在我们就能访问他了)
/ #  httpd -f -h  /data/html/

# 另开一个终端,查看容器
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
13cca67b41d4        busybox:latest      "sh"                2 minutes ago       Up 2 minutes                            b1

# 查看容器地址
[root@localhost ~]# docker inspect b1
                    "IPAddress": "172.17.0.2",      (地址是172.17.0.2)

# 访问httpd(我现在是用的你这个特权用户空间跟刚才创建的那个新用户空间叫做b1的用户空间,容器里面那个程序进行交互的,相当于两个虚拟机之间的通信)
[root@localhost ~]# curl 172.17.0.2
Busybox httpd server.

# 退出容器
/ # httpd -f -h /data/html/
^C      (ctrl+c退出的是httpd程序)

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 sh
   11 root      0:00 ps
/ # exit      (执行exit表示退出sh,那sh已终止容器也就终止了)
[root@localhost yum.repos.d]# 

# 在查看容器(容器没了)
[root@localhost yum.repos.d]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

# 查看全部包括停滞容器
[root@localhost yum.repos.d]# docker  ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
13cca67b41d4        busybox:latest      "sh"                14 minutes ago      Exited (0) 2 minutes ago                       b1

run跑一nginx个容器

  -it就不要了,因为它里面运行的不是shell,没必要跟他交互,那他就可以-d到后台去运行了。本地或者刚刚给下去。而后指定镜像,如果运行的时候本地没有镜像,他会自动去下载你不用担心,他会自动去docker pull。只要你的docker hub能正常通过网络访问到镜像就会自动去下载到本地。
# nginx容器跑起来了 
[root@localhost ~]# docker run --name web1 -d nginx:1.14-alpine 
85d1f9bd71f2fbebbfc9aaac2c4c6cda9282f4aed735f15dae2dde1dd6abe6d3

# 查看一下容器(一个叫做nginx的容器跑起来了,而且它还PORTS监听了80端口,COMMAND运行的程序是"nginx -g 'daemon of…"什么之类的表示不要活动在后台,一个容器,就是为了运行一个程序,对吧。如果这个程序跑后台去运行,没有任何程序,他就以为这个程序终止了,那容器就结束了。在容器中跑任何程序,包括服务程序一定不能让它在容器种运行在后台的。只要运行在后台一启动就终止,nginx也不例外,因为他是唯一进程,他是骨架,骨架都没了,那就塌了。)
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
85d1f9bd71f2        nginx:1.14-alpine   "nginx -g 'daemon of…"   39 seconds ago      Up 39 seconds       80/tcp              web1

# 查看web1容器监听的地址
[root@localhost ~]# docker inspect web1
                    "IPAddress": "172.17.0.2",

# 访问nginx
[root@localhost ~]# curl 172.17.0.2
<title>Welcome to nginx!</title>

run跑一个redis容器

# 本地没有镜像,只要网络可以,他会自动下载
[root@localhost ~]# docker run --name kvstor1 -d redis:4-alpine


# 查看容器(这个redis,我们简称为kvstor1正在运行中)
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
2c0578ef5fc6        redis:4-alpine      "docker-entrypoint.s…"   39 seconds ago      Up 38 seconds       6379/tcp            kvstor1
85d1f9bd71f2        nginx:1.14-alpine   "nginx -g 'daemon of…"   24 minutes ago      Up 24 minutes       80/tcp              web1

# 测试一下,访问服务。测试要有redis客户端,这个镜像里面自带有客户端,我们可以绕过容器的边界登陆进去,我们现在要交互是登录进去看一下,redis怎么运行的
[root@localhost ~]# docker exec -it kvstor1 /bin/sh
/data #

# 查看容器内的所有进程(所以在一个容器中运行多个进程是可以的)
/data # ps
PID   USER     TIME  COMMAND
    1 redis     0:01 redis-server      (这里有默认运行的redis-server )
   12 root      0:00 /bin/sh      (和刚才交互式登陆的sh)
   17 root      0:00 ps      (以及sh的子进程,我们自己运行的ps)


# 是为了这样的目的去运行的,比如到里边去探测,去探查他的运行情况是不是正常,那可以使用netstat -tnl,看有没有监听6379端口
/data # netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:6379            0.0.0.0:*               LISTEN      
tcp        0      0 :::6379                 :::*                    LISTEN    

# 使用redis-cli命令连接一下(登陆了)
/data # redis-cli
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]> exit
/data # 


在容器中运行命令

# exec 指明在哪个容器中,执行什么命令,要能够执行sh,必须给他附加终端,并交互式登陆
[root@localhost ~]# docker exec -it kvstor1 /bin/sh
/data # 

停止容器

[root@ecs-kc1-large-2-linux-20200825091713 ~]# docker ps -a
CONTAINER ID  IMAGE                           COMMAND               CREATED      STATUS          PORTS                 NAMES
e01cb058cfd5  docker.io/library/nginx:1.16.0  nginx -g daemon o...  8 weeks ago  Up 8 weeks ago  0.0.0.0:8080->80/tcp  nginx
4d3f7adfaf95  docker.io/library/httpd:latest  httpd-foreground      8 weeks ago  Up 8 weeks ago  0.0.0.0:80->80/tcp    quirky_swartz
[root@ecs-kc1-large-2-linux-20200825091713 ~]# docker stop e01cb058cfd5 4d3f7adfaf95
e01cb058cfd54d503c78c56e8731e7dc29047d2af9971208504d1f61090a001f
4d3f7adfaf95c32fc8b5178f5ca54b70943f3e57ff4230bc8f25435b367e293a
[root@ecs-kc1-large-2-linux-20200825091713 ~]# docker ps -a
CONTAINER ID  IMAGE                           COMMAND               CREATED      STATUS                    PORTS                 NAMES
e01cb058cfd5  docker.io/library/nginx:1.16.0  nginx -g daemon o...  8 weeks ago  Exited (0) 3 seconds ago  0.0.0.0:8080->80/tcp  nginx
4d3f7adfaf95  docker.io/library/httpd:latest  httpd-foreground      8 weeks ago  Exited (0) 3 seconds ago  0.0.0.0:80->80/tcp    quirky_swartz


# 使用方法
[root@ecs-kc1-large-2-linux-20200825091713 ~]# docker stop --help
Usage:  docker stop [OPTIONS] CONTAINER [CONTAINER...](用法:  docker stop [选项] 容器 [容器...])

Stop one or more containers(停止一个或多个容器)

Options:(选项)
  -t, --time int   Seconds to wait for stop before killing it (default 10)(杀死它之前等待停止的秒数(默认为10))

激活所有处于停滞状态的容器(启动容器)

[root@localhost yum.repos.d]# docker start -ai b1
/ # 

# -a 连接STDOUT / STDERR和转发信号      (-i使用交互式接口,并附加到终端上,要使用-a,要使用-ai进入交互式接口。如果他此前运行的不是shell,就是nginx的一个守护进程,你不用加-i,也不用加-a,直接起就行。docker start 加容器名)
# -i 附加容器的STDIN      (要使用交互式接口,使用-i)

# 更换终端查看(容器b1,又处于up状态,启动38秒钟,CREATED容器创建时间是30分钟之前)
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
13cca67b41d4        busybox:latest      "sh"                30 minutes ago      Up 38 seconds                           b1

kill杀掉容器

# kill和stop区别在什么地方,就相当于一个给他发了-15信号,一个发了-9,就这样的区别,kill信号发送-9,强制终止。所以正常情况下,如果没出什么异常状态,一定不要强制终止,可能会导致数据丢失的。
[root@localhost ~]# docker kill b1
b1

# 查看容器(Exited 现在又处于停止状态了,停止状态的容器是可以直接删除的)
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                        PORTS               NAMES
13cca67b41d4        busybox:latest      "sh"                36 minutes ago      Exited (137) 27 seconds ago                       b1

删除容器

  停止状态的容器是可以直接删除的
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                        PORTS               NAMES
13cca67b41d4        busybox:latest      "sh"                36 minutes ago      Exited (137) 27 seconds ago                       b1
# 注意容器在Exited 退出或停止状态,否这可以使用-f选项强制删除(docker rm -f b1)
[root@localhost ~]# docker rm b1
b1
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES


# 查看帮助
[root@localhost ~]# docker rm --help

Usage:  docker rm [OPTIONS] CONTAINER [CONTAINER...]

Remove one or more containers

Options:
  -f, --force     Force the removal of a running container (uses SIGKILL)      (-f,--force强制删除正在运行的容器(使用SIGKILL))
  -l, --link      Remove the specified link      (-l,--link删除指定的链接)
  -v, --volumes   Remove anonymous volumes associated with the container      (-v,--volumes删除与容器关联的匿名卷)

容器关闭时自动删除容器

  --rm 容器关闭时自动删除容器
[root@localhost yum.repos.d]# docker run --name t1 -it --rm busybox:latest
/ # exit

[root@localhost yum.repos.d]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

在容器外注入容器主机名

  -h 在容器启动时设置容器主机名
[root@localhost yum.repos.d]# docker run --name t1 -it --network bridge -h t1.haoran.com  --rm busybox:latest
/ # hostname
t1.haoran.com

设置自定义DNS服务器

  --dns 给定dns地址
[root@localhost yum.repos.d]# docker run --name t1 -it --network bridge -h t1.haoran.com  --dns 114.114.114.114 --rm busybox:latest
/ # cat /etc/resolv.conf
search mshome.net      (搜索域,他是默认的,依然来自于主机)
nameserver 114.114.114.114      (这个地址是自己指定的)

设置自定义DNS搜索域

  --dns-search 给定搜索域
[root@localhost yum.repos.d]# docker run --name t1 -it --network bridge -h t1.haoran.com  --dns 114.114.114.114 --dns-search ilinux.io --rm busybox:latest
/ # cat /etc/resolv.conf
search ilinux.io      (搜索域,自己指定的)
nameserver 114.114.114.114      (这个地址是自己指定的)

添加自定义主机到IP的映射(host:ip)

  如果期望在hosts文件中自动解析,比如像www.ilinux.io这样的主机名怎么办,靠hosts文件来解析也可以,使用--add-host 后面跟上主机名加ip,先写主机名再写ip,以这种格式他能自动注入进来,生成为hosts文件中的解析记录,可以在外部注入hosts文件解析记录
[root@localhost yum.repos.d]# docker run --name t1 -it --network bridge -h t1.haoran.com  --dns 114.114.114.114 --dns-search ilinux.io --add-host www.haoran.com:1.1.1.1 --rm busybox:latest
/ # cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
1.1.1.1 www.haoran.com
172.17.0.2      t1.haoran.com t1

查看本地所有网络

[root@localhost yum.repos.d]# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
6d87738a0382        bridge              bridge              local
fe3a676e468a        host                host                local
afb57c48a2ef        none                null                local

查看docker对象基础信息

[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
13cca67b41d4        busybox:latest      "sh"                5 minutes ago       Up 5 minutes                            b1
[root@localhost ~]# docker inspect b1
# 或
[root@localhost ~]# docker inspect 13cca67b41d4

获取一个容器的日志

  因为日志只用单个程序运行,他就没必要非要保存在文件中,而是直接发到控制台上去的。使用log命令可直接获取。      显示曾经有一个172.17.0.1访问了我们的/主页,使用curl命令来访问的,这就是日志,也不用去看文件了,直接看控制台就行。这也是去调试容器,看看有没有问题的时候去获取相关信息的一个更加有效的手段。
[root@localhost ~]# docker logs web1
172.17.0.1 - - [25/Oct/2020:14:46:19 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"

#$ docker logs [OPTIONS] CONTAINER
#  Options:
#        --details        显示更多的信息
#    -f, --follow         跟踪实时日志
#        --since string   显示自某个timestamp之后的日志,或相对时间,如42m(即42分钟)
#        --tail string    从日志末尾显示多少行日志, 默认是all
#    -t, --timestamps     显示时间戳
#        --until string   显示自某个timestamp之前的日志,或相对时间,如42m(即42分钟)

#例子:
#      查看指定时间后的日志,只显示最后100行:
#            $ docker logs -f -t --since="2018-02-08" --tail=100 CONTAINER_ID

#      查看最近30分钟的日志
#            $ docker logs --since 30m CONTAINER_ID

#      查看某时间之后的日志:
#            $ docker logs -t --since="2018-02-08T13:23:37" CONTAINER_ID

#      查看某时间段日志:
#            $ docker logs -t --since="2018-02-08T13:23:37" --until "2018-02-09T12:23:37" CONTAINER_ID

docker镜像

  About Docker Images(关于Docker映像)

        Docker镜像含有启动容器所需要的文件系统及其内容(每个创建的容器都有根文件系统),因此,其用于创建并启动docker容器

              采用分层构建机制(这种分层和大体分为两部分),最底层为bootfs(第一部分,最底层的叫引导文件系统,我们称作叫bootfs),其之为rootfs(向上真正让用户拿来去构建与用户空间并运行进程的容器的,我们称作叫rootfs)      (底层的驱动,我们用到的是有sufs/btrfs或者overlay这样的文件系统来确保能够引导并启动一个用户空间。毕竟很有可能我们用户空间跟底层内核还是有一点点不同之处的。但是这个内核所实现的功能仅仅适用于引导并启动用户空间,启动完以后这个内核就没用了。所以说的很清楚。)

                    bootfs:用于系统引导的文件系统, 包括bootloader和kernel,容器启动完成后会被卸载以节约内存资源;(所以我们很多时候各位是看不见的,他是在底层的驱动之上来负责构建,任何一个非常基础镜像我们称作叫非常基础的镜像,其底部应该就会包含btrfs,向上那就是rootfs。)

                    rootfs:位于bootfs之上,表现为docker容器的根文件系统;

                          传统模式中,系统启动之时,内核挂载rootfs时会首先将其挂载为“只读"模式,完整性自检完成后将其重新挂载为读写模式;(在传统模式当中,学过centos6或者centos7的启动过程,对吧。他启动时根文件系统,被挂载以后先要做自检,为了避免自检时出现错误而导致数据被误删除,被误伤。所以一般自检之前挂载的应该是只读挂载的。自检完以后,如果没问题了才重新挂载为读写模式这是传统使用的。)

                          docker中, rootfs由内核挂载为“只读”模式,而后通过“联合挂载”技术额外挂载一个“可写”层;(但在docker中,整个rootfs是始终挂载为“只读”的,它不会重新挂载为读写模式。而后通过“联合挂载”技术额外挂载一个“可写”层)

        所以这里的分层构建涉及到这样很多问题。真正去做一个比如像apache镜像就是运行httpd的镜像,那么有可能在一个底层的非常基础的系统镜像之上,一个纯净的最小化的centos或者其他系统这样的版本,在他之上我们添加了一个编辑器emacs相当于vim一样。除此之外要添加一个httpd。这每添加一个软件都是一个独立的层次,所以这里是三层。底下这一层bootfs(kernel)在容器启动时,一旦被引导完了,rootfs时会被卸载并移出的,注意这个移除并不是删除文件,而是从内存中移除掉。因此,真正在用户空间运行的只有这三层(base image(debian或centos),image(add emacs),image(add apche),container(writable)),而这三层是有层级关系。最底层的叫做base image,他通常用来供给一个系统的基本构成。就是我们刚刚看到的什么bin,sbin,user等这样的目录但他是最小化的,没有已所依赖的应用程序。如果我们要用到某一个额外的一个工序的话,我们需要。在上面执行安装操作。各位知道,比如我们最小化安装了一个centos,接下来我们要用vim那么就执行yum install vim就完成了。但是对于我们的镜像来讲,底层的这个镜像本来就是最小安装的镜像,他是不会动的。你去额外安装一个vim。他会在这个系统之上再生成一个新的层次,这个层 可以理解为就是vim层,他只包含vim,在向上一层,在yum install 一次比如在装一个nginx,那么我们就额外装一个nginx他又生成一层,就是三层,随后我们要启动nginx,你需要把这三层都走下来。先启动最底层,挂载最底层,在最底层的基础之上,挂在第二层,然后再挂载,第三层,他们是叠加在一起挂载的。所以我们称作叫联合挂载。而后,这三层都是“只读”的。容器启动完以后,如果某一进程需要创建一些临时文件什么之类的,他应该放在tmp目录下。事实上,tmp所在的底层。基础镜像是不允许编辑的,要想能够编辑或者我们支持编辑的只能是最上层container(writable),这一层才是容器自有的,底下base image(debian或centos),image(add emacs),image(add apche)这些层都是可供多个基于镜像启动的容器所共享的层次。      因此,对于一个容器来讲,他的所有写操作仅能够在最上面这个container(writable)层次上,来实现。而且如果删除了容器这个writable也会被删除的,就是我们docker rm,删除容器,而不是删除镜像。当你删除容器以后,这个容器自有的可写层会一并被删除的。      这是docker的镜像层。

  Aufs

        advanced multi-layered unification filesystem:高级多层统一文件系统

        用于为Linux文件系统实现“联合挂载

        aufs是之前的UnionFS的重新实现, 2006年由Junjiro Okajima开发(从名字应该可以看出来他应该是一个日本人);

        Docker最初使用aufs作为容器文件系统层,它目前仍作为存储后端之一来支持;

        aufs的竞争产品是overlayfs,后者自从3.18版本开始被合并到Linux内核; 

        docker的分层镜像,除了aufs, docker还支持brtrs, devicemapper和vfs等
              在Ubuntu系统下, docker默认Ubuntu的aufs;而在CentOS7上,用的是devicemapper;

        刚才说到过镜像的这种分层构建和联合挂载依赖于专有文件系统的支撑才能实现。而早期用到的专有文件系统叫Aufs全称我们抽称作叫高级多层统一文件系统。      那么Aufs是最早被docker拿来用于实现联合挂载的linux文件系统,而aufs前身(就是上一代产品)叫UnionFS而aufs是UnionFS重构第二次实现。但是呢这一个文件系统UnionFS写的据说代码很烂。被重写以后Aufs,代码依然很烂。一个文件系统据说有三万行代码。三万行意味什么呢?据说ext4这样的文件系统只有区区四五千行代码。这代码是要被整合进内核的。所以aufs后来去申请要被合并进内核代码树的时候,你写的是一坨烂代码是不同意的。所以这个作者修改一次申请一次修改一次,申请一次,4次被打回来。最后一怒之下不再申请了。因此aufs一直都不是内核中自有的文件系统,想用你待自己向内核打补丁,编译使用的。很遗憾的是,centos所属的红帽发行版一般不会干这种出格的事儿,因为他们是以保守稳定著称的。因此,在centos系列的系统之上想使用aufs这种联合挂载文件系统没有可能,但是Ubuntu它还是略微激进一点。Ubuntu server是很早一批,就直接把aufs打包进,糅合进它的内核当中去,而且随它的发行版直接打包提供这个补丁过的内核。因此,早些时候我们要使用docker没有很好的选择。除了是有Ubuntu server这个要了解。      那另外aufs的竞争产品叫overlayfs,overlay叫叠加。所以UnionFS这叫联合文件系统或者叫统一联合文件系统,而overlayfs这叫叠加文件系统,虽然他们所实现的功能是一样的。而且overlayfs这个功能从3.18版开始,已经被合并进linux内核了。       那另外centos又不支持aufs早期可能overlayfs也不支持,你说怎么办?所以docker的分层镜像除了aufs之外。docker还支持基于brtrs文件系统或devicemapper的方式来实现,devicemapper我们称作叫dm他是红帽牵头来维护维持的一个内核模块,他就叫devicemapper是红帽上所使用的lvm用的一定是dm。既然docker也支持dm,那么很显然centos或者红帽应该推动在他的系统上使用dm来作为解决方案,所以说在centos7甚至centos6上在早些时候用的是devicemapper现在我们看到的如果daocker是新版的他也能启用的是overlayfs,而且overlayfs现在已经到第二版了,叫overlay2,既然如此我们就没必要再来了解devicemapper有人做过测试devicemapper在试用联合挂载方面性能很差,因为它使用所谓的超控的方式来实现。而且非常不稳定。既然如此,不用介绍他。各位只需要知道,我们的文件系统目前来讲比较成熟的支持的后端专用于在主机之上。无论是dockerhub就是docker registries上放镜像,还是我们本地,单机之上放镜像。为了支持这种所谓的多层实现联合挂载。必须要能够使用此前曾经让各位看到docker info输出的内容当中,这儿Storage Driver: 这里显示为overlay2,当然overlay2是一种抽象的二级文件系统,它需要建构在本地文件系统上,可以看到。他的后端的是  Backing Filesystem: xfs,这个xfs作为后端,前端用的是overlay2


  Docker Registry(Docker注册表)

        启动容器时, docker daemon会试图从本地获取相关的镜像;本地镜像不存在时,其将从Registry中下载该镜像并保存到本地;
              The Registry is a statcless, highly scalable server side application that stores and lets you distribute Docker images.

        我们去构建镜像时,镜像做好之后应该有一个统一存储的位置。这个统一存储的位置就叫做Docker Registry启动容器时docker daemon会自动试图从本地获取相关的镜像。先看你的本地Storage Driver所指向的存储空间中有没有镜像文件。如果有,那就直接使用,所以本地镜像文件不存在时,那将会从Registry中下载该镜像并保存到本地。      这个Registry再强调一遍,如果我们没有特别指定,那么他通常指的就是dockerhub。如果要指向别的Registry,我们必须在镜像的访问路径当中给明服务器地址,如果没给服务器地址,只指了仓库名,只指了tag。那么这个镜像一定是指dockerhub之上的镜像。因为是默认的,除非去改他的配置。

        Docker Registry分类

              Registry用于保存docker镜像,包括镜像的层次结构和元数据

              用户可自建Registry,也可使用官方的Docker Hub

              分类
                    Sponsor Registry:第三方的registry,供客户和Docker社区使用
                    Mirror Registry:第三方的registry,只让客户使用(我们的docker cn,或者我们称作叫docker 阿里云的docker加速器等等,他们都可以理解为是加速器。)
                    Vendor Registry:由发布Docker镜像的供应商提供的registry(这时服务商的Registry,比如红帽就有registry,但他可能并不开放给所有人使用。一般只开放给买了红帽操作系统服务的那些人去使用。 )
                    Private Registry:通过设有防火墙和额外的安全层的私有实体提供的registry(自建Registry这种Registry好处在于他至少不消耗互联网带宽。我尤其我们在本地大规模去部署某一个容器的时候,他们都要去拖镜像。很显然我们要从本地的镜像服务上拖镜像是不是要快的多,而且我们也说过互联网上的镜像就是dockerhub中的镜像,多数情况下我们都用不了,不是不让你用,而是他不符合你的需要,原因很简单,举个例子,下载nginx他的nginx配置文件,肯定不符合你的需要对吗。我们没准需要在原有的nginx基础上改一改配置文件,改成我们符合需要的再重新做一个新镜像。多加一层是改完配置文件的镜像,多加一层放在我们自己的私有层,复制上去使用它。所以一般来讲,都需要二次去定制镜像的。当然了,我们定制一般不是为了改配置文件。要改配置文件的话,这代价也太大了。)

  Registry(repository andindex)(注册表(存储库和索引))

        Repository

              由某特定的docker镜像的所有迭代版本组成的镜像仓库

              一个Registry中可以存在多个Repository

                    Repository可分为“顶层仓库”和“用户仓库”

                    用户仓库名称格式为“用户名/仓库名”

              每个仓库可以包含多个Tag(标签) ,每个标签对应一个镜像

        Index

              维护用户帐户、镜像的校验以及公共命名空间的信息

              相当于为Registry提供了一个完成用户认证等功能的检索接口

        一个Registry通常会有两部分组成,无论它是私有的还是第三方的什么之类的。第一部分就是各种各样的仓库,应该叫Repository更合适。因为它是由一大堆的仓库。然后有一个Index索引,共同组成,一个索引区和一大堆的仓库。每一个仓库通常是由特定的docker镜像的不同版本所组成的,而后一个Registry中可以存在多个Repository,需要注意的是Repository中可分为“顶层仓库”和“用户仓库”这两类。其中顶层仓库是直接有仓库名,而用户仓库他应该是用户名加斜线加仓库名(用户名/仓库名)所分割,而一个仓库里的镜像标识,我们可以使用Tag(标签)来标识,一个仓库上可以有多个Tag,用来分别引用不同的镜像,其实一个镜像也可以有Tag,但一个Tag只能属于一个镜像。      Index的作用在于维护用户账号、镜像的校验信息以及公共名称空间信息,就是我们有哪些用户账号,因为用户账号是可以作为将来仓库里的前缀使用的。      好的,除此之外,他应该是提供一个检索接口,让用户使用docker search 就使用类似命令时能基于索引来搜索我们这面对应关键词有哪些镜像的存在。

              Docker Registry

                    Docker Registry中的镜像通常由开发人员制作,而后推送至“公共”或“私有”Registry上保存,供其他人员使用,例如"部署"到生产环境;

                    而Docker Registry中的镜像来自于何处啊。通常应该是有镜像开发人员所制作。注意他是镜像开发人员所制作,而后我们要给他推送至公共环境或私有的Registry上保存的,供其他人员使用。      这个Developer开发人员可通过Docker private registry(Docker私有注册表)或 Docker public registry(Docker公共注册表)去pull,能拖到本地来一些镜像,在原有镜像的基础之上。做一些额外的修改,生成一个新的层次。就做出了新镜像了,把新镜像可以推到自己的Docker private registry私有仓库中去。而后就可以由我们运维人员所使用,他们可以把这些镜pull到,就是拖到这些服务器上来完成部署并启动,这些环境可能有UAT(测试),Promote(推广),Production(生产)不同环境它的镜像里面的配置信息可能还会有所不同,对不对,没准我们还有多个不同版本的镜像或者叫不同配置的同一个镜像。刚刚一直在讲,如果配置信息,通过制作镜像来做的话,会非常头疼。因此,现在有一个术语,各位应该在不同城场景当中听说过叫cloud native叫云原生。什么叫云原生啊。此前讲过,程序员要写程序,他应该是针对于某一个开发环境时的程序,对吧。比如我们要写c程序,应该根据c环境。没错,尤其是我们系统级开发,应该针对的是我们的系统库编程接口,他调用系统库或者你自己补进来的这三方库当中的库文件去生成自己的代码,然后这个代码就能够运行在我们系统级的环境之上。这叫系统原生,那么云原生什么意思?说白了,他就是面向云环境中去运行这个程序。而调云系统本身既有的功能,而开发应用程序,它就是为了云计算环境运行而生,所以我们把他称作叫云原生。很显然像早期那些开发运在单机上的应用程序,比如像什么nginx。如果我们把它托管到容器云上去运行,他有着诸多不便之处,最大的不便在你改配置文件很麻烦。那些云原生开发的程序会使得对云计算这个应用程序怎么配置更方便。他用那种配置接口来给你提供所谓的配置逻辑。而容器本来就加了一层外壳,我们去操纵里边的数据是不方便的。所以后来人们想了一办法用的比较广泛的就是通过向容器启动时传环境变量来看信息。那环境变量赋值来看信息,然后你的配置文件你应该是容器启动时从环境变量加载自动注入到配置文件中生成的。这是早期常用的解决方案。你就意味着我们通过环境变量来配置容器的启动。但是nginx好像,很少说我们能够接受环境变量以后,把它自动注入到配置文件中去,对不对?所以说他不是云原生,而云原生一定大量配置都是直接向通过环境变量来获取数据。而不是自己非得去修改配置文件,改配置文件参数,来配置。这个要先有个概念。好,那这是我们,提到的docker Registry中,他们对应的镜像来自于何处。要注意。多数情况下,我们做镜像应该是基于别人已存在的某一个基础镜像来实现的,把它称为叫做 base image。比如像一个纯净版的最小化的centos,Ubuntu。各位有没有想过这个最小化的Ubuntu镜像从何而来? 我们镜像为了更简单,应该是基于别人已经已有的基础镜像在上面再加一些层次来实现,就这么来做。但问题是这个基础镜像从何而来?一般是dockerhub的相关维护人员就是docker官方他们手动制作的。比如我们自己做一个镜像很简单,按照镜像格式我们把它拼凑起手工把他建立好。要把他打包成镜像格式就行了。那这个做起来对他们的专业人员来讲是非常容易的。但对我们的很多中端用户做起来是比较困难的。但如果有了基础镜像在做上层镜像就比较容易了。

  Docker Hub

        Docker Hub is a cloud-based registry service which allows you to link to code repositories, build your images and test them, stores manually pushed images, and links to Docker Cloud so you can deploy images to your hosts.(Docker Hub是基于云的注册表服务,可让您链接到代码存储库,构建映像并对其进行测试,存储手动推送的映像以及与Docker Cloud的链接,以便您可以将映像部署到主机。)

        It provides a centralized resource for container image discovery, distribution and change management, user and team collaboration, and workflow automation jhroughout the development pipeline.(它为整个开发管道中的容器映像发现,分发和变更管理,用户和团队协作以及工作流自动化提供了集中式资源。)

        想必已经认识到dockerhub的重要性了是吧。默认Docker daemon一定是从到了dockerhub上去拖各种各样的镜像,而且她也能存用户上传的第三方镜像,建议注册一个账号

              Docker Hub provides the following major features(Docker Hub提供以下主要功能)
                    Image Rcpositorics
                          Find and pull images from community and official libraries, and manage, push to, and pull from private image libraries to which you have access.(从社区和官方库中查找和提取图像,以及管理,推送和从您有权访问的私有图像库中提取。)

                    Automated Builds
                          Automatically create new images when you make changes to a source code repository.(对源代码存储库进行更改时自动创建新图像。)

                    Webhooks
                          A feature of Automated Bulds. Webhooks let you trigger actions after a successful push to a repository.(自动构建的功能。 使用Webhooks,您可以在成功推送到存储库后触发操作。)
                    Qrganizations
                          Create work sroups to manage access to image reposttorics.(创建工作清单以管理对图像后置信息的访问。)

                    GitHub and Bitbucket Integration
                          Add the Hub and your Docker Images to your current workflows.(将集线器和Docker映像添加到当前工作流程中。)

                    在dockerhub之上,如果你登录以后会发现里边有这样几个标签。Image Rcpositorics,Automated Builds,Wcbhooks,Qrganizations,GitHub and Bitbucket Integration来简单介绍一下。这几个标签分别表示了他们的不同功能。比如第一个叫Image Rcpositorics指的是镜像仓库,你可以自己在你的名称下,建镜像仓库。比如说注册了账号123,那就再123下创建一个nginx这里专门放的我们自己做的nginx镜像,每一个就表示一个镜像仓库,你自己做的仓库就在Image Rcpositorics就是你个人账号。      第二个叫Automated Builds这叫自动构建,做镜像,我们需要在本地。早些时候需要在本地或者这至少是其中一种途径,更加早些时候。使用命令docker build,基于docker file来做。 第二种呢,也可以基于容器来说,大家知道我们的容器使用起来以后,上面有一层叫可写层,对吧。对可写层里面你可能做了很多改变,比如yum install 一个程序包,回头我们就把这个可写层给他固定下来,做成一个镜像是可以的,直接做。我们使用docker commit命令就能实现。这都是我们手动去做的。但是标准方法建议使用docker file去做,那定义完以后,我们需要在本地执行docker build这是一种方式。还有一种方式,你可以把你的docker file文件直接放在Automated Builds这个位置,那就不用在本地做了对源代码存储库进行更改时自动创建新图像。但是呢这里的自动构建是通过联动的方式来实现的,怎么联动呢。你想象一下我们的docker file放哪儿啊,还记得github吗。好,我们可以在github上创建一个项目project比如说。这个project里面放的就是我们做镜像的docker file文件。然后我们把文件推到这么一个github的仓库中,这个仓库可以与docker hub的仓库建立起关联关系,而docker hub仓库可以持续监控着这个,这个github的仓库。你这里边放的一旦创建了一个docker file文件,docker hub他就把这个docker file拖过来,自动做成镜像,并帮你放在你的仓库当中。这样实现的。所以以后我们想创建新版的镜像怎么办,你自己应该有开发主机对不对,在你的开发主机上,把这个docker hub改一改,是不是有新版本了,然后把你的docker file文件再给他推到github仓库上,github上的变动,docker hub会看见,docker hub会在自动给他拖过来做成镜像,就类似于这种形式,      。那么关注哪一个github仓库Webhooks就帮助实现这个功能的,叫Webhooks,通常叫web钩子,,是我们自动构建功能的一种特性,一种特征。它能够干什么呢?就触发某个行为,刚才解释了你自己本地做了一个docker file,新版文件给他推到github上,而这个github的变动会通知给你的docker hub,而docker hub会把这个文件给他拿过来自动构建成镜像。Automated Builds 和 Webhooks这两个是有关联性的。      Qrganizations这个表示组织,你可能会属于某个组织的成员之类的,去创建一个工作组,大家可以携同起来工作。      GitHub and Bitbucket Integration表示我们可以跟GitHub或者Bitbucket进行整合,有了解就行了。对我们来讲可能用的更多的应该还是。Image Rcpositorics,Automated Builds好的,这是给大家介绍的docker hub原生所支持的功能有哪些

获取镜像

  Getting inages from renote locker registries(从远程Docker注册表获取图像)

        To get locker images from a remote registry (such as your own Docker registry) and add them to your local systen, use the docker pull command:(要从远程注册表(例如您自己的Docker注册表)获取更衣柜映像并将其添加到本地系统,请使用docker pull命令:)
              
              docker pull <registry>[:<port>]/[<namespace>/]<name>:<tag>(docker pull <指明哪一个仓库服务器,如果地址是docker hub可省略>[:<加端口,如果地址是docker hub可省略>]/[<指明名称空间,就是指哪个用户的仓库,如果是顶层这一步是可省的>/]<仓库名>:<标签名>)

              The <registry> is a host that provides the docker-distribution service on TCP <port> (default: 5000)(<registry>是在TCP <port>上提供docker-distribution服务的主机(默认值:5000))

              Together, <namespace> and <name> identify a particular image controlled by <namespace> at that registry(<namespace>和<name>一起在该注册表中标识由<namespace>控制的特定映像)

                    Some registries also support raw <name>; for those, <namespace> is optional(有些注册表还支持原始的<name>;对于这些注册表,<namespace>是可选的)

                    When it is included, however, the additional level of hierarchy that <namespace> provides is useful to distinguish between images with the same <name>(但是,如果包含它,则<namespace>提供的附加层次结构有助于区分具有相同<name>的图像)
# 世界上除了docker hub之外,还有很多可用的仓库服务器的。比如有个著名的叫quay.io这个站点,我们将来可能会用到flannel这是个非常重要的在k8s上会用到的镜像。如果像这种镜像,我们需要获取到本地进行安装的话。但他不是默认的镜像仓库,所以我们要下载必需指定这种格式docker pull quay.io/coreos/flannel指定服务器地址quay.io端口没有指默认就是443端口,因为他是通过https协议来获取的,所以不指端口默认是443。这里的coreos应该是一个用户名,就是我们所谓叫名称空间中的名字,它应该有个用户名叫coreos,不是顶层仓库。而后在coreos下有个仓库名叫flannel而后我去获取flannel哪个镜像,要加冒号加tag加标签就行了。 没加标签意味着latest最新的,

[root@localhost ~]# docker pull quay.io/coreos/flannel:v0.10.0-amd64


基于容器制作镜像

  镜像的制作途径刚才说过,一般有这样的两种方式,第一用docker file。我们自己使用一个命令叫docker build来制作,这是专门用来做镜像的。另外一个基于容器来做,比如某个容器我们已经启动并处于运行当中,我们使用docker commlt会把容器的最面可写层。单独创建成一个镜像,一个镜像层。 第三个,给大家解释过,docker hub上有Automated Builds,其实Automated Builds还是基于docker file来实现的。正常来讲,因此他只能算得上是制作方式当中的制作途径不同,但基础的制作路径他就是docker file。那我们这会儿简单给大家说一说怎么去基于容器来做,而docker file是一个大类后面介绍。            我们看看怎么去基于所谓的容器来做镜像,基于容器做镜像,你先启动一个容器,在容器中做好,你打算做的修改。比如说启动一个简单的centos,在centos之上使用yum install 安装一个nginx。而后你就可以把nginx上面安装生成nginx文件这一套的,生成的那个writable的可写层单独做成镜像就使用docker commit的来做就行了。举个例子,我们仍然以最小化的系统为例来说。比如我们此前曾经用过的那个 busybox我们假设把busybox做成一个这样的镜像。在busybox基础之上,只有基础系统,我们给它加一个/data/html目录, 在里面创建index.html网页。这个网页默认是不存在的,对不对。我们现在给他加进去,加进去之后呢,把这个加的结果做成镜像,以后基于我们做的镜像在启动,这是一个最简单的示例了。
# 他默认运行的程序就是bin/shell,运行起来后,这时候在根目录下一定没有data目录的
[root@localhost ~]# docker run --name b1 -it busybox
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # 

# 创建一个目录,并创建一个网页叫index.html(这个文件要注意的是,你下次只要基于busybox启动创建,它里边这个文件是一定不会有的,对不对。我期望把我刚才改变的内容,这个改变的结果给他保存下来)
/ # mkdir -p /data/html
/ # vi /data/html/index.html
<h1>Busybox httpd server.</h1>


# 启动新终端,基于容器制作镜像(现在我们不能关闭容器,一般来讲要让容器处于运行状态。而后基于容器来做镜像)
[root@localhost ~]# docker commit -p b1
sha256:a28d840fa54567455602da2299cb360525db562ef5fb3e4beef551461c794d59

## 帮助
[root@localhost ~]# docker commit --help

Usage:  docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]](用法:docker commit [OPTIONS] 基于哪个容器做镜像 [做完以后这镜像他属于哪个仓库[:拥有什么标签,可省的,如果省略就表示你做出一个镜像,就是本地裸镜像,不属于任何仓库,也没有任何标签]])

Create a new image from a container's changes(根据容器的更改创建新图像)

Options:
  -a, --author string    Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")(作者(例如,“约翰·汉尼拔·史密斯<hannibal@a-team.com>”))
  -c, --change list      Apply Dockerfile instruction to the created image(将Dockerfile指令应用于创建的映像)
  -m, --message string   Commit message(提交讯息)
  -p, --pause            Pause container during commit (default true)(提交期间暂停容器(默认为true))(做镜像过程中,容器中的这个应用程序,它在运行着,他还不断在生成新文件。那么很有可能,你保存下来的文件有些是一半的,对吧。为了避免产生这样的问题,也可以使用-p选项让他停下来,所以做镜像过程,你尽量让容器停下来。)

# 查看镜像(可以看到这里有一个镜像,REPOSITORY仓库为<none>空,TAG标签也为<none>空,只有一个IMAGE ID, 35秒钟之前创建的(35 seconds ago),SIZE大小1.23MB)
[root@localhost ~]# docker image ls
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
<none>                   <none>              a28d840fa545        35 seconds ago      1.23MB
busybox                  latest              f0b02e9d092d        2 weeks ago         1.23MB

添加标签

  (为了引用起来方便,也可以给他加上标签,加标签后期管理标签使用docker tag命令,这叫专门的标签管理命令。打成什么标呢,比如他属于docker hub仓库上的,就是docker hub服务器上我们有一个用户账号就叫做hoaran,我期望给他上传到docker hub上去这个haoran的账号目录下的叫做httpd这个仓库中,打个标签用来标识镜像,比如v0.1-1,事实上你也可以,给他打第二个标签,比如现在它是最新版,他应该叫latest。打第二个标签引用时就可以不用id了,使用haoran/httpd这个名称,      他们两个的id是一样的,因此我们删除一个标签,如果还有其他标签使用这个镜像,是不会被真删的,你只是删除一个标签。)
[root@localhost ~]# docker tag a28d840fa545 haoran/httpd:v0.1-1
[root@localhost ~]# docker image ls
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
haoran/httpd             v0.1-1              a28d840fa545        18 minutes ago      1.23MB

[root@localhost ~]# docker tag haoran/httpd:v0.1-1 haoran/httpd:latest
[root@localhost ~]# docker image ls
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
haoran/httpd             latest              a28d840fa545        25 minutes ago      1.23MB
haoran/httpd             v0.1-1              a28d840fa545        25 minutes ago      1.23MB


## 帮助
[root@localhost ~]# docker tag --help

Usage:  docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG](用法:docker tag 可以给哪个镜像,引用一个镜像时,他如果本来就又标签,我可以在已有的标签之上,给他再打个标签。一个镜像是可以有多个标签,如果他一个标签都没有,那只能使用id来引用他[:TAG] 打上哪个标签,这个标签应该是他所属的仓库名和标签名[:TAG])

Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE(创建一个引用了SOURCE_IMAGE的标签TARGET_IMAGE)

删除标签

  如果是一个镜像有多个标签,他们两个的id是一样的,因此我们删除一个标签,如果还有其他标签使用这个镜像,是不会被真删的,你只是删除一个标签。
[root@localhost ~]# docker image rm haoran/httpd:latest
Untagged: haoran/httpd:latest

运行新创建的镜像,并查看创建的文件是否存在

  这就是保存了,我们此前曾经创建内容所生成的镜像文件。而做镜像也就这么简单,这叫基于容器做镜像。但是这一个新镜像还没有改变默认要运行的命令。我们假设希望这个容器启动起来以后不再运行shell。而运行httpd,httpd有一个对应的选项叫-f就表示运行在前台,-h这指明他的网页文件的根目录,如果不指就是当前目录,我们已经刚好准备好文件了/data/html/index.html 随后我们再做一个镜像,在基于此镜像启动容器以后。默认运行的就不再是shell,而是httpd。
[root@localhost ~]# docker run --name t1 -it haoran/httpd:v0.1-1
/ # ls /data/html/
index.html
/ # cat /data/html/index.html 
<h1>Busybox httpd server.</h1>

/ # httpd -h
        -f              Don't daemonize(不要守护)
        -h HOME         Home directory (default .)(主目录(默认)。)

创建镜像,容器启动时运行指定命令

  镜像定义了基于此镜像启动容器时默认要运行的程序。我们使用docker inspect看一下这个镜像,启动有一个地方叫做Cmd他表示,如果基于此镜像启动容器默认运行我这里定义的命令。      接下来我们如果希望在创建镜像时改变原来镜像中,我们的容器一定是基于某个镜像启动的。我们现在基于这个容器创建新镜像时,我们期望这个镜像默认运行命令不再是原来命令,什么意思呢?比如我们刚才创建的镜像haoran/httpd这个镜像他默认运行的命令还是busybox这个基础镜像的命令,我们使用docker commit的时候额外加一项,看帮助又一个选项叫-c 意思就是修改原有基础镜像要运行的命令。叫docker file的指令。我们这儿呢简单一点,只是改里边cmd指令。所有方法很简单。
[root@localhost ~]# docker inspect nginx:1.14-alpine
 "Cmd": [
      "nginx",
      "-g",
      "daemon off;"      
],      # 这表示让nginx运行在前台,因为这个容器只运行单个程序,对吧,这个程序必须要运行在前台,否则容器一启动他就到后台去。这个容器我们认为就结束了。

# 制作镜像(注意:被制作镜像的容器不能关闭,-c我们期望他默认运行的命令,不在是shell,使用CMD指令必需是大写的,给他一个列表,列表中写上命令,这里是/bin/httpd命令,第一个参数是-f运行在前台,第二个参数是-h,-h还有一个参数是网页文件路径是/data/html/下,其实-f和-h参数他俩可以写在一起,但是-h和指定的路径中间有空格,所以把他俩分开写也行,只要次序不错。      -p镜像制作时容器要现处于暂停状态,而后我们要基于t1来做,制作后的镜像直接起名haoran/httpd这是第二个了标签为:v0.2)
[root@localhost ~]# docker commit -a "HaoRan <haoran@haoranedu.com>" -c 'CMD ["/bin/httpd","-f","-h","/data/html"]' -p t1 haoran/httpd:v0.2
sha256:e5bff338324b89a73804352535e084decc7dfac09f57d73b9e182a29d8823e41

[root@localhost ~]# docker image ls
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
haoran/httpd             v0.2                e5bff338324b        26 seconds ago      1.23MB

# 基于v0.2镜像启动一个容器(-it就不比了,因为他不在使用交互式了,也不用加上-d了不让他在后台去,就让他在前台跑,对应的我们要用的镜像就是haoran/httpd:v0.2,启动后没什么信息,因为他不是交互式接口,因为他默认运行的不是shell,运行的是httpd。)
[root@localhost ~]# docker run --name t2 haoran/httpd:v0.2

# 新启一个终端,查看容器(可以看到这个容器已经About a minute大于一分钟了,CMD运行的是"/bin/httpd -f -h /d…")
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
d23b040b9e08        haoran/httpd:v0.2   "/bin/httpd -f -h /d…"   About a minute ago   Up About a minute           

# 查看这个容器的地址,并访问httpd服务(直接web服务就能访问,基于busybox做一个默认就直接运行web服务器,很危险的web服务器的镜像,就制作好了。)
[root@localhost ~]# docker inspect t2
                    "IPAddress": "172.17.0.5",

[root@localhost ~]# curl 172.17.0.5
<h1>Busybox httpd server.</h1>



# 帮助
[root@localhost ~]# docker commit --help

Usage:  docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

Create a new image from a container's changes(根据容器的更改创建新图像)

Options:
  -a, --author string    Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")(作者(例如,“约翰·汉尼拔·史密斯<hannibal@a-team.com>”))
  -c, --change list      Apply Dockerfile instruction to the created image(将Dockerfile指令应用于创建的映像,意思就是修改原有基础镜像要运行的命令)
  -m, --message string   Commit message
  -p, --pause            Pause container during commit (default true)

推镜像到仓库

  做好镜像了,假如我们测试也能用,可以共享给别人,可以放到仓库上,比如docker hub上,当然你要确保你的仓库,确实有账号。我没在docker hub上有一个账号就叫haoran所以我们要确保我们刚才打了标签的仓库叫httpd,如果没有Create Repository +创建一个仓库,取名叫httpd,描述假如叫busybox httpd就行了,选择Visibility仓库要不要公开,选择

Public公开,好了以后点Create创建,这个时候仓库就创建完了,这个时候我们就可以往上推镜像,就推到这个仓库里面来。 注意,你需要清楚,你本地的标签跟你远程仓库,他的名称一定要保持一致才能推上去,使用docker image ls可以看到镜像标签。推镜像使用docker push

# 在推镜像之前有可能需要你现登陆,不然的话你不登录,对这个仓库没有访问权限。
[root@localhost ~]# docker login -u haoran
Password: 
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded(登陆成功会提示Login Succeeded登录成功)



## 帮助
[root@localhost ~]# docker login --help

Usage:  docker login [OPTIONS] [SERVER](用法:  docker login [OPTIONS] [如果就是登陆docker hub这个SERVER可以不用指,如果是登陆别的服务器,后面要给定server登陆哪个服务器,])

Log in to a Docker registry.(登录到Docker注册表。)
If no server is specified, the default is defined by the daemon.(如果未指定服务器,则默认值由守护程序定义。)

Options:(选项)
  -p, --password string   Password(密码)
      --password-stdin    Take the password from stdin(从标准输入密码)
  -u, --username string   Username(用户名)

# 推镜像(不加标签,我们吧这个镜像下的所有标签都推上去了,如果成功的话,在这个仓库的tags中所有的镜像都在这儿了,以后别人就可以直接用这些镜像了,因为我们做的是公开的仓库,别人访问的方式就是使用docker pull haoran/httpd加上标签就ok了)
[root@localhost ~]# docker push haoran/httpd
The push refers to repository [docker.io/dockerhaoran/httpd]
0a47a073afef: Pushed 
199a7909234c: Pushed 
d2421964bad1: Mounted from library/busybox 
v0.2: digest: sha256:18e03b7c398664a0f0dd7268146e2d54fa6d8cea31dfa38712fccf7ef3c1cb37 size: 941



## 帮助
[root@localhost ~]# docker push --help

Usage:  docker push [OPTIONS] NAME[:TAG](用法:  docker push [OPTIONS] 指定name[:加标签,不加标签,我们也可以使用-a这样类似选项把一个镜像的所有标签都推上去])

Push an image or a repository to a registry(将映像或存储库推送到注册表)

Options:(选项:)
      --disable-content-trust   Skip image signing (default true)(--disable-content-trust跳过图像签名(默认为true))

使用阿里云的镜像仓库

  国内访问比较快的镜像服务器平台建议用的时候就用阿里云的。而且登陆阿里云后有个人可用的很多设定。包括我们个人可用的加速器地址应该都在这儿。他会告诉我们,如果你想加速的话如何配置指定系统,写的很详细,复制粘贴就可以了。      如果要想用阿里云的镜像服务,这里有镜像仓库,在这里也可以创建镜像仓库。

使用阿里云加速器

# 不同地址用逗号隔开,重启才会生效
[root@localhost ~]# vi /etc/docker/daemon.json 
{
        "registry-mirrors": ["https://******.mirror.aliyuncs.com","https://registry.docker-cn.com"]
}

# 重启docker容器
[root@localhost ~]# rpm -ql docker-ce
/usr/bin/docker-init
/usr/bin/docker-proxy
/usr/bin/dockerd
/usr/lib/systemd/system/docker.service
/usr/lib/systemd/system/docker.socket
[root@localhost ~]# systemctl restart docker.service

把镜像推到阿里云容器仓库中

  一样的先创建仓库,现选择你仓库服务器属于阿里云的那个服务器,比如使用华北1,名称空间是haoran,注意这个是自己创建的,也就是说,你登录阿里云账号是一个账号,你的仓库前缀这个号是可以自己定义。仓库名还叫httpd,摘要叫busybox httpd,填完以后选择公开还是私有,下一步,我们这个镜像从哪来,这里选择从本地仓库推上来的,选择别的就是表示他要基于你在这些代码仓库上的docker file自动触发自动构建的。我们不要,就点本地仓库,点击创建。      一个叫httpd的仓库就有了,      对于这个仓库的使用点管理,会有操作指南,以后我们要用的时候怎么用。      区别最大的就是必须要使用指定格式的标签前缀,所以要把镜像重新打标,为阿里云镜像仓库的指定格式的标签,才能推到阿里云的镜像仓库服务器上去      所以之前一直在解释如果不是docker hub,你必须跟上服务器地址,名称空间,和仓库名,加标签这种逻辑才行      登陆阿里云镜像服务就可以推镜像了,如果之前登陆过别的镜像仓库,要先docker logout登出,在docker login登陆到阿里云的镜像仓库,操作指南上都有提示。      阿里云这个是他比较独特的地方,甚至登录阿里云账号的密码,不是去使用镜像时的密码。需要单独去设置在我们对应的后台,设置Registry自己的登陆密码,      登陆成功后就可以往阿里云镜仓库中推镜像了,镜像已经在阿里云镜仓库中了,也可以在这里选择安全扫描一下,看看有没有问题,有没有漏洞。前一段时间。有人向docker hub传了很多镜像,里边有那个所谓挖矿代码,所以很多人使用他们镜像部署容器以后,直接被别人拿来挖矿。所以我们一定不要谁便使用。公开仓库当中不知来源的,就你不要随便下那些用户的镜像,要用一定要用官方的。

镜像导入和导出

  那再看另外一种镜像的共享方式叫镜像的导入和导出。我们如果有两个服务器。一个node01,一个node02,假设node02也装了docker,现在node02想用node01的镜像怎么办?node01自己做的镜像。刚才我已经给了路径了。第一我先把它推到一个公共的位置。第二,node02使用docker pull 就能用了,这种方式,好像还是有点麻烦。假如果只是测试用的,我在一个主机上做完以后到另外主机上跑一下就行了。没必要推到docker hub上去推来推去,下来下去的更麻烦。还有一个更简单的办法,我们可以直接在已有镜像主机上把镜像打包,打包成一个压缩文件。到另外一个主机上再把它装上就行了。所以就要用到两个命令,第一个命令叫docker save叫保存打包文件,第二个命令叫docker load可以从打包文件给他直接装入,而且一次打包还可以打包多个文件
# 打包镜像(他可以打包并压缩,没有指定路径,就表示在当前路径下,创建一个myimages.gz文件,把这个文件通过scp或者其他文件爱你共享方式就行了)
[root@localhost ~]# docker save -o myimages.gz haoran/httpd:v0.2 haoran/httpd:v0.1-1
[root@localhost ~]# ls
anaconda-ks.cfg  myimages.gz

## 帮助
[root@localhost ~]# docker save --help

Usage:  docker save [OPTIONS] IMAGE [IMAGE...](用法:docker save [OPTIONS] 镜像 [镜像...])

Save one or more images to a tar archive (streamed to STDOUT by default)(将一个或多个图像保存到tar存档(默认情况下流式传输到STDOUT))

Options:(选项)
  -o, --output string   Write to a file, instead of STDOUT(写入文件,而不是STDOUT,使用-o指定保存在哪个文件中)


# 发送镜像
[root@localhost ~]# scp myimages.gz 192.168.162.130:/root/


# 加载镜像
[root@localhost ~]# docker load -i myimages.gz 
d2421964bad1: Loading layer [==================================================>]   1.45MB/1.45MB
199a7909234c: Loading layer [==================================================>]  4.608kB/4.608kB
0a47a073afef: Loading layer [==================================================>]   2.56kB/2.56kB
Loaded image: haoran/httpd:v0.2
Loaded image: haoran/httpd:v0.1-1

[root@localhost ~]# docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
haoran/httpd        v0.2                0d60852cb307        12 hours ago        1.23MB
haoran/httpd        v0.1-1              a28d840fa545        14 hours ago        1.23MB


## 帮助
[root@localhost ~]# docker load --help

Usage:  docker load [OPTIONS]

Load an image from a tar archive or STDIN(从tar存档或STDIN加载图像)

Options:
  -i, --input string   Read from tar archive file, instead of STDIN(从tar存档文件中读取,而不是从STDIN中读取,-i表示从哪个文件load这表示使用输入指定的方式。)
  -q, --quiet          Suppress the load output(抑制负载输出)


docker容器网络

  现在的linux内核在内核级已经直接支持6名称空间。分别是主机名和域名的叫UTS,用户的叫User,挂载文件系统的叫Mount,进程间通讯的IPC,进程ID的Pid,还有网络名称空间Net。网络这种名称空间在内核的较早版本当中2.6.29应该是就已经添加进去了,也就意味着在我们现在这个系统内核之上,默认这六种名称空间功能只要有相关的名称空间工具支持,我们是可以直接操作的。所谓网络名称空间主要用于实现协议栈和网络设备隔离的实现,什么意思呢?假如这是我们一个物理机。举个例子,物理机它有4快网卡,我们现在要创建两个名称空间。而这些设备是可以单独关联给某一个单独的空间使用。比如说在这儿起一个名称空间,我们可以把这个网卡,分配给第一个名称空间使用。那么这个时候其他名称空间就看不见这个设备了。一个设备,一般只能属于一个空间。同样的我们这里要是有4快网卡,可以使用4个名称空间。一个名称空间,使用一个。就能使得这每一个名称空间都能配给ip地址,并且能够与外部进行通信,因为他们是物理网卡就是没有任何问题的。      那么试想一下,假如说我们现在的名称空间数量超过物理网卡数量,怎么办?每一个名称空间内部的进程,也需要通过网络进行通讯。该如何是好?在KVM的网络架构中讲过可以使用。虚拟网卡设备。用纯软件的方式来模拟一组设备来使用。 Linux内核级支持两种级别设备的模拟,一种是2层设备,一种是三层设备。      先说二层设备,我们知道我们的物理网卡本来也只能算得上是二层设备,那么这种各位更容易理解,他就是一个工作在链路层,能封装物理报文,实现在各设备,网络设备之间实现报文转发的组件。而这个功能是完全可以在linux之上利用内核当中对二层虚拟设备的支持,创建虚拟网卡操作的。而且这种虚拟网卡接口很独特,每一个网卡,网络接口设备它是成对儿出现的。可以模拟为一根网线的两头。一头可以插在我们的主机之上模拟,一头可以插在交换机之上模拟。那就相当于让一个主机连到交换机上去了,而Linux内核原生支持2层虚拟网桥设备,就是用软件来构建一个交换机。      好的,那一个软件交换机,一个软件所实现的虚拟机。像刚才说的名称空间自己创建一个网卡,有两头,一头分配给这个名称空间,一头把它分配给交换机,就相当于模拟了让一个主机连到交换机上来了。同样的,如果你有两个名称空间,这两个名称空间都这么干。各自创建一对虚拟网卡一半留在名称空间上,一半留在交换机之上模拟交换机的接入的机制。那么我们就能实现网络连接的功能,他两个就好像是连到同一个交换机上的两台主机。很显然,如果他们两个的网卡配置的网络地址在同一网段,是不是就可以直接通信了,这很简单。这就是所谓的叫做虚拟化网络,从网络设备通信的物理设备到网卡都是用纯软件的方式来实现,我们能够在一台主机之上用软件方式来实现,所以我们把它称作叫网络虚拟化技术当中的一种简单的实现。      有一个著名的应用程序叫OVS把它称作叫OpenVSwitch(开源的虚拟交换机)。用纯软件方式实现一个交换机,它还能模拟实现非常高级的三层网络设备所实现的功能,像什么vlan技术,vxlan技术,gre技术,甚至流控技术就是我们所谓的SDN叫软件驱动的网络 ,他都能支持但存软件实现功能非常强大。只不过他不属于linux内核模块本身所支持的功能,你需要额外安装这么一个软件。而这个软件之所以这么强大,是因为它是由system等众多专业的网络设备生产公司联合研发的一款软件。而现在在云计算的大潮下,我们知道我们构建一个云计算中心,其实构建网络,应该是最复杂的一方面了,对吧,然后才是网络之上承载的主机,他才能通信的。而这个网络虚拟化为了实现更高级的功能,那需要软硬件结合起来,来实现而且把传统意义上的网络平面,控制平面,数据平面就是传输平面等等给隔离开来实现将控制平面集中到一个专业的设备上实现全局调度。这就实现了所谓叫SDN的机制叫软件定义的网络或者软件驱动的网络。        那这些几乎都跟网络虚拟化技术相关。所以我们要将来做云计算中心的网络虚拟化时它需要既要在硬件设备层面支持,还需要在每一主机之上构建很复杂的虚拟网络来。毕竟在一个物理服务器上有可能要运行多个虚拟机或者多个容器。这每个容器都需要用到网络。      朋友们应该已经了解,如果是在同一个物理机上分两个容器或者两个名称空间想通信,我们办法就是在这同一个主机上建立一个虚拟交换机建一个软交换机,而后呢,我们让两个虚拟机或者两个容器都行,名称空间就相当于容器了。让两个容器,各自用纯软件的方式建一对儿虚拟网卡。那么一半在容器上,一半在交换机上,让他们俩连起来。就相当于一根网线让你的交换机和你的服务器或者虚拟机或者称作叫容器的网卡建立通信联系了。这是单节点之上的两个容器间通信。当然单节点之上的两个容器间通信可能也有一些复杂状况。比如我们期望这里所构建的容器,这是在同一个交换机上的,那假如我们有多个交换机怎么办?有没有可能会有多个交换机呢?我们做两个软交换机,两个软交换机上各自连接有不同的容器,比如这里有c1,c2之类的容器,和c3,c4容器,他们各自连接到s1和s2上。这种可能性各位应该很容易理解,各位想过c1和c2在s1软交换机上通信容易理解。那么c1和c3通信怎么办。那我们是不是要把两个交换机连接起来?那交换机怎么连呀。说起来容易,这可是软件。这也不是插根网线就完了。      刚才说过网卡是成对儿出现的,那么再做一个网卡,一半在s1交换机上,另外一头在s2交换机上,就相当于用网线把它连起来了。这还是很简单的方式,对吧。但如果我期望c1和c3通信要经过路由转发,因为他们在不同的网段中你说怎么办?那于是我们需要在中间加路由器,那路由怎么实现?我们知道linux内核,自己是不是就支持当路由器来使用,如何用呢?iptables规则,或者打开可以转发就行,其实。这也是一个很简单的方式。但是路由器怎么实现的呢?路由器是个三层设备在linux内核级直接使用一个单独的名称空间就能支持。注意只要单独的名称空间,刚才说的c1,c2,c3,c4不都是容器吗。再做一个容器,这个容器什么事也不干,就当路由来使用,里面运行的就是一个内核,你可以这么去想象他,那这真实的内核来实现报文转发的,仅此而已。可以实现这种功能。但是你待模拟出网卡来,让他们之间建立关联关系。这是一种情景。      那再拓展一下,假如c1和c5需要通讯怎么办,注意c5是另外一个服务器上的容器,,你可以想象成另外一个虚拟机,这该怎么办?我们在使用vmware workstation的时候,跨物理机做虚拟机,是怎么通讯的?桥接转发是吧,没错c1桥接,c5也桥接是吧,怎么桥啊?什么叫桥接?所谓物理桥接是指把物理网卡当交换机来用,所有发给c1,c2,c3,c4都先到一个物理网卡。物理网卡根据目标MAC来判定他应该是交给c1,c2还是c3,c4。局域网通信不就是靠MAC地址来识别的吗。因此也就意味着c1,c2,c3,c4都有专用的,独有的MAC地址,而且跟物理网卡的MAC地址一定不能一样。所以如果说目标MAC是物理网卡MAC的,这是是送给物理机的,如果是c1MAC才是送给c1的。      这是局域网调节通信的时候。所以这样一来我把物理网卡当交换机来用。能接收,即便MAC不是自己的报文,从而转发给内部的c1,c2。而真正到达本机的再做一个虚拟网卡,这就是软网卡,这个软网卡专门负责为物理机接收报文。物理网卡就拿来,被征用为交换机了,这样就没有s 1的概念了。因为c1直接连接到这个物理交换机上,也就是物理网卡模拟的交换机上来的,就类似这种效果。那很显然,c5也不应该连到s1上,应该连到,他自己的物理网卡上来。把物理网卡当交换机来用,所以c1和c5他俩之间就可以直接通信了。但这种通信方式代价很大。为什么呢?首先,你的所有容器假如都是桥接的,大家是不是都在同一个平面网络中,很容易产生风暴。因此,在隔离上也是极为不容易的,再隔离和管理上。因此在大规模的虚拟机或者容器的场景中使用桥接这种方式无疑就是自找死路。除非你使用大二层的技术,能把它们隔离开来。否则我们都不应该直接桥接的。      那如果不桥接,又能够与外部通信,那我们用的是什么技术?Nat技术,那Nat网络是怎么实现的?还是刚才那种方式,如果说c3想与c6通信,怎么办?有一个办法,c3是虚拟网卡没问题,c3这里的交换机,也的确是一个软交换机。c3的网络地址跟你的物理网卡地址不在同一网段,那么c把网关指向s2,把s2直接当宿主机的一个网卡来使用。可以给他配了一个IP地址,跟c3在同一网段,我们把C3的网关指向S2,然后在物理机上打开核心转发功能。所以当C3给C6通信时,先送给S2,到达物理内核,物理机判定目标地址不是自己,查路由表要到达C6,要经由物理网卡,报文送出去。但是报文回不来,因为C4或者C3是一个私有地址。我们现在要他能回来怎么办?最好在c3的报文送走离开物理机h1之前要把那个原IP改成H1主机的物理网卡IP地址,这样子c6或者c5回复的时候到h1的物理地址就行了,h1的物理地址通过nat表的查找知道这是到C3的访问,应该给C3。所以我们说这就是一种叫nat网络,也能保证你的c3能跨主机于c6这样的主机进行通信,那这里有一个很大的问题,在什么地方呢?有没有发现h2这儿的通信也其实需要经由nat来实现。两级nat,首先c6难道不是私有地址吗,假如C6也是nat服务的,他也是私有地址。那么C6怎么可能被C3看见呢?不可能因为他隐藏在nat背后。我们知道nat背后的服务器,如果想被访问怎么办?把他暴露出去是吧?把它发布出去怎么发布? DNAT。我们说在物理机的这个对外的物理网卡上,明确说明,我们某一个端口,是提供服务的,但他其实没有,被映射给谁c6了,那么如果c4是想访问C6。那么C6其实C4是看不见的,因为他在nat背后,不然的话,nat的隐藏意义还有何在。所以这个时候我们只能告诉他,要想访问这个服务,要先访问h2的物理地址,就是物理网卡的IP地址。所以C4直接访问h2的物理地址的,而且h2做DNAT发给c6。但是C4出来的时候是怎么出来的?SNAT出来的,刚刚说了c4发的报文出去,请求能出去,但别人响应,响应不回来,因为C4也是隐藏在nat背后的。所以你会发现,如果要跨物理主机,让两个虚拟机之间通信,你要经由两级nat。那首先c4经由h1出去的时候要SNAT,然后要到达H2的时候要DNAT发给内部的c6。那这效率能高吗,可以想得明白对不对,他要做两次转换,而且更悲剧的是。c4以为访问的服务器,不是真正的服务器。 C4和C6之间他俩,是隔着两层来对话的。比如说别人给你介绍对象,应该有一个红娘,还要等两个红娘他们之间聊一聊,说我这有个朋友急得找对象。,那边说我这也有个朋友急着找对象, OK,咱俩就凑合凑合吧。所以他俩就在中间不停的传话。你给你的红娘说一声,她要先传给对方的那红娘。对方的红娘在传给真正的目标人。大概类似于这种状态。这种方式好玩吗?你想。所以这种方式还是那句话,效率非常的低。而且他们俩真正的通信,你看不到真正的对方。C4看不见C6,C6看不见C4。C4以为是跟h2通信。 C6以为跟H1通信。因为C6的响应,响应是H1绝对不是C4。因为它的地址源已经改过了,改成h1的物理地址了或者物理网卡的地址了。这种方法好处在于网络易管理。很悲剧的是效率很低,那要怎么实现,能让他效率高一点。但又不至于桥接,纯桥接的方式,大家都顶着一颗脑袋,直接到江湖上去混了,这是一种方式,而要使用nat的方式呢,大家都当缩头乌龟,都缩在一个个nat背后,来互相通信效率又很低。那有没有一种方式折中一下,不至于顶着脑袋直接出去,大家挤在一块。又不至于两级nat以后通信效率非常的低。这个网络解决方案比较常见,比较简单的,我们叫叠加网络Overlay Network。叠加网络是怎么叠加的呢?简单来讲,他是这样子。依然是可能不止一个物理机,有h1,h2,h3,上面可能都要有虚拟机,你的物理机的物理网卡。同时在虚拟机上我们做一个虚拟的桥,让各容器或者虚拟机连到这个虚拟的桥上来,最后他们通信时借助与我们的物理网络来完成报文的隧道转发,从而可以实现h1上的C1容器,可以直接看到h2上的C6或者C5容器。实现方式就是,物理主机本来使用物理网络,是不是就连接在一起的,他们是可以直接通信的,而c1通信时,c1使用的地址,跟你的物理网络不在同一个地址段内,但是他跟c5在同一地址段内,比如c1是192.168.1.1,c5地址是192.168.1.5。那么C1发送给192.168.1.5的报文,很显然不在h1本机之上,是在另外h2物理服务器之上的。c1发送时先送给h1上的虚拟桥, 这个桥他会看到,假设啊,他会知道c5并不在本地物理服务器上,于是这个报文会,使得要从一个物理网卡发出去,但是发出去之前是这么发的,他要做隧道转发。就是c1的这个ip报文,源地址是c1,目标地址是c5,在h1主机上报文不动,他会在额外再封装一个ip首部,源地址是h1的物理网卡地址,目标地址是c5所在的h2物理网卡地址。所以这个报文送给h2主机,h2主机拆完第一层ip封装,就看到第二层ip封装了,第二层ip封装,目标地址是c5的,所以直接交给h2本地的软交换机,有软网桥交给c5。我们看c1和c5里面通信时,直接源地址是c1,目标地址是c5。但他基于别的网络,本来自己已经是一个三层报文,改接下来封装2层了,但是他没有封装2层,而且又重新封装三层四层报文,就是一个udp或者一个tcp的首部,在封装一个ip首部,然后实现2级三层封装。而完成了报文转发。那这种用一个ip来承载另外一个ip我们说叫隧道。这样他们可以基于这种方式让c1和c5直接在同一个网段内可直接放行,所以c1我们再去请求服务时,假如这个服务在c5上,也可以直接向c5的地址发请求,就不用中间2级nat以后大家都隐藏在某一nat背后,都看得见真正对方的地址。这种网络呢,就叫叠加网络,而且基于隧道叠加实现的另外一层网络途径。      

  那接下来我们就可以说一说docker的网络了。docker的网络默认使用的叫bridge,docker安装完以后自动提供了,三种网络,使用docker network ls可以看到,一个叫bridge,一个叫host,一个叫none。其中bridge就表示桥接式网络,并不是物理桥,表示nat桥,意思是说他把本机之上自动创建一个纯粹的软交换机叫docker0注意这是一个交换机,他可以当网卡使用,既可以扮演二层的交换设备,同时可以扮演网络层设备,如果你不给他地址,他就是交换机,给他地址它既能当交换机,又能当网卡,注意docker0这里地址是172.17.0.1,随后我们启动每一个容器,就可以给这个容器自动分配一对儿网卡的设备,其中一半在容器上,一半在docker0桥上,因为他是交换机。相当于一根网线连到到两只设备一样,从而使得就好像c1连到了这个交换机上。

查看docker网络设置

# 查看docker自动提供的三种网络
[root@localhost ~]#  docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
d1332d5a537f        bridge              bridge              local
fe3a676e468a        host                host                local
afb57c48a2ef        none                null                local

# 查看docker的bridge创建的软交换机叫docker0,有一个叫vethecf4e47的设备名字是随机的,这其实就是一对儿网卡当中的一半(容器要处于启动状态才能看到),另外一半在容器上,
[root@localhost ~]# ip a
[root@localhost ~]# ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:4aff:fe3f:4266  prefixlen 64  scopeid 0x20<link>
        ether 02:42:4a:3f:42:66  txqueuelen 0  (Ethernet)
        RX packets 23  bytes 2355 (2.2 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 30  bytes 2190 (2.1 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.162.138  netmask 255.255.255.240  broadcast 192.168.162.143
        inet6 fe80::7562:2afd:4ea3:7738  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:00:01:03  txqueuelen 1000  (Ethernet)
        RX packets 6003575  bytes 678525486 (647.0 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 7645244  bytes 1189338733 (1.1 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1  (Local Loopback)
        RX packets 76  bytes 6624 (6.4 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 76  bytes 6624 (6.4 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

vethecf4e47: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::4440:c9ff:fed2:45c5  prefixlen 64  scopeid 0x20<link>
        ether 46:40:c9:d2:45:c5  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 7  bytes 578 (578.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# 查看容器(这里有一个容器,我们要支撑容器的运行,他们都是用默认网络,都使用bridge,所以我们为每一个容器都要创建一对儿网卡,一半在容器里面,一半在宿主机上vethecf4e47,并且他们还被关联到,被插在了docker0上,有人说了这是怎么插上去的,我们使用brctl命令就能看到)
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
ece5044399ad        busybox             "sh"                38 hours ago        Up 5 minutes                            b1

# 安装bridge-utils工具(以太网桥管理工具)
[root@localhost ~]#  yum -y install bridge-utils

# 查看以太网桥 (可以看到在docker0上,关联了一个接口(如果有多个可以看到关联的有多个)。关联的接口和我们看到的网卡名字一致,可以使用ip link show查看)
[root@localhost ~]# brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02424a3f4266       no              vethecf4e47

##         show            [ <bridge> ]            show a list of bridges(显示桥梁列表,查看当前是否有桥设备)

# 查看网卡(可以看到这里的vethecf4e47网卡名于docker0关联的接口名一致,而且可以看到@符号后面if28还有另外一半,而这一半其实并没有显示出来,在某一个容器中,这也是为什么在容器中使用ifconfig可以看到一个网卡的原因)
[root@localhost ~]# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
    link/ether 00:15:5d:00:01:03 brd ff:ff:ff:ff:ff:ff
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT 
    link/ether 02:42:4a:3f:42:66 brd ff:ff:ff:ff:ff:ff
29: vethecf4e47@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT 
    link/ether 46:40:c9:d2:45:c5 brd ff:ff:ff:ff:ff:ff link-netnsid 0

# 查看容器内的网络设备(这个eth0其实就是一个软网卡,其一半在容器内表现为eth0的,另外一半就关联到docker0上去了,所以着就当做一根网线来使用了。因此这个容器ping172.17.0.1就是那个docker0那个地址可以ping通,docker0桥其实是一个nat桥,你每创建一个容器,并启动以后,他自动分配地址以后会给你生成一个iptables规则,在物理主机上看。)
[root@localhost ~]# docker exec -it  b1 /bin/sh
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02  
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:8 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:648 (648.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # ping 172.17.0.1
PING 172.17.0.1 (172.17.0.1): 56 data bytes
64 bytes from 172.17.0.1: seq=0 ttl=64 time=0.081 ms
64 bytes from 172.17.0.1: seq=1 ttl=64 time=0.082 ms
64 bytes from 172.17.0.1: seq=2 ttl=64 time=0.085 ms

# 查看iptables规则(POSTROUTING可以看到in进来的是*表示从任何接口进来,out也是*表示只要不从!docker0出去。source源地址为172.17.0.0表示源地址来自于172.17.0.0网络的任何主机地址,destination目的地为0.0.0.0表示无论到达任何主机。 target为MASQUERADE叫地址委托,就相当于SNAT而且是自动SNAT,所谓自动snat就表示自动选择一个最合适的源地址,在本机上的物理地址自动选择一个源地址,所以说docker0桥默认是nat桥,就是这个原因)
[root@localhost ~]# iptables -t nat -vnL
Chain PREROUTING (policy ACCEPT 6478 packets, 949K bytes)
 pkts bytes target     prot opt in     out     source               destination         
   10  1658 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 200 packets, 38985 bytes)
 pkts bytes target     prot opt in     out     source               destination         

Chain OUTPUT (policy ACCEPT 1313 packets, 181K bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 DOCKER     all  --  *      *       0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 1313 packets, 181K bytes)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0           

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    1    84 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0   
  那假设就按这个逻辑推理下去,问几个问题,能不能想清楚。第一个问题。像刚才我们曾经创建了一个叫web1的基于nginx启动的一个容器,这个容器默认我们没有选择网络,他应该也是bridge网络,应该也是nat方式的,我们这里有一个docker0网卡,也是docker0桥,在物理机上,他有个172.17.0.1的地址,随后我们创建一个容器,叫web1,web1内部有一个网卡,我们说这个网卡一半在容器上,而另外一半在宿主机上,事实上在宿主机上,他被关联至了docker0桥上,相当于在另外一头上插上交换机上而已。      那如果这个nginx,大家知道nginx应该是有服务,对不对,如果这个服务想被别的客户端所访问。那你看客户端有几种来源,首先假如就是同一个主机上的另外一个容器,另外一个容器也使用了桥网络,那应该是不是理解为他也连到了同一个桥上了,那他应该是不是也在同一网段的同一个地址,那这两个容器之间既然如此,是不是可以直接通信,没有任何问题,这是第一种客户端,用一个容器当服务器,一个容器当客户端,比如我们之前运行的几个容器当中有一个web1的容器,查看网络是172.17.0.3,因此在我们当前这个busybox这个容器地址是172.17.0.2,在busybox这个主机上直接访问172.17.0.3应该没有任何问题,因为他俩都在同一个交换机上连着呢,而且网段也属于同一个,所以去ping172.17.0.2直接是通的,访问172.17.0.2的web服务直接访问它,没有curl命令,就是用wget-O - -q 去访问http://172.17.0.2,所以wget也可以像curl一样来使用,来验证,访问成功,这在容易个主机上很容易理解,那么请问,如果通过物理机来访问他,也可以,因为物理机把这个软交换机当网卡用了,而且还有172.17网段的ip地址,172.17.0.1本来就可以直接跟,容器直接通信,他俩都在同一网段,所以使用物理机访问也没问题,这是第二个客户端,就是物理机本身。所以事实上就相当于大家是同一网络中的主机,这个时候我们也可以想象到他实际上是一个所谓的仅主机桥。但是如果考虑到有iptables规则以后,他就不仅仅是仅主机了,而是一个nat。            但是,试想一下,假如有一个外部的服务器,你不管是物理机还是虚拟机,假如有一个脱离了这个主机的。比如node2想访问nginx怎么办,他能看到nginx这个容器吗,看不到,因为说了他是一个nat桥背后的主机,他是被nat隐藏在背后的,因此你的nginx除了能够被物理机或者同一主机上的其他容器访问之外,都访问不到。要想能访问到,要发布,只能由一个办法做DNAT,假如这个物理机有个物理网卡,在物理网卡上有一端口,提供有web服务。在用户客户端访问这个网卡ip地址上的某一特定端口时,把他使用DNAT的方式转发至,这个软交换机上来,进而到达我们的nginx容器,我们给他起名叫web1,只能这么干,没有其他更好的办法,对于外部的客户端访问来讲,      那也就是说我们随后在任何一个docker主机上创建了容器,当我们使用所谓默认的桥接式网络时,只能给他再额外添加一个DNAT规则,以便于他可以被外部的其他客户端所访问。你试想一下,如果我这里基于nginx镜像启动两个容器,一个web1,一个web2,他们都是web服务,正常情况下,假如你对外通信的物理机,只有一个网卡,而且只有一个IP地址,我们怎么做DNAT呢。外部的客户端访问时如果到达物理网卡这个ip地址的80端口,就转给web1对不对,那web2怎么办。80端口只有一个物理机的,你也可以使用非80,但客户端怎么知道访问哪个端口,因为他们不是众所周知的端口,你说你的8080,我怎么知道你是8080,更何况你可能还有web3呢。所以这的确就面临这样的问题的复杂度。没办法,这也是这种网络模型当中我们必然要面临的没问题,因为我们只能通过映射的方式被访问。      但是考虑回来,假如我们使用的是叠加网络刚才描述的,这种叠加c1和c5那我们就不需要做DNAT,它本身就是通过隧道承载的。所以C1直接访问C5的80端口就行了,他们两个互相访问对方的80端口就可以了因此我们不需要在物理机的基础上做映射,这也是叠加网络能够带给我们的第二个好处,优势。默认nat网络是不具有的,这种方式比较悲剧。      再看假如我们的web1要想能够被外部主机访问,还有没有别的办法?可以桥接对吧,那除了桥接以外还有没有别的办法。好像也没有别的办法了是吧,      但是容器有一个功能,刚刚讲过每一个容器无非就是一个容器内部拥有独立而隔离的个名称空间User,Monut,Pid下面三个比较关键,UTS,Net,和IPC。每一个容器都有自己独立的这么一组资源。我们可不可以这样,让每一个容器,只拥有User,Monut,Pid这是隔离而独立的,而这两个容器,底下UTS,Net,和IPC这三个资源是共享的使用同一组,这样带来什么效果?也就是说这两个容器每一个人能看到自己的用户账号,大家互不干扰,能有自己的文件系统,大家互不干扰,能有自己的pid,大家互不干扰。但是他们使用同一组网络,意思就是看见的是同一个网络设备。就是同一组网卡什么之类的。拥有同一个网络协议栈,拥有同一个主机名和域名。对外大家使用同一个主机名和域名,对外大家同时使用同一个地址。好处在于,假如第一个容器运行的是tomcat,假如第二个容器运行的是redis。tomcat要访问redis中的数据,大家通过io就可以通信了,因为他们是同一个协议栈,同一个协议栈是不是用一个lo接口啊,所以使用127.0.0.1访问redis也可以。那这样此前如果是彼此隔离的,能不能通过io来访问,左边的容器有自己的lo接口,右边有他的io,这两个lo接口不一样,所以想通过127.0.0.1访问是不可能的。      后面的模型表示有自己隔离的名称空间,却也可以共享一个空间。但我们一般指共享网络通信相关的,像UTS主机名一般是跟通信有关,IPC进程间通信有关,Net也是,网络协议栈,网络协议什么之类的,这是可以共享的。这个玩法是不是精巧多了。      这样我们要说另外一个概念。物理机也有网络名称空间,只要使用了名称空间,那么物理机自己无非有了一个特权名称框架而已,他看的都是物理网卡设备。      那么我们能不能让一个容器直接使用物理机的名称空间。刚刚说了容器和容器之间可以共享网络名称空间,难道容器就不能共享物理机那个名称空间吗,必需可以。意思是说。假如这里跑了两个容器,我可以让第一个容器直接使用物理机的网络名称空间。也就意味着在这容器内改了网卡的所有信息是直接改物理机的,但第二个容器可以使用桥接的,第一个容器就拥有了管理物理网络的特权,而这种,我们就称之为叫网络类型中的第二种host,使用docker network ls可以看到,所谓host就叫主机,也叫宿主机,意思是就表示让容器使用宿主机的,网络名称空间,那none又是什么?可以看到null,表示空。      所以如果我们要设定一个容器使用none网络意味着没有网络,不给他使用网络,他只有lo接口,没有任何网卡,相当于买了一个主机,里面没有网卡,那没网卡意味什么?不能执行网络通信。那他就是个孤岛,一个信息孤岛。那很奇怪,我们要这种容器有用吗?有用,有些容器可能确实不需要网络通信的。你比如说我要启动一个容器,他要运行一个复杂程序,把数据处理一下,把数据格式转换一下,数据通过存储卷,一加载处理完了退出就完了,那么像这种批处理模式的确不需要网络通信的。他都是一些后台的,计算或处理任务。那像这种我们就不用给他网络,没有问题。运行结束,容器删除就ok,加工的数据还放在外部的存储卷上。      可以总结一下了,docker一共有4中网络模型,有三种已经详细介绍了,第四种算是第三种联盟式的共享网络通信名称空间的模式的延申,第三种式容器间共享,第四种是直接共享宿主机的网络名称空间就这意思。      如果我们在创建容器时没有指定,他们通通都是默认为第二种网络叫桥接式网络,而且这个桥式nat桥,而不是物理桥,万万要记得,有了这些概念,我们可以轻松去设定docker容器启动时能够使用什么网络了。而方法就是在创建docker容器时使用一个专门的选项docker container run  -- network,后面跟一个字串就是指明要用哪个网络的,而如果不指,他叫做default默认,可以使用docker network ls看到bridge,他式default默认的网络,这个bridge相当于docker0桥,可以使用docker network inspect 专门看网络相关内容的,这里看叫做bridge网络,可以看到bridge网络关联的接口叫docker0
# 查看容器
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
ece5044399ad        busybox             "sh"                     38 hours ago        Up 57 minutes                           b1
85d1f9bd71f2        nginx:1.14-alpine   "nginx -g 'daemon of…"   4 days ago          Up 39 seconds       80/tcp              web1


# 查看web1容器地址,并查看web服务监听的接口(地址是172.17.0.3。可以看到监听的端口是80端口)
[root@localhost ~]# docker inspect web1
                    "IPAddress": "172.17.0.3",

[root@localhost ~]# docker exec -it web1 /bin/sh
/ # netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     


# 查看busybox容器地址,ping测试web1容器,并访问web1容器服务(wget选择-O表示把内容加载完以后不要保存在文件中,直接显示,- 表示输出到当前桌面上来, -q:不显示指令执行过程;)
[root@localhost ~]# docker inspect b1
                    "IPAddress": "172.17.0.2",

[root@localhost ~]# docker exec -it  b1 /bin/sh
/ # ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.103 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.053 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.091 ms

/ # wget -O - -q http://172.17.0.3
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

# 使用物理机访问web1容器服务
[root@localhost ~]# curl http://172.17.0.3
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

# 查看bridge相关内容()
[root@localhost ~]# docker network inspect bridge
            "com.docker.network.bridge.name": "docker0",      (bridge关联的接口叫docker0)
                    "Subnet": "172.17.0.0/16",      (在这个地址范围内,网段内来分配地址,每创建一个容器,就自动分配一个这个网段内的地址)
                    "Gateway": "172.17.0.1"      (网关是172.17.0.1,已经被docker0使用)

# 查看容器的相关信息
[root@localhost ~]# docker container inspect web1
            "Networks": {      (对于web1这个容器来讲)
                "bridge": {      (他在bridge网络)
                    "Gateway": "172.17.0.1",      (在bridge网络中他的网关是172.17.0.1)
                    "IPAddress": "172.17.0.3",      (web1自己的地址是172.17.0.3)
                    "IPPrefixLen": 16,      (这是掩码长度)
                    "MacAddress": "02:42:ac:11:00:03",      (当前虚拟网卡的MAC地址)

手动操作名称空间

   隔离式网络,桥接式网络(物理桥式网络),nat式网络,除此之外具有的特殊网络模型。事实上呢我们完全可以手动去操作我们名称空间的。因为我们有ip命令,在一个主机上,没有装任何docker,但我们需要他会用到ip命令。      ip命令的众多可操作对象当中包含netns就叫做网络名称空间,也就意味着说,不像其他的几个名称空间,像什么UTS,User什么之类的。如果想使用网络名称空间,简单来模拟容器间通信的话,只靠ip命令就能操作完,比如我们要想能够添加网络名称空间就使用ip netns的相关子命令,      当我们使用ip去管理网络名称空间时,只有网络名称空间是被隔离的,其他名称空间都是共享的。也就意味着大家都在同一个除了网络名称空间之外的其他的名称空间中都是那么一个。这个我们讲的容器6个都是隔离的,略有不同。。

查看iproute包(已经安装过了,这是ip命令所属的程序包)

[root@localhost ~]# rpm -q iproute
iproute-3.10.0-87.el7.x86_64

# ip命令帮助
[root@localhost ~]# ip
Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }
       ip [ -force ] -batch filename
where  OBJECT := { link | address | addrlabel | route | rule | neigh | ntable |
                   tunnel | tuntap | maddress | mroute | mrule | monitor | xfrm |
                   netns | l2tp | macsec | tcp_metrics | token }
       OPTIONS := { -V[ersion] | -s[tatistics] | -d[etails] | -r[esolve] |
                    -h[uman-readable] | -iec |
                    -f[amily] { inet | inet6 | ipx | dnet | bridge | link } |
                    -4 | -6 | -I | -D | -B | -0 |
                    -l[oops] { maximum-addr-flush-attempts } |
                    -o[neline] | -t[imestamp] | -ts[hort] | -b[atch] [filename] |
                    -rc[vbuf] [size] | -n[etns] name | -a[ll] }

# 查看ip netns的相关子命令帮助
[root@localhost ~]# ip netns help
Usage: ip netns list      (列出网络名称空间)
       ip netns add NAME      ( 添加网络名称空间)
       ip netns set NAME NETNSID      (设置网络名称空间的sid,另外甚至可以把一个进程,或者把一个设备挪到名称空间中去,使用ip link 这个命令)
       ip [-all] netns delete [NAME]
       ip netns identify [PID]
       ip netns pids NAME
       ip [-all] netns exec [NAME] cmd ...      (而且还可以在一个网络名称空间中去执行命令)
       ip netns monitor
       ip netns list-id

添加网络名称空间

  网络名称空间,名字是谁便取的
[root@localhost ~]# ip netns add r1
[root@localhost ~]# ip netns add r1

列出网络名称空间

[root@localhost ~]# ip netns list
r2
r1

查看网络名称空间中的网卡

  r1,r2内应该可以看他的网卡,如果我们没有给他单独指令创建的话,那他就只有一个
# exec表示在名称空间中执行命令,在r1和r2名称空间中执行ip a(或ifconfig -a),他们都只有一个lo没有给他们任何网络接口设备
[root@localhost ~]# ip netns exec r1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

[root@localhost ~]# ip netns exec r2 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

创建虚拟网卡对儿

  我们也可以使用ip命令直接创建虚拟网卡对儿,然后把一个人工给他分配到这个网络名称空间中来。使用ip link命令
# 创建一对儿网卡(add的时候,使用name指明网卡设备的名称,比如叫veth1.1就表示第一个设备的第一段,也可以叫1.x什么之类的,后面指明type类型为veth,指明peer name就是另外一半的name,假如叫veth1.2)
[root@localhost ~]#  ip link add  veth1.1 type veth peer name veth1.2


# 查看帮助
[root@localhost ~]# ip link help
Usage: ip link add [link DEV] [ name ] NAME      (对于link来讲可以直接使用add添加设备,NAME指定名称就行了)
                   [ txqueuelen PACKETS ]
                   [ address LLADDR ]
                   [ broadcast LLADDR ]
                   [ mtu MTU ]
                   [ numtxqueues QUEUE_COUNT ]
                   [ numrxqueues QUEUE_COUNT ]
                   type TYPE [ ARGS ]
       ip link delete { DEVICE | dev DEVICE | group DEVGROUP } type TYPE [ ARGS ]      (而后,指定名称以后还可以指定TYPE,是什么类型的,底下有说明)

       ip link set { DEVICE | dev DEVICE | group DEVGROUP }      (可以set 设定,dev DEVICE一个设备,他属于一个特定的名称空间,netns)
                          [ { up | down } ]
                          [ type TYPE ARGS ]
                          [ arp { on | off } ]
                          [ dynamic { on | off } ]
                          [ multicast { on | off } ]
                          [ allmulticast { on | off } ]
                          [ promisc { on | off } ]
                          [ trailers { on | off } ]
                          [ txqueuelen PACKETS ]
                          [ name NEWNAME ]
                          [ address LLADDR ]
                          [ broadcast LLADDR ]
                          [ mtu MTU ]
                          [ netns { PID | NAME } ]      (这表示可以把一个网络设备给他挪到一个名称空间中去)
                          [ link-netnsid ID ]
                          [ alias NAME ]
                          [ vf NUM [ mac LLADDR ]
                                   [ vlan VLANID [ qos VLAN-QOS ] ]
                                   [ rate TXRATE ]
                                   [ max_tx_rate TXRATE ]
                                   [ min_tx_rate TXRATE ]
                                   [ spoofchk { on | off} ]
                                   [ query_rss { on | off} ]
                                   [ state { auto | enable | disable} ] ]
                                   [ trust { on | off} ] ]
                          [ master DEVICE ]
                          [ nomaster ]
                          [ addrgenmode { eui64 | none } ]
                          [ protodown { on | off } ]
       ip link show [ DEVICE | group GROUP ] [up] [master DEV] [type TYPE]
       ip link help [ TYPE ]

TYPE := { vlan | veth | vcan | dummy | ifb | macvlan | macvtap |      (支持的类型,vlan(虚拟局域网),veth(虚拟以太网网卡,所以要添加一对儿虚拟网卡的话,应该使用veth) )
          bridge | bond | ipoib | ip6tnl | ipip | sit | vxlan |
          gre | gretap | ip6gre | ip6gretap | vti | nlmon |
          bond_slave | geneve | bridge_slave | macsec }

查看创建的一对儿网卡

  ip add list 也能查看,可以看到创建了两对儿网卡,他是成对儿出现的,veth1.2和veth1.1是一对儿,veth1.2的另外一半是veth1.1,veth1.1的另外一半是veth1.2。      有没有发现这两头都在我们属主机上,这是正常情况,我们现在有两个网卡了,而且他们默认都没有被激活,使用ifconfig是看不见的,现在可以人工的把其中一个,比如说把veth1.1留在属主机上, 把veth1.2挪到刚才我所创建的名称空间r1中去。方法也很简单,一样使用ip link命令
[root@localhost ~]# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
    link/ether 00:15:5d:00:01:03 brd ff:ff:ff:ff:ff:ff
3: veth1.2@veth1.1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether 26:b5:f4:d6:1d:da brd ff:ff:ff:ff:ff:ff
4: veth1.1@veth1.2: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether a6:47:fa:cd:94:01 brd ff:ff:ff:ff:ff:ff

将网络设备给他挪到一个名称空间中去

  一个网卡怎么分配名称空间的,我们手动使用这种命令,调系统内核级的系统调用,完成一个操作,而且我们甚至还可以做更多的操作。比如给网卡设备改改名,
# 把veth1.2挪到r1名称空间中,把veth1.1留着
[root@localhost ~]# ip link set dev veth1.2 netns r1


# 查看(这里只有veth1.1了,一个设备只能属于一个名称空间,我们在到r1中去看,在r1名称空间执行命令,可以看到有一个veth1.2)
[root@localhost ~]# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
    link/ether 00:15:5d:00:01:03 brd ff:ff:ff:ff:ff:ff
4: veth1.1@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000
    link/ether a6:47:fa:cd:94:01 brd ff:ff:ff:ff:ff:ff link-netnsid 0

[root@localhost ~]# ip netns exec r1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: veth1.2@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether 26:b5:f4:d6:1d:da brd ff:ff:ff:ff:ff:ff link-netnsid 0

将名称空间中的网卡设备改名

[root@localhost ~]# ip netns exec r1 ip link set dev veth1.2 name eth0
[root@localhost ~]# ip netns exec r1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: eth0@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether 26:b5:f4:d6:1d:da brd ff:ff:ff:ff:ff:ff link-netnsid 0

激活网卡

  改完名以后,给他ip地址激活就可以了,然后跟宿主机那一半就能通信了。假如他在同一网段,举个例子,宿主机上把veth1.1这一半激活。      在把r1名称空间中已经改名为eth0的另外一半也给他激活,也是一个网段的地址,应该就可以直接通信了
# 宿主机网卡添加IP地址,并激活
## 添加ip
[root@localhost ~]# ip address add 10.1.0.1/24 dev veth1.1
[root@localhost ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:15:5d:00:01:03 brd ff:ff:ff:ff:ff:ff
    inet 192.168.162.142/28 brd 192.168.162.143 scope global dynamic eth0
       valid_lft 86302sec preferred_lft 86302sec
    inet6 fe80::7562:2afd:4ea3:7738/64 scope link 
       valid_lft forever preferred_lft forever
4: veth1.1@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000      (<BROADCAST,MULTICAST>状态中没有up网卡未激活,但是有ip地址)
    link/ether a6:47:fa:cd:94:01 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.1.0.1/24 scope global veth1.1
       valid_lft forever preferred_lft forever

## 激活网卡
[root@localhost ~]# ip link set veth1.1 up
[root@localhost ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:15:5d:00:01:03 brd ff:ff:ff:ff:ff:ff
    inet 192.168.162.142/28 brd 192.168.162.143 scope global dynamic eth0
       valid_lft 86250sec preferred_lft 86250sec
    inet6 fe80::7562:2afd:4ea3:7738/64 scope link 
       valid_lft forever preferred_lft forever
4: veth1.1@if3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN qlen 1000      (<NO-CARRIER,BROADCAST,MULTICAST,UP>状态已经是up状态)
    link/ether a6:47:fa:cd:94:01 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.1.0.1/24 scope global veth1.1
       valid_lft forever preferred_lft forever

# 名称空间网卡添加IP地址,并激活
## 
[root@localhost ~]# ip netns exec r1 ip address add 10.1.0.2/24 dev eth0
[root@localhost ~]# ip netns exec r1 ip link set eth0 up
[root@localhost ~]# ip netns exec r1 ip address
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: eth0@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000      (已经激活状态为UP)
    link/ether 26:b5:f4:d6:1d:da brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.1.0.2/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::24b5:f4ff:fed6:1dda/64 scope link 
       valid_lft forever preferred_lft forever


## 或
ifconfig veth1.1 10.1.0.1/24 up


# 测试通信,直接使用宿主机物理名称空间ping,名称空间r1中的地址,就好像两个虚拟机一样
[root@localhost ~]# ping 10.1.0.2
PING 10.1.0.2 (10.1.0.2) 56(84) bytes of data.
64 bytes from 10.1.0.2: icmp_seq=1 ttl=64 time=0.096 ms
64 bytes from 10.1.0.2: icmp_seq=2 ttl=64 time=0.044 ms
64 bytes from 10.1.0.2: icmp_seq=3 ttl=64 time=0.066 ms

挪动网卡设备

  可以把宿主机上的这一半,送给r2名称空间,这样就好像r1和r2直接通信了
# 当前主机上已经没有veth1.1网卡设备,此刻依然在r2名称空间中了,但是此网络设备在r2中是没有激活的,需要激活一下
[root@localhost ~]# ip link set dev veth1.1 netns r2
[root@localhost ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:15:5d:00:01:03 brd ff:ff:ff:ff:ff:ff
    inet 192.168.162.142/28 brd 192.168.162.143 scope global dynamic eth0
       valid_lft 86333sec preferred_lft 86333sec
    inet6 fe80::7562:2afd:4ea3:7738/64 scope link 
       valid_lft forever preferred_lft forever

# 查看一下r2名称空间中的设备
[root@localhost ~]# ip netns exec r2 ip address
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: veth1.1@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000      (<BROADCAST,MULTICAST>没有UP状态未激活)
    link/ether a6:47:fa:cd:94:01 brd ff:ff:ff:ff:ff:ff link-netnsid 0

# 给r2名称空间中的设备添加ip地址,激活网卡
[root@localhost ~]# ip netns exec r2 ip address add 10.1.0.3/24 dev veth1.1
[root@localhost ~]# ip netns exec r2 ip link set veth1.1  up
[root@localhost ~]# ip netns exec r2 ip address
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: veth1.1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000      (<BROADCAST,MULTICAST,UP,LOWER_UP> 依然是UP激活状态)
    link/ether a6:47:fa:cd:94:01 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.1.0.3/24 scope global veth1.1
       valid_lft forever preferred_lft forever
    inet6 fe80::a447:faff:fecd:9401/64 scope link 
       valid_lft forever preferred_lft forever

# 在r2名称空间中ping名称空间r1中的地址(r1和r2两个名称空间中的网卡依然可以进行通信,在这个基础上我们去模拟什么物理桥,nat桥,都易如反掌)
[root@localhost ~]# ip netns exec r2 ping 10.1.0.2
PING 10.1.0.2 (10.1.0.2) 56(84) bytes of data.
64 bytes from 10.1.0.2: icmp_seq=1 ttl=64 time=0.162 ms
64 bytes from 10.1.0.2: icmp_seq=2 ttl=64 time=0.058 ms
64 bytes from 10.1.0.2: icmp_seq=3 ttl=64 time=0.050 ms

模拟网络模型

实现第一种模式封闭时容器

  创建一个容器,这个容器创建一个封闭式容器,没有任何网络,只有lo接口,使用busybox演示
# 启动一个busybox容器,关闭后自动删除,进入后默认就是/bin/sh不用指,正常启动他是拥有eth0网络接口的,应该也属于172.17.0.0网段,这种容器,也可以显式来定义
## --rm 容器关闭后,直接删除
[root@localhost yum.repos.d]# docker run --name t1 -it --rm busybox:latest
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02  
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:13 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:1086 (1.0 KiB)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # exit
[root@localhost yum.repos.d]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

# 重新创建容器,创建时,添加额外的选项--network给他加入到网络中去,人工指定为bridge网络,效果跟不指是一样的
[root@localhost yum.repos.d]# docker run --name t1 -it --network bridge --rm busybox:latest
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02  
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:7 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:578 (578.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # exit


# 这回把--network bridge换成none启动容器,这里就是使用lo,这就是使用none网络,不给他创建网络设备
[root@localhost yum.repos.d]# docker run --name t1 -it --network none --rm busybox:latest
/ # ifconfig
lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

实现第二种模式桥接式容器

  桥接式容器是用来能对外通信的,虽然这个对外可能并不包含当前属主机之外的其他主机,但至少包含当前宿主机之上其他容器的,我们知道主机间通信很多时候会用到主机名,那当前主机应该是什么名字呢,使用hostname看一眼
# 查看当前容器主机名(40057498985d这些名字是容器id号)
[root@localhost yum.repos.d]# docker run --name t1 -it --network none --rm busybox:latest
/ # hostname
40057498985d

[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
40057498985d        busybox:latest      "sh"                About a minute ago   Up About a minute                       t1

# 如果我们想正常使用主机名,可以使用hostname直接设置,还可以在容器启动时给他分配主机名,使用-h 指定主机名,主机名不是容器id了,而是我们注入进来的主机名,假如说这个容器还期望通过主机名的方式访问其他容器。那无非两种方式,第一,这个容器能够通过主机名解析。通过DNS服务器来解析主机名,再不然我们通过自己本地的hosts文件。
[root@localhost yum.repos.d]# docker run --name t1 -it --network bridge -h t1.haoran.com  --rm busybox:latest
/ # hostname
t1.haoran.com

# 看一眼hosts文件,这里对自己的解析是没有问题,自动生成为hosts文件的条目,这是第一个
/ # cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2      t1.haoran.com t1

# 第二个在看一下/etc/resolv.conf,他所指向的DNS服务器,有没有发现他用的是你的宿主机可用的正常的外部的DNS服务器,那只要能跟这个地址通信,那应该是能解析主机名的
/ # cat /etc/resolv.conf 
# Generated by NetworkManager
search mshome.net
nameserver 192.168.162.129

# 使用nslookup测试是否可以解析主机名,-tpye指定A记录,解析成功了,因为他通过192.168.162.129,这个地址确实是一个名称服务器,他还能访问互联网
/ # nslookup  -type=A www.baidu.com
Server:         192.168.162.129
Address:        192.168.162.129:53

Non-authoritative answer:
www.baidu.com   canonical name = www.a.shifen.com
Name:   www.a.shifen.com
Address: 182.61.200.7
Name:   www.a.shifen.com
Address: 182.61.200.6

暴露端口

  Opening inbound communication(开放入站通信)

        Docker0为NAT桥,因此容器一般获得的是私有网络地址

        可以把容器想像为宿主机NAT服务背后的主机

        如果开放容器或其上的服务为外部网络访问,需要在宿主机上为其定义DNAT规则,例如

              对宿主机某IP地址的访问全部映射给某容器地址
                    主机IPDD容器IP
                          -A PREROUTING-d主机IP-j NAT--to-destination容器1P

              对宿主机某IP地址的某端口的访问映射给某容器地址的某端口
                    主机IP:PORTO容器IP:POIRT
                          -APREROUTING-d主机P-p tpludp --dport主机端口jDNAT-to-destntion容器IP容器端口

        为docker run命令使用-p选项即可实现端口映射,无须手动添加规则

  假如我们跑的这个容器是个nginx,nginx本来就是为了提的向外提供服务的。在默认情况下,它是以隐藏在我们docker0桥背后的,因为他对外的通信。nignx容器是这是一个私有地址,172.17是私有网络地址。比如说在node02上已经启用了一个容器172.17.0.2,在node03上去ping 172.17.0.2这是不可达的是吧,这里并没有一个路由信息可以到达172.17.0.2 除非你专门设定说要到达172.17.0.2,要经由172.16.0.67就是node2的物理地址。但这一条设置是不足以解决的问题的。因为我们将来的宿主机将来可能会有很多的,你要需要加多少这样的条目。所以这时候我们说过。应该把服务主动给他发布或称为叫暴露到,本来是隐藏的不让别人看见,那我现在需要把它暴露到对外通信的网络中去,就相当于使用DNAT做发布。不过我们这确实把它叫暴露,叫做,而且暴露的方式有4个,创建容器时,使用-p选项可实现暴露。假如说这个容器运行是一个nginx,默认监听在80端口,对吧,那容器说监听哪个就应该暴露哪个一般来讲,比如说监听在80,我80暴露出去。暴露出去,暴露在哪儿,他应该暴露在这个容器所在的物理机物理网卡的ip地址之上的某个端口上,对不对,那应该是哪个端口啊,80?no,他是一个动态端口,是一个随机端口,应该是3万到32767之间的随机端口。很显然,这一随机不当紧,别人不知道访问哪个了,那这种方式有个好处,假如你在一个宿主机上运行了多个nginx,每一个都暴露80,大家都使用随机的,这样就不会导致冲突。就他的好处。所以动态能解决这个问题。那我先不妨暴露一下试试。

        -p选项的使用格式(如果我们要暴露多个端口,比如说我们的httpd自己既监听了80又监听了8080,把这两个端口都要暴露给客户端使用。      那-p 选项可使用多次,暴露的时候,应该暴露你的容器的确监听了的端口,没监听你暴露是没有用的。      那如何实现开放nat桥,桥接式容器的服务到宿主机外部去,我们使用-p,      其实还有一个选项可以用叫杠大P -P。      小写p就表示你暴露哪个端口直接给出列表就行。而大写P表示暴露所有端口,所谓暴露所有端口是指,如果你在镜像中已经指定了要暴露哪个端口。基于镜像启动容器时,默认是不会被暴露的,除非你使用-大P选项。比如nginx镜像,一定默认就直接暴露了80端口的,但事实上nginx镜像自己启动为nginx服务时,在容器所在的宿主机外部依然无法被访问,因为他默认是不会暴露的。但这个时候我们加一个-大写P不用指哪个端口,他也能被暴露。dockerfile的制作和相关指令时会有相关的指令)
              -p <containerPort>
                    将指定的容器端口映射至主机所有地址的一个动态端口
              -p <hostPort>:<containerPort>
                    将容器端口<containerPort>映射至指定的主机端口<hostPort>
              -p <ip>::<containerPort>
                    将指定的容器端口<containerPort>映射至主机指定<ip>的动态端口
              -p <ip>:<hostPort>:<containerPort>
                    将指定的容器端口<containerPort>映射至主机指定<ip>的端口<hostPort>
              "动态端口”指随机端口,具体的映射结果可使用docker port命令查看

              docker run -p <host_port1>:<container_port1> -p <host_port2>:<container_port2>
                    暴露多个端口,指定多个-p选项
将指定的容器端口映射至主机所有地址的一个动态端口
# 启动一个nginx容器,可以向外提供服务,-p 把这个容器的端口80暴露出去,不加-d让他运行在前台(这个容器已经改了默认运行的程序了,现在容器应该跑起来,而且应该已经暴露出端口来了)
[root@localhost ~]# docker run --name myweb --rm -p 80  dockerhaoran/httpd:v0.2

# 看一下容器的信息(所使用的地址是172.17.0.3,)
[root@localhost ~]# docker inspect myweb
                    "IPAddress": "172.17.0.3",

# 访问一下服务(我们宿主机上访问172.17.0.3是很简单的,这只是内部通信,如果想在外部访问这个web服务怎么办,应该访问我们node02宿主机的地址,因为我们做了DNAT,但是我们应该访问宿主机地址哪个端口,我们说了暴露80,但是80被映射为哪个端口?可以使用iptables查看)
[root@localhost ~]# curl 172.17.0.3
<h1>Busybox httpd server.</h1>

# 查看暴露的端口(查看iptables生成的规则,这里是32768,意思就是你访问我们宿主机的32768这个端口的时候,宿主机物理地址被转发至172.17.0.3:80上。那于是可以愉快的,从外部访问了,      输入http://192.168.162.142:32768/      Busybox httpd server.     这其实就是靠tables上的这条nat规则,docker run -p选项会自动帮你生成这条规则,      如果把这个容器删除了,规则还会自动删除的。)
[root@localhost ~]# iptables -t nat -vnL
Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination         
    0     0 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0           
    0     0 DNAT       tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:32768 to:172.17.0.3:80

# 关掉myweb容器
[root@localhost ~]# docker kill myweb
myweb

# 再来查看iptables规则(没有这条规则了,自动生成,自动清除)
[root@localhost ~]# iptables -t nat -vnL

将容器端口映射至指定的主机端口
  那我想宿主机的所有可用地址指定80。假如你的宿主机的80没有是被使用。没问题,
# -p 地址不给了80冒号80,注意一个冒号。地址没给,表示地址是所有可用。
[root@localhost ~]# docker run --name myweb --rm -p 80:80  dockerhaoran/httpd:v0.2


# 显示容器上的映射端口(宿主机的0.0.0.0任意地址的80端口,所以就可以愉快的访问这台主机了      访问http://192.168.162.135/      这是我们自己指定端口的,但没指定地址。当然我们也可以既指定端口,又指定地址ip)
[root@localhost ~]# docker port myweb
80/tcp -> 0.0.0.0:80

将指定的容器端口映射至主机指定的动态端口
  指定的宿主机地址,指定容器端口,看清楚左侧是宿主机地址,右侧是容器端口。表示把这个容器的端口暴露给宿主机这个固定地址的映射端口上去。
# -p 固定宿主机地址,加两个冒号,这两个括号中间表示宿主机使用什么端口。宿主机端口为空表示动态的端口,随机端口。
[root@localhost ~]# docker run --name myweb --rm -p 192.168.162.135::80  dockerhaoran/httpd:v0.2

# 显示容器上的映射端口(他就不是0.0.0.0的32769而是192.168.162.135一个动态端口32768。      访问192.168.162.135:32768,这的确是一个对外通信的可用端口。       那我想宿主机的所有可用地址指定80。假如你的宿主机的80没有是被使用。没问题。既指定端口又指定地址。)
[root@localhost ~]# docker port myweb
80/tcp -> 192.168.162.135:32768

将指定的容器端口映射至主机指定的端口
  那我想宿主机的所有可用地址指定80。假如你的宿主机的80没有是被使用。没问题。既指定端口又指定地址。
# -p 指定宿主机ip,指定宿主机端口冒号指定容器端口,来完成映射。那因此就固定指定为192.168.162.135的8080来映射容器的80。
[root@localhost ~]# docker run --name myweb --rm -p 192.168.162.135:8080:80  dockerhaoran/httpd:v0.2

# 显示容器上的映射端口(访问http://192.168.162.135:8080/)
[root@localhost ~]# docker port myweb
80/tcp -> 192.168.162.135:8080

显示指定容器上的映射

  他其实就是把你的容器,myweb的tcp协议的80端口,容器启动时只指定了端口,没有指协议,默认就是tcp协议。映射到物理机的0.0.0.0所有可用地址的32769端口上这个端口是动态的。      那如果想固定这个地址,而不是物理机的所有可用地址。      还可以使用第二种格式。指定的宿主机地址,指定容器端口
[root@localhost ~]# docker port myweb
80/tcp -> 0.0.0.0:32769
 

实现第三种模式联盟式容器

  Joined containers(连接的容器,联盟式容器,让两个容器可以共享同一个网络名称空间。)

        联盟式容器是指使用某个已存在容器的网络接口的容器,接口被联盟内的各容器共享使用;因此,联盟式容器彼此间完全无隔离,例如
              创建一个监听于2222端口的http服务容器
                    ~]# docker run -d -it --rm -p 2222 busybox:latest /bin/httpd -p 2222 -f
              创建一个联盟式容器,并查看其监听的端口
                   -# docker run -it --rm --net container:web --name joined busybox:latest netstat -tan

        联盟式容器彼此间虽然共享同一个网络名称空间(共享UTS,Nat,IPC名称空间),但其它名称空间如User,Mount,PID等还是隔离的

        联盟式容器彼此间存在端口冲突的可能性,因此,通常只会在多个容器上的程序需要程序loopback接口互相通信、或对某已存的容器的网络属性进行监时才使用此种模式的网络模型
# 启动两个容器,看一下联盟式容器效果
## 现启动第一个容器,假如这里使用docker run 取一个命令,--it进入交互式 --rm容器关闭时删除容器,启动busybox容器(可以看到他的确有网络,地址是172.17.0.3,还有一个lo接口,没有问题,我们再启动一个容器)
[root@localhost ~]# docker run --name b1 -it --rm busybox
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:03  
          inet addr:172.17.0.3  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:7 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:578 (578.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

## 新开一个终端,启动第二个容器,这次叫b2,还是用交互式来启动,使用同一个镜像,主要是看效果的,(可以看到正常情况下他的网络地址应该使用172.17.0.4  不同的地址,因为正常启动两个容器一定是隔离的。      但我们现在使用这个功能,重新创建额外加一个选项叫--network 指明容器b1,看懂什么意思了吗,此前我们使用--network只有三种之中的其中之一,那--network 指明容器b1这什么什么意思呢?这就表示要共享b1这个容器的网络名称空间了。      启动起来后可以看到地址是172.17.0.3跟b1的地址一致,但他们文件系统还是隔离的,注意这点,可以创建文件,在b1容器和b2容器上测试。)
[root@localhost ~]# docker run --name b2 -it --rm busybox
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:04  
          inet addr:172.17.0.4  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:7 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:578 (578.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # exit
[root@localhost ~]# docker run --name b2 --network container:b1 -it --rm busybox
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:03  
          inet addr:172.17.0.3  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:8 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:648 (648.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

## 测试
### 测试文件系统是否隔离
#### b1创建文件
/ # mkdir /tmp/testdir
/ # ls /tmp
testdir

#### b2查看创建的文件(没有文件,两个容器间虽然网络名称空间是共享的,但文件系统是隔离的)
/ # ls /tmp/
/ # 


### 测试网络是否共享
#### b1启动httpd,简单一点,创建文件便于验证,启动httpd -h指定文件,不加-f不要守护,默认在后台启动。可以看到80端口已被监听
/ # echo "hello world" > /tmp/index.html
/ # httpd -h /tmp/
/ # netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 :::80                   :::*                    LISTEN   

#### 在b2上访问本地http服务,可以看到访问成功,b1和b2他俩一定是共享lo接口,相当于他俩共享是IPC。      因为他俩共享同一个网络名称空间,就相当于他俩是运行在同一主机上的两个进程一样。效果就象我们过去传统使用中,在同一个主机上的两个进程。可直接进行通信。但事实上,他们还是有一定程度上的隔离的。这是传统意义上的,在同一主机上运行的两个进程,彼此之间所不具有的功能。是不是很高级。,他既然能加入一个容器的,也就能加入宿主机的,对不对。
/ # wget -O - -q 127.0.0.1
hello world

实现第四种网络模型host

        有没有发现刚才难为的不得了,一个容器怎么暴露给外部访问,找不到方法,这个就是最简单的方法。也不用桥接,直接共享宿主机的网络名称空间,启动服务以后,他监听的自然也就是宿主机地址和相关端口。所以你要确保宿主机只要没有监听这个端口,在容器上监听这个端口。      那这有什么作用啊?为什么喜欢这样玩?那干脆既然如此,直接在宿主机上启动一个httpd不就完了吗,为什么要跑在容器中?用腿想。请问你要在宿主机上启动httpd部署方式是什么?安装nginx,改配置文件,启动服务。那如果要基于容器的方式呢?docker run结束。是不是很简单,而且将来我们这个主机坏了,想在其他主机上同样启动,docker run结束。充分利用了容器的优势,同时还能保持工作在宿主机上的优势,至少他通过宿主机的网络接口对外提供服务的这种优势。所以以往很多工作为宿主机守护进程的那些系统级管理的进程。以后就可以使用容器这种方式来玩。这就叫做第4种网络模型。
# 测试直接使用宿主机的网络名称空间
## 先停掉两个容器, 这次启动容器时直接指明host,这表示使用宿主机的了,而宿主机本身就存在,不用先去创建什么之类的。查看网络可以看到效果,直接输出的是宿主机的所有网卡和地址,容器直接使用了宿主机的地址和网卡。      如果在这里启动了httpd服务会有什么效果,可以看到他监听的是本机的80端口,谁的80?是192.168.162.135的80端口,那么在物理机外部也能访问到,     访问http://192.168.162.135/(可能需要关闭宿主机的防火墙)
[root@localhost ~]# docker run --name b2 --network host -it --rm busybox
/ # ifconfig
docker0   Link encap:Ethernet  HWaddr 02:42:CF:E4:6A:BC  
          inet addr:172.17.0.1  Bcast:172.17.255.255  Mask:255.255.0.0
          inet6 addr: fe80::42:cfff:fee4:6abc/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:72 errors:0 dropped:0 overruns:0 frame:0
          TX packets:78 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:5379 (5.2 KiB)  TX bytes:7989 (7.8 KiB)

eth0      Link encap:Ethernet  HWaddr 00:15:5D:00:01:03  
          inet addr:192.168.162.135  Bcast:192.168.162.143  Mask:255.255.255.240
          inet6 addr: fe80::7562:2afd:4ea3:7738/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:5328167 errors:0 dropped:0 overruns:0 frame:0
          TX packets:6082509 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:568051380 (541.7 MiB)  TX bytes:798260052 (761.2 MiB)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:140 errors:0 dropped:0 overruns:0 frame:0
          TX packets:140 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:12192 (11.9 KiB)  TX bytes:12192 (11.9 KiB)

veth1182386 Link encap:Ethernet  HWaddr 6A:0F:AB:4B:F9:90  
          inet6 addr: fe80::680f:abff:fe4b:f990/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:9 errors:0 dropped:0 overruns:0 frame:0
          TX packets:16 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:645 (645.0 B)  TX bytes:1120 (1.0 KiB)

vethe3cdcb6 Link encap:Ethernet  HWaddr 52:B8:26:88:4E:59  
          inet6 addr: fe80::50b8:26ff:fe88:4e59/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:18 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:0 (0.0 B)  TX bytes:1188 (1.1 KiB)

/ # echo "hello container" > /tmp/index.html
/ # httpd -h /tmp/
/ # netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      
tcp        0      0 :::80                   :::*                    LISTEN      
tcp        0      0 :::22                   :::*                    LISTEN      
tcp        0      0 ::1:25                  :::*                    LISTEN     

修改docker默认docker0网络

  修改docker默认docker0网络不在使用172.17,可以使用别的网段。      自定义docker0桥的网络属性信息要改daemon.json文件(/etc/docker/daemon.json)要使用这么几个key,如果有多个key要使用逗号隔开,最后一个key一定不能有逗号,否则是语法错误。

        {
              "bip": "192.168.1.5/24",      (这些选项中最重要的就是bip,只要改了bip其他选项他可以自动计算得知,当然dns无法计算得知,可以额外定义。      第一bip,就是指docker0桥的ip地址和他的掩码,系统会自动推算出这个桥所属的网络,并把那个网络当做随后加入这个桥的所有容器,默认网络。docker会自动给他配置一个dhcp)
              "fixed-cidr": "10.20.0.0/16",      ()
              "fixed-cidr-v6": "2001:db8::/64",
              "mtu": 1500,
              "default-gateway": "10.20.1.1",      (指明默认网关,每一个容器,获得ip地址还能获得默认网关,可以人工的设定每一个容器默认网关,他可以不是你的docker0桥网关)
              "default-gateway-v6": "2001:db8:abcd::89",      (这是ipv6的网关)
              "dns": ["10.20.1.2","10.20.1.3"]      (还可以指dns服务器地址,每一个容器启动以后,自动拥有相应的dns服务器地址)
        }

        核心选项为bip,即bridge ip之意,用于指定docker@桥自身的IP地址;其它选项可通过此地址计算得出。
# 修改node02的docker0桥ip,先停掉服务
[root@localhost ~]# systemctl stop docker

# 编辑daemon.json(指定bip,额外的可以不用指,假如用不着的话。如果期望每一个容器自动获得的dns服务器不是宿主机的dns服务器设定,还可以在这儿加一个dns,他的值是一个列表,可以给定至多3个,至少1个,再多了也没用)
[root@localhost ~]# vi /etc/docker/daemon.json
{
        "registry-mirrors": ["https://registry.docker-cn.com"],
        "bip":"10.0.0.1/16"      # 添加bip地址,这里使用10.0.0.1使用16位掩码,16位掩码的意思是10.0是固定的,0.1是主机地址,我们使用的是16位掩码
}

# 启动服务,启动服务,查看docker0桥ip,可以看到是10.0.0.1,随后分配的ip地址10.0.0.2往后顺延,这是如果去改docker0桥自己的所使用的地址的
[root@localhost ~]# systemctl start docker
[root@localhost ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:15:5d:00:01:0d brd ff:ff:ff:ff:ff:ff
    inet 192.168.162.136/28 brd 192.168.162.143 scope global dynamic eth0
       valid_lft 86339sec preferred_lft 86339sec
    inet6 fe80::84fc:3c4b:1479:81ce/64 scope link 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN 
    link/ether 02:42:97:b4:18:ed brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.1/16 brd 10.0.255.255 scope global docker0
       valid_lft forever preferred_lft forever

允许外部连接docker服务器

   dockerd守护进程的C/S,其默认仅监听Unix socket格式的地址,/var/run/docker.sock;如果使用TCP套接字,
        /etc/docker/daemon.json:
              "hosts": ["tcp://0.0.0.0:2375" , "unix:///var/run/docker.sock"]

        也可向dockerd直接传递"-H | --host"选项;

   这个docker自己守护进程,是只监听在Unix socket文件对不对。node01是一个docker,node02是一个docker,我们可不可以在node02上控制node01的docker容器?默认是不可以的,因为你的docker自己只监听了Unix socket文件,只支持本地通信,这个路径是/var/run/docker.sock,而docker命令,就是客户端命令默认通信,他连服务器是怎么连的呢,docker 有一个选项,docker -H指明docker服务器是谁,如果不指就是/var/run/docker.sock。      如果我期望我们的docker服务器能允许别人从外部连接进来,那我们应该监听在一个正常的tcp的端口了,具体方式一样的改daemon.json配置文件,设定hosts这个键,他默认只有unix:///var/run/docker.sock这一个路径,我们现在可以在这个路径之外在加上一个,有tcp协议监听本机的,tcp协议的2375端口,而且是本机的0.0.0.0所有可用地址
(也不好使)
# 编辑docker文件:/usr/lib/systemd/system/docker.service
vim /usr/lib/systemd/system/docker.service

# 修改ExecStart行为下面内容
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \

# 重新加载docker配置
systemctl daemon-reload // 1,加载docker守护线程
systemctl restart docker // 2,重启docker

# 查看监控的端口
[root@localhost ~]# ss -tunl
tcp   LISTEN     0      128                                      :::2375                                                 :::*      

# 验证,在node01上连接node02


----------------------------------------------- 下面功能不好用 -----------------------------------

# 关闭docker服务
[root@localhost ~]# systemctl stop docker

# 编辑daemon.json文件(在加一项hosts,本地的sock文件应该保留着,因为当我们连接本机的时候,默认不指应该就是找这个路径的)(不好用)
{
        "registry-mirrors": ["https://registry.docker-cn.com"],
        "bip":"10.0.0.1/16",
        "hosts": ["tcp://0.0.0.0:2375","unix:///var/run/docker.sock"]
}


# 启动服务

# [root@localhost ~]# systemctl reset-failed docker.service      命令能够重置单元的启动频率计数器。 系统管理员在手动启动某个已经触碰到了启动频率限制的单元之前,可以使用这个命令清除启动限制。 注意,因为启动频率限制位于所有单元条件检查之后,所以基于失败条件的启动不会计入启动频率限制的启动次数之中。 注意, slice, target, device, scope 单元不受此选项的影响, 因为这几种单元要么永远不会启动失败、要么只能成功启动一次。

[root@localhost ~]# systemctl start docker 

创建自定义的桥

# 查看已有的网络(事实上我们还可以创建别的桥)
[root@localhost ~]# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
5021794dc405        bridge              bridge              local
93fb6367a31e        host                host                local
148c073ba58a        none                null                local

# 查看对应所支持的网络插件(bridge桥仍然以桥接格式的跟docker0桥很相似,macvlan基于mac的vlan虚拟网络,overlay叠加网络)
[root@localhost ~]# docker info
  Network: bridge host ipvlan macvlan null overlay

# 创建桥(-d 指定桥类型,默认bridge桥接式的桥和docker0一样,      --subnet指定子网范围172.26.0.0指定16位的掩码,      --geteway指定网关172.26.0.1。这个选项不指应该也是0.1就是这个网段内的第一个地址,      加上网络接口名,比如叫mybr0)
[root@localhost yum.repos.d]# docker network create -d bridge --subnet "172.26.0.0/16" --gateway "172.26.0.1" mybr0
3980590328c88fea53bc897a5003708fc5c7241f46f7a79d5704dd8f25db4313

# 列出网络(名字为mybr0的bridge桥已然存在)
[root@localhost yum.repos.d]# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
c0ba97cf5b24        bridge              bridge              local
dbc05772cfc7        host                host                local
3980590328c8        mybr0               bridge              local
551f9e42fb2a        none                null                local


# 查看网络设备上的协议地址(可以看到br-3980590328c8这样名字的设备,注意这个名字不是你网络的名字,我们网络的名字为刚才自己创建的mybr0,知道了吧,网络叫mybr0可不是网络接口加mybr0,这接口名可以使用ip link改名,而且地址是172.26.0.1)
[root@localhost yum.repos.d]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:b7:aa:0b brd ff:ff:ff:ff:ff:ff
    inet 172.20.18.136/20 brd 172.20.31.255 scope global noprefixroute dynamic eth0
       valid_lft 81347sec preferred_lft 81347sec
    inet6 fe80::27a7:2a8d:c9ef:256b/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:d8:8e:b4:63 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
4: br-3980590328c8: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:ce:be:3e:c5 brd ff:ff:ff:ff:ff:ff
    inet 172.26.0.1/16 brd 172.26.255.255 scope global br-3980590328c8
       valid_lft forever preferred_lft forever

# 将br-3980590328c8接口改名(set设置,dev设备是br-3980590328c8,name改名字为docekr1)
[root@localhost yum.repos.d]# ip link set dev br-3980590328c8 name docker1
RTNETLINK answers: Device or resource busy(RTNETLINK答案:设备或资源繁忙)      (需要先把设备domn掉在改就行了)

# 创建容器时加入mybr0(可以看到mybr0的网络172.26.0.2,现在就有两个桥接式网络了,思考一下这两个桥可以比如两个交换机,两个交换机网段地址不同,这两个设备上的主机可以通信吗?其实他们两个交换机的网关是不是在宿主机上,把宿主机的核心转发功能打开就行了)
[root@localhost yum.repos.d]# docker run --name t1 -it --net mybr0 busybox
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:1A:00:02  
          inet addr:172.26.0.2  Bcast:172.26.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:13 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:1102 (1.0 KiB)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

# 打开宿主机的核心转发功能(值为1,他们两个交换机之间是可以互通的,只要我们没有规则阻止他,iptables会阻断这些容器之间互相通信的)
[root@localhost ~]# cat /proc/sys/net/ipv4/ip_forward
1


## 查看帮助(创建桥时支持使用很多选项)
[root@localhost yum.repos.d]# docker network
  create      Create a network(创建一个网络( 创建一个桥))

[root@localhost yum.repos.d]# docker network create --help

Usage:  docker network create [OPTIONS] NETWORK

Create a network

Options:
      --attachable           Enable manual container attachment(启用手动容器附件)
      --aux-address map      Auxiliary IPv4 or IPv6 addresses used by Network driver (default map[])(网络驱动程序使用的辅助IPv4或IPv6地址(默认映射[]))
      --config-from string   The network from which copying the configuration(从中复制配置的网络)
      --config-only          Create a configuration only network(创建仅配置网络)
  -d, --driver string        Driver to manage the Network (default "bridge")(驱动程序来管理网络(默认为“bridge”))
      --gateway strings      IPv4 or IPv6 Gateway for the master subnet(主子网的IPv4或IPv6网关)
      --ingress              Create swarm routing-mesh network(创建集群路由网状网络)
      --internal             Restrict external access to the network(限制外部访问网络)
      --ip-range strings     Allocate container ip from a sub-range(从子范围分配容器ip,分配的地址池)
      --ipam-driver string   IP Address Management Driver (default "default")(IP地址管理驱动程序(默认为“默认”))
      --ipam-opt map         Set IPAM driver specific options (default map[])(设置IPAM驱动程序特定的选项(默认映射[]))
      --ipv6                 Enable IPv6 networking(启用IPv6网络)
      --label list           Set metadata on a network(在网络上设置元数据)
  -o, --opt map              Set driver specific options (default map[])(设置驱动程序特定的选项(默认映射[]))
      --scope string         Control the network's scope(控制网络范围)
      --subnet strings       Subnet in CIDR format that represents a network segment(代表网段的CIDR格式的子网,他的子网)

docker存储卷

  通过此前内容的讲解,相比应该能够理解到对于docker来讲他作为一个容器运行的底层引擎来组织和运行其容器时。每个容器内只运行一个程序及其子程序,那么对这个程序本身来讲,他启动时时是依赖于底层很有可能不止一层的镜像联合挂载启动而成,而且他底层能够存储此类分层构建,并实现联合挂载镜像的文件系统包含像什么aufs以及overlayfs2,随后一定要在最上层构建一个所谓的可写层,让他能写,默认情况下是可读的,所以他是一个读写层更合适一点,对于读写层来讲,所有在容器中所执行的操作,包括对数据,对内容的修改,事实上都是保存在这些分层镜像之上的读写层中的,而对下层内容的操作,假如说要删除某个事先存在的文件,那我们要是用过所谓的叫“写时复制”的机制来实现

  Why Data Volumes?(为什么需要数据量?)
        Docker镜像由多个只读层叠加而成,启动容器时, Docker会加载只读镜像层并在镜像栈顶部添加一个读写层

        如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本仍然存在,只是已经被读写层中该文件的副本所隐藏,此即"写时复制(COW"机制

              这个“写时复制”所实现的效果,首先如果一个文件在Layer0最底层是存在的,如果在Layer1上面我们给他标记为删除,要真正执行删除,只能标记为删除,而后假如现在最高的层级是用户看到的Layer2在Layer2上是看不到这个文件的。      同样的逻辑在Layer0所存在的某一个文件在Layer2上也能删除掉,他只是标记为删除。      接着在Layer1上的文件在Layer2上标记为删除,      以及Layer0上的文件在Layer1上改了,在Layer2上也删除,      反正意思是在到达最上层Layer2之前我们如果把他标记为了删除,对于这个最上层的用户一定是不可见的,如果没标记为删除,或者是标记为删除了,但用户在最上层又建了一个一模一样的同名文件的用户才可见。      所以无论在Layer0上添加,在Layer1上添加,Layer2上添加,这都是用户直接可见的,用户能够访问到的文件,      同样的在底层存在,或者在底层上存在以后被删除,但是在Layer2最上层我又对他做了一些所谓的创建或者修改类的操作的话,那么对上层用户来讲他也是可见的,这是所谓的镜像内部其文件系统层级的组织结构和组织关系,我们且不说这种方式,我们用户到底对哪些内容可见了,对于这种方式来讲我们先访问一个文件,需要先改删除等一类的操作,一般效率应该非常的底,因为毕竟组织了这么多层,他要不断的去隐藏,而且也不是真正的删除等一类的操作,所以效率会非常的底,那么带给我们直接的结果就是,如果我们能够期望能够,尤其是那些对IO要求较高的应用,比如像Redis他要实现持久化缓存的时候,这样的存储系统势必对在底层应用数据存储时性能要求较高,      比如说我们如果运行了所谓的存储服务叫mariadb或者是mysql,我们试想一下,mysql本来就对IO要求就较高,如果我们mysql运行又是写在,一个运行在容器中mysql自己的文件系统上,就是联合挂载的文件系统最上可写层之上的时候,那么在容器停止时且不说他应被删除,那么在实现数据存取时其效率比较底也是必然的结果,      要想绕过这种使用的限制我们可以通过使用存储卷的机制来实现,所谓存储卷,我们可以简单想象成在特权级的名称空间当中,我们也可以理解为就是宿主机好了,在宿主机当中找一个本地文件系统,就是我们主机级的自己的文件系统,之上我们可能存在某一目录,而后把这个目录直接与容器内部的文件系统之上的某一目录,或者叫某一访问路径建立所谓的绑定关系,事实上文件也可以,我们这里特意强调目录,是因为目录比较常见一点,那也就意味着就像我们此前的挂载一样,      比如说我们在/data目录下存在一个web这样的目录(/data/web),这样一个目录,我们把容器内的/data/web与我们宿主机之上的某一路径比如叫做/containers/data/web这个目录建立的绑定关系,随后容器内的进程向/data/web这个目录中写数据的时候是直接被写在宿主机的/containers/data/web这个目录上的(/data/web -> /containers/data/web),在我们之前学习使用mount命令时就应该知道,我们使用mount -B, --bind              挂载其他位置的子树(同 -o bind),就能够让两个目录建立绑定关系,这里与其所实现的效果非常相识,所以这样就使得我们容器内的进程在实现数据保存时,能跳过或者能绕过容器内部文件系统的限制,从而与宿主机的文件系统建立的关联关系,使得我们可以在宿主机和容器内,共享数据内容,同样的也可以让容器直接访问宿主机的内容,同时也可以让宿主机直接向容器供给内容,二者是同步的,这就相当于说mount这样一个名称空间,对所有容器来讲本来是隔离的,但是我们确可以让两个本来是隔离的文件系统在某个路径上建立一定程度的绑定关系,从而使的在两个容器之间的文件系统的某个子路径上不在是隔离的,而是能够实现一定意义下的共享的效果,所以这种所谓的关联关系使得能够让容器之间在跨文件系统共享数据时变得比较容易了,而在宿主机上的这个目录,就是跟容器内文件系统建立绑定关系的这个目录相对于容器来讲就被称之为叫做volume我们把他称之为叫卷,很多时候为了加以区别通常称作叫存储卷。      如果所有的对容器内这个进程的所有有效数据都是保存在存储卷,从而脱离了容器自身文件系统以后那带来的好处就是,当容器关闭甚至并删除时我们都不用担心数据丢失了,只要不删除与之绑定的在宿主机上的这个存储目录就可以,因此我们就能够实现数据持久,脱离容器的生命周期而持久这样以来的好处我们也曾经想过,即便容器停止完以后,就是直接被删除,还记得我们之前创建容器都会添加一个--rm选项对吧,容器一关闭就意味这删除了,但是随后再次重建这个容器时,如果我们能让他使用到或者关联到同一个存储卷上,在创建容器虽然已经不是之前那个容器了,但数据还是那个数据。      这就特别类似此前讲到的进程的运行逻辑是一样的,进程本身不保存任何数据,数据应该都在进程之外的文件系统上,或者是专业的存储服务之上,因此进程每一次停止,进程就终止,进程就不见了,本身本地保存的只有进程所属的程序文件,那么对容器来讲也是如此,以后容器自身就当做一个完全有生命周期的动态对象来使用,容器关了就是容器删除的时候,而后在底层的镜像文件是一直在的,所有我们随后可以基于镜像同时在重新启动这个容器,      不过各位有没有发现我们启动容器和启动进程时有一个特点不太一样,启动容器需要向容器内的程序所传递的选项比较多,我们不一定每次都能记得这些选项,所以上次我启动容器时到底被关联到了哪个或哪些卷上,下次启动忘记了怎么办。那这样子我们就很难复现一个一模一样的进程或者叫做一个一模一样的容器了。因此如果可以的话,我们最好能用,一个文件保存下来是如何启动并创建容器的相关配置,那该有多好,那这一般都是容器编排工具的作用。我们这里使用docker,大多数情况下都只能在命令行中去启动,如果可以能让docker读一个文件,里面包含docker启动时所关联的所有的,比如关联的卷啊,启动时传的参数等等等等,那就好很多了,但事实上docker虽然能读,但事实上这样子也只能编排一个容器。所以我们说后续我们需要专业的容器编排工具来实现这个功能,这也是原因之一了。      另外我们通过这种方式来管理容器,它带给我们优势还有一项,容器就可以不具与,不但与启动在哪台主机之上。      那如果我们现在有多个docker host,更重要的是它所关联的卷其实也不是宿主机本地的文件系统。      假设我使用一个共享的文件系统,比如像NFS,找一个NFS服务器共享一个目录,然后呢这每一个docker宿主机挂载了NFS目录,那么很显然,对宿主机来讲,这好像就是本地目录一样,对不对。你先确保宿主机能驱动并连接至后端的存储服务上去,那也就意味着这每一个宿主机得首先是NFS客户端,并正常实现了挂载,那于是他本地就有文件系统了。      在这个宿主机之上,我们启动某一容器时,容器使用自己的,注意自己本地的某个路径,打开一个目录,关联到了宿主机之上的某个目录,而这个目录恰恰是NFS之上的一个对应的路径,或者一个文件系统,那么很显然,随后当这个容器启动,比如redis假设,保存数据,当它保存在宿主机之上,事实上是保存在NFS之上的,所以当这个容器停止以后就自动被删除了,之所以被删除,还有个好处在于以后下次再启动这个容器时,启动到哪儿就不一定非得仅限于这个主机了,对不对,我们可以实现全集群范围内来调度容器的启动和运行的。      比如现在启动到第二台主机上,一样的,只要那个NFS上的数据目录依然在,只要这个宿主机依然能关联到NFS之上,那么于是第二台容器,他也照样能访问到里面的数据的,因此我们以后再去分配存储计算和内存资源的时候,就不局限在单机之上了。而可以在集群范围内,在多级范围内进行,这种计算我们在上节课都已经提到过,他就是云计算,对吧,我说叫云里雾里的计算OK。而这种工具其实我们也说过,目前来讲各种所谓的docker的编排工具,这几种主流的基本都能实现此类功能。但是大家有没有发现这背后严重依赖于一个组件叫共享存储系统,对吧。这是因为考虑到我们的容器应用是需要持久存储数据是有可能是“有状态”的。如果是一个NFS反向代理,他有没有必要存储数据?其实我们应用我们此前反复强调,它可以分为“有状态”和“无状态”两种,对吧。其中“有状态”,我们是认为说,当前这一次连接请求的处理一定与此前的处理是有关联关系的。      而“无状态”应用它的前后应用属于是没有关联关系的。      大多数“有状态”应用都是需要持久存数据的,比如像mysql,redis他们都算是“有状态”的应用,都需要前后存数据。      像nginx做反代服务器就属于纯粹的“无状态”应用。      那么任何“有状态”的应用都是需要持久存储数据。你可以这么来理解,其实虽然并非如此,但各位可以这么去理解,我们应用呢,从他所谓是“有状态”还是“无状态”,你是否需要持久存数据方面,我们可以给他定义了一个正交的坐标系,它是有着正交关系的坐标系。假如说,横轴和纵轴,上面表示“有状态”,下面表示“无状态”,横轴右侧表示需要存储,左侧表示不需要存储,这就有了第一、第二、第三、第四象限,对吧,一、二、三、四、四个象限,所以我这是一种叫正交关系。在第一象限当中表示那些“有状态”且需要存储的,大多数的存储服务,像mysql、redis等等都是“有状态”且需要持久存储的这是第一象限。有些是有状态但无需持久存储的,比如像tomcat,如果只是在内存中保存会话,不惧丢失,丢失了无所谓,那么他就是一种有状态无需持久存储的,还有一种就是“无状态”,也无需存储的数据。像刚才讲到的各种反向代理服务器都是如此。还有我们LVS他也无状态,每一个请求连接请求都是被当做一个独立的请求这一角度,他本地也不需要保存任何数据,所以叫无状态。无持久存储数据,那么第四象限就叫做无状态,但需要持久存储数据的,这种极少见,了解就是。将来运维的大多数场景,而且运维起来最难的是有状态且需要持久的。它需要大量的运维经验和运维操作步骤的支撑,才能工作起来,比如做一个mysql主从。你想想是不是这个道理,它需要我们运维知识、运维技能、运维经验柔和进去,你才能实现所谓的部署,扩展,扩容或者缩容就好。还有出现问题以后的修复。你必须要了解你的整个集群规模有多大,有几个主节点有几个从节点,那么从节点是指向的主节点是谁,从节点上面只负责哪些数据库,你必须要一清二楚,接下来我们才能够在他出现故障时进行修复的,对不对,所以这些就完全强依赖于我们的运维经验。对于无状态的这事儿就非常简单,大家知道我们要装一个nginx,yum install 给他推一个配置文件就直接启动起来了,一个nginx不够怎么办,在起一个跟之前的没有任何关系,对于无状态的应用来讲,我们可以非常迅速的实现复制,在运维上是可以实现自动化,很容易的。但是对于有状态的来讲,这几乎是目前来说我们脱离不了相关人员运维技能来做管理的一个维度。因此极少有平台能在这个功能上、在这个层次上取代运维人员的工作。就连K8S也不能,早期,现在k8s也有一些相关工具能实现类似的功能。后面会再讲这个东西在k8s上也是非常困难的,到现在为止其实也依然不成熟。      那因此所以刚才我给大家讲docker这种存储数据什么之类的,我们运行为一个容器时,需要考虑到,假设我们不把他当作分布式的逻辑,即便都把他当做分布式的应用来运行,我们运行一个单一的mysql,他也需要持久存储数据。如果说我们期望对应的这种应用程序能跨多机被调出的话,这就必须涉及到他在下一次在其他宿主机上被启动时,能找到此前持久存储数据,否则你这种启动和运行,它俨然已经不在是你此前所正在运行的应用,你用它来复原,他要从零开始,这对我们实际生产力来讲是不合适的。因此持久数据是必须且必备的条件。      因此再强调一遍,对于这种所谓的“有状态”类的应用来讲,不用存储卷数据只能放在容器本地。假如你能接受它的存储效率,但是它会搞这个容器,没法被迁移。就相当于给我们当前的这个宿主机就绑定了,而且随着容器生命周期终止,比如说他停了,你也不能把它删除,只能等待让它处于停止状态,下次再启动数据还在,否则如果你删了再启动数据就没了。因为它的可写层是随容器生命周期而存在的,所以这么一来,大家应该牢牢建立起一个概念来。对于“有状态”应用,几乎存储卷是,只要你需要持久存数据,存储卷就几乎是必须的。不过呢对docker来讲,他的存储卷用起来并没有我们假想的这么麻烦,因为它并没有我们想象的这么强大的功能。至少说你如果不借助于自己的额外的体系来组织这个维度的话。因为docker的存储卷默认情况下是使用其所在的宿主机之上的本地文件系统目录。也就是说你的宿主机只有一块磁盘这个磁盘并没有共享给其他的 docker主机。然后这个容器所使用的目录,就那个存储卷是关联到宿主机的磁盘上一个目录而已,也就意味着这个容器在当前的一个宿主机之上,你停了甚至删了再创建,只要能关联宿主机上的数据还存在,但我把它调到下去就很可能了。这也是docker本身没有解决的问题,大家一定要注意这一点。所以说docker默认存储卷就是docker主机本地的,它并不是我刚才给你描述的说,我们使用一个共享存储。但是各位有没有发现我们自己真正建立docker主机,我们自己手动去做一个NFS,自己手动 让docker主机都把它挂上来,他就能达到我们要求了,对吧。但是这样的有一个缺陷在于,有没有发现这个过程是不是还是强依赖于运维人员自己的解决问题的能力的

  Why Data Volume?
        关闭并重启容器,其数据不受影响;但删除Docker容器,则其更改将会全部丢失

        存在的问题
              存储于联合文件系统中,不易于宿主机访问;
              容器间数据共享不便
              删除容器其数据会丢失

        解决方案: "卷(volume)
              “卷”是容器上的一个或多个“目录",此类目录可绕过联合文件系统,与宿主机上的某目录"绑定(关联)"

  Data volumes(数据量)
        Data volumes provide several useful features for persistent or shared data(数据量为持久性数据或共享数据提供了几种有用的功能)
              Volume于容器初始化之时即会创建, 由base image提供的卷中的数据会于此期间完成复制
              Data volumes can be shared and reused among containers(数据量可以在容器之间共享和重用)
              Changes to a data volume are made directly(直接更改数据量)
              Changes to a data volume will not be included when you update an image(更新映像时将不包括对数据量的更改)
              Data volumes persist even if the container itselfis deleted(即使删除容器本身,数据卷仍然存在)

        Volume的初衷是独立于容器的生命周期实现数据持久化,因此删除容器之时既不会删除卷,也不会对哪怕未被引用的卷做垃圾回收操作;

        卷为docker提供了独立于容器的数据管理机制
              可以把“镜像”想像成静态文件,例如“程序”,把卷类比为动态内容,例如"数据";于是,镜像可以重用,而卷可以共享;

              卷实现了“程序(镜像)”和“数据(卷)”分离,以及“程序(镜像)”和“制作镜像的主机”分离,用户制作镜像时无须再考虑镜像运行的容器所在的主机的环境;

                    在docker之上如果我们要用到存储卷的话,我们不必要自己手动去创建。但事实上虽然也可以,但是它的使用方式是当我们需要用到存储券的时候,你只需要在启动或创建docker容器时。一般是创建,在容器初始化时自动被创建。而后启动容器完以后它自动建立所谓的绑定或关联关系。所以说Volume的初衷是独立于容器的生命周期实现数据持久化,因此删除容器之时既不会删除卷,也不会对哪怕未被引用的卷做垃圾回收操作;。但是如果我们删除容器时,使用了特殊的选项,也可以一并删除容器的存储卷。我们可以做到,但是默认他是不这么干的。所以从而有了存储卷以后,我们在docker容器内部的程序在实现读写数据时,如果你写在根上,它依然保存在我们联合挂载文件系统之中。如果我要写在一个叫卷的目录上,被组织成卷的目录上,他就写在宿主机的关联目录上去了。大家知道程序运行过程当中经常会生成很多的临时数据,对吧,临时数据就会保存在tmp目录下,而tmp目录通常不是卷,所以他通常会写在自己的可写层当中,这些数据当然随着容器的删除而被删除,是没有任何影响的,是没有问题的。只有那些关键性的数据,我们才需要把它保存在存储卷上。



  Volume types(卷类型)
        Docker有两种类型的卷,每种类型都在容器中存在一个挂载点,但其在宿主机上的位置有所不同;

              Bind mount volume(绑定挂载量)
                    a volume that points to a user-specified location on the host file system(指向主机文件系统上用户指定位置的卷)

              Docker-managed volume(Docker管理的卷)
                    the Docker daemon creates managed volumes in a portion of the host“s file system that“s owned by Docker(Docker守护进程在Docker拥有的一部分主机文件系统中创建托管卷) 

                    对darker来讲,它有两种类型的存储卷,第一种叫绑定挂载卷,所谓绑定挂载卷,简单来讲就是在宿主机上的路径,你需要人工指定一个特定路径,在容器的路径也需要指定一个特定路径,让二者两个已知路径建立关联关系。第二种叫docker管理的卷,所谓docker管理的卷,指的是要看我们只需要在容器内指定容器内的挂载点是什么,而被绑定的是宿主机上的哪个路径下的路目录你不需要管,有容器引擎docker daemon 自行创建一个空目录,或者使用一个已存在目录,与你的存储卷路径建立关联关系。所以这个目录用户是不需要指的,它是自动有docker daemon所维护。而且他约定俗成的位于某个特定目录下卷名称,目录名称通常是id号 ,那这种方式就极大的解脱了用户在使用卷时的这种耦合关系。但缺陷在于用户没法指定使用哪个目录。所以作为临时存储,就是我们先初始化一个容器,给他提供一个空间让他能用的时候,非常有效。但是你一旦把这个容器删除了,下次重建容器时你就必须使用绑定挂载方式,不然的话他有可能会给你重新初始化一个,另外一个卷。      好的,所以docker有两种类型的卷,一种是宿主机上的目录和容器内的目录你都必须要明确指定的,叫绑定挂载卷。一种是我们只需要指定容器内的目录,宿主机上的目录有它在某一特定路下自动创建一个,自动关联用户无需指定,这种就叫docker管理的卷。

  在容器中使用Volumes(要想在docker中使用存储券,方法非常简单)
        为docker run命令使用-v选项即可使用Volume
              Docker-managed volume
                    ~l#docker run -it-name bbox1 -v /data busybox(我在docker run的时候加一个-v指定目录,这里给一个目录指的是容器内的目录,没有给宿主机的目录,所以这就是一种docker 管理的卷。)
                    ~# docker inspect -f {{.Mounts}} bbox1
                          查着bbox1容器的卷、卷标识符及挂载的主机目录

              Bind-mount Volume
                    ~]# docker run -it -v HOSTDIR:VOLUMEDIR --name bbox2 busybox(第二种,我们在指定了HOSTDIR宿主机的目录,VOLUMEDIR又在其前方指定了宿主机上的特定路径。像这种就叫做绑定挂载卷)
                    ~]# docker inspect -f {{.Mounts}} bbox2

绑定卷

  我们使用node1和node2来举例说明

创建容器,并指定有docker管理的存储卷

# -v挂载到/data目录下,这个目录事先是不存在的,创建完成后可以看到这里立即有一个data目录。那这个data目录到底是属于哪的路径,与哪个存储卷有关联没关系啊,我们去探测一下容器的详细信息。
[root@localhost ~]# docker run --name b1 -it -v /data busybox 
/ # ls /
bin   data  dev   etc   home  proc  root  sys   tmp   usr   var


# 新终端查看容器详细信息(对于b1来讲,这里告诉我们Volumes是/data。哪这个Volumes到底跟谁建立的关联关系,在Mounts这一段中有说明)
[root@localhost ~]# docker inspect b1
        "Mounts": [
            {
                "Type": "volume",(类型叫做volume)
                "Name": "a2e07899722483a4e9d8c28cf610777a44bf6511553af298fa75ef55a4fafd68",(这个名称是uid,卷名称或者叫卷id)
                "Source": "/var/lib/docker/volumes/a2e07899722483a4e9d8c28cf610777a44bf6511553af298fa75ef55a4fafd68/_data",(这个对应的卷在那,这是他真正关联的那个路径)
                "Destination": "/data",(目标路径,就是我们的容器内的路径是/data)
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }

#     ----------------------------------------
            "Volumes": {
                "/data": {}

# 查看,并测试宿主机上关联的目录(我直接在宿主机这个路径下,创建一个文件,在容器内是可以看到见的)
[root@localhost ~]# cd /var/lib/docker/volumes/a2e07899722483a4e9d8c28cf610777a44bf6511553af298fa75ef55a4fafd68/_data
[root@localhost _data]# ls
[root@localhost _data]# echo "hello container" >> test.html
[root@localhost _data]# ls
test.html

## 在容器内查看验证,并测试在容器内创建的文件在宿主机上是否可以看到
/ # ls /
bin   data  dev   etc   home  proc  root  sys   tmp   usr   var
/ # ls /data
test.html
/ # cat /data/test.html 
hello container
/ # echo "hello host" >> /data/test.html 

## 在宿主机验证(可以看到,所以能就非常方便的能实现在容器和宿主机之间共享数据。而且这个路径它也的确是有宿主机自动生成的。)
[root@localhost _data]# cat test.html 
hello container
hello host

创建容器,并指定绑定挂载卷

# 删除掉之前创建的b1容器,避免有影响
[root@localhost ~]# docker rm b1
b1

# 创建容器,--rm关闭容器时,自动删除。我们在创建-v的时候,前面的路径是宿主机路径,冒号分割后面的路径是关联到容器内的路径
[root@localhost ~]# docker run --name b1 -it --rm -v /data/volumes/b1:/data busybox
/ # 

# 在新终端,宿主机上查看容器详细
[root@localhost _data]# docker inspect b1
        "Mounts": [
            {
                "Type": "bind",
                "Source": "/data/volumes/b1",(宿主机的路径是/data/volumes/b1,这个路径在宿主机上会自动创建,在里面创建数据,只要容器指定了这个路径,数据就存在,这就拥有了脱离容器生命周期的持久功能 )
                "Destination": "/data",(容器内的路径依然是/data)
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }

使用模板过滤相关消息

  无论什么卷,我们可以看到他的绑定信息都是保存在docker inspect信息中的Mount中的。OK,其实我们在使用docker,inspect去查看,去探测容器的相关消息时,可以使用go模板,因为docker是用go语言开发的。好,当我们去获取其中某一段内容的时候,你可以指定说这是从根开始的ID,比如Status的上一级是State,那State的上一级是谁,那没有上一级是不可能,我们通常把他称作根,由此如果我们想看某一特定的键的相关数据时,你可以使用类似这种最简单的方法来进行过滤。      这里过滤式可以用一些编程语法的比如过滤,判断
# 这就表示我们看根下的Mounts,最外层的{}是我们的语法格式,引用模板的,内层的这个{.Mounts}本来就是一个数组,就是一个json格式的数组。这里可以看到bind是把/data/volumes/b1与/data建立启的关联关系的
[root@localhost _data]# docker  inspect -f {{.Mounts}} b1
[{bind  /data/volumes/b1 /data   true rprivate}]

# 如果要看NetworkSettings相关信息
[root@localhost _data]# docker  inspect -f {{.NetworkSettings}} b1
{{ 2fc5ecde0e710a73747b7ade36b6258e2108a060f6cf48d3d469d62616f470ce false  0 map[] /var/run/docker/netns/2fc5ecde0e71 [] []} {6ff07e35dff1d547e4447c76ac6d894573c0f7590299a71dee804775871eeb83 172.17.0.1  0 172.17.0.2 16  02:42:ac:11:00:02} map[bridge:0xc0002e8c00]}


# 如果想看IPAddress信息(这样就可以单独获取一个容器的IP地址了)
[root@localhost _data]# docker  inspect -f {{.NetworkSettings.IPAddress}} b1
172.17.0.2

复制别的容器卷设置

  另外大家试想一下,既然一个docker容器能够与宿主机的,就一个docker容器的目录,能够与宿主上的目录建立关联关系的。请问,那我可不可以让两个docker容器同时关联到宿主机的同一个目录上来,可以,OK那这样子其实就实现了什么?没错,他们可以实现共享使用同一个存储卷,而且也能够在容器之间共享数据了。      在我启动b3时要使用跟B2一模一样的存储卷设定,除了在容器启动时手动指定,还能怎么办呢?我们可以初始化B2时直接指明说我要复制B2的卷设置,我要使用跟b2一模一样的存储卷就可以了,他就不用非得自己指定使用什么卷了。      所以以后可以这么玩,我们找一个容器起一个容器,这个容器什么事也不敢,不用启动,我只是创建了没启动,它只要在就行。只不过在创建这个模板容器时,我指定了他该使用什么样的存储路径,但他都不启动了,以后就放这儿了。作为我们接下来启动其他几个有关联的容器的基础架构容器,基础支撑容器。所以我在启动真正有生产作用的第一个容器时,去复制它的存储卷设置,能明白吧,那因此随后再启动第二个也去复制它的存储卷设置,明白了把,那因此好处在于,这两个容器是不是使用的一模一样的存储卷,他们就通过这个底层的基础支撑容器获得某些环境设定。当然如果只是为了存储卷一个用途,就这么去大张旗鼓的搞,好像有点大材小用,有点动作太大了。事实上我们曾经讲过joined Containers叫联盟式容器。联盟式容器他能干什么,他可以共享网络,共享同一个底层的网络名称空间的包含了什么UTS,Network,IPC,所以这样子,如果这两个容器就是建立,本来就有比较密切的关联关系,我举个例子,比如说要设置一个nt,想象nginx加tomcat。nginx对外他提供服务是不是就够了,tomcat有没有必要向外提供服务?没有必要,那tomcat是服务于谁,是nginx。tomcat他仅是向nginx这个反代提供服务。所以我们只需要让nginx对外提供服务就行了。那这样子一来,大家有没有发现他们两个共享同一个底层容器所创建时使用的网络名称空间。因此nginx和tomcat都应该有什么地址,这两个容器都应该有使用同一组相同的地址,比如有对外使用的一个网络地址,也有lo接口,所以tomcat只监听在lo上是不是就行,而nginx可以监听在对外通信的地址上。所以这么一来,所有用户想访问这两个容器,得先找谁呀?先找nginx,由nginx反代至tomcat上就够了,还可以把tomcat这个容器隐藏起来的。 好不好玩。      所以我们说有了这个基础支撑容器,可以就把他们两个容器打包起来,联合起来工作了。那然后我们再组织一个,仍然在同一个基础之上,再组织一个mariadb监听在172.0.0.1上或者lo接口上,让tomcat通过lo接口跟他通信,因为他只服务于tomcat。这一层级关系,建立起来了,做一个NMT就这么组织起来,而且T和M还不需要对外监听什么端口。      大家有没有发现,这三个东西联合起来特别像一个虚拟机或者像一个专门的主机,就是一个独立的主机,只不过这一个主机只跑了三个进程而已,一个ng inx,一个tomcat,一个mariadb。因为它对外同一个主机名、同一个域名、同一组IP地址,你说是不是同一主机,因此将来我们希望把几个关系紧密的容器组织在一起的时候,就可以把它们这样揉起来,能够共享网络名称空间中的三个UTS,Network,IPC,除此之外,他们还可以共享存储卷,网页如果放在了某一个路径,你的动态的,静态的都打包在一块儿了。nginx直接处理静态的,tomcat处理动态的,他们在同一个目录下,你说怎么让他们怎么访问同一个目录? 存储卷,使用存储卷挂载同一目录路径。没错,这种组织方式确实是我们将来,来构建此类应用时的一个有意思的用法。

  Sharing volumes(分享卷)
        There are two ways to share volumes between containers(在容器之间共享卷有两种方法)
              多个容器的卷使用同一个主机目录,例如
                    ~]# docker run -it --name cl -v /docker/volumes/vl:/data busybox
                    ~]# docker run -i t--name c2 -v /docker/volumes/vl:/data busybox

              复制使用其它容器的卷,为docker run命令使用--volumes-from选项
                    ~]# docker run -it --name bboxl -v /docker/volumes/v1:/data busybox
                    ~]# docker run -it --name bbox2 --volumes-from bboxl busybox(使用--volumes-from选项意思是,直接去复制bboxl一个已存在的容器的卷,而不用使用-v来定义了,从而就拥有了一模一样的存储卷设定。)
创建NTM架构
# 创建基础支撑的容器(-it就不必了,因为不用进入容器,为了出效果可以加上-it,甚至不用启动它,-v指定宿主机卷路径,冒号,关联至容器内的路径下,这个是用来提供网页的,后面是我们使用的镜像。这里我们也不用去删除它了,这个底层基础支撑容器千万不要动不动就删除)
[root@localhost ~]# docker run --name infracon -it -v /data/infracon/volume/:/data/web/html busybox
/ # ls /data/web/
html

# 在终端上,在启动一个nginx,它要加入到infracon网络中来,使用--network设置为container指定为哪一个容器,这表示要使用infracon这个容器的网络,--volumes-from指明复制哪个容器的存储卷设置,如果需要交互式进入-it,这容器时可以关闭后删除的,如果又必要的话,就不适用nginx了使用busybox了,代表是一样的,代表一下
[root@localhost _data]# docker run --name nginx --network container:infracon --volumes-from infracon -it busybox
/ # 


# 新终端看一下infracon 容器详细(这个容器的地址是172.17.0.2,使用的存储卷/data/infracon/volume和容器内的/data/web/html这是没问题的,看一下第二个容器的)
[root@localhost ~]# docker inspect infracon
        "Mounts": [
            {
                "Type": "bind",
                "Source": "/data/infracon/volume",
                "Destination": "/data/web/html",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }


                    "IPAddress": "172.17.0.2",


# 看一下nginx容器详细(这里没有显示地址,因为它是joined进来的,在容器内使用ifconfig可以看到,只不过在docker inspect时没有显示出来,存储卷/data/infracon/volume和容器内的/data/web/html这是没问题的,      那因此一个基础支撑容器就能工作起来了,但如果需要也可以创建第二个,按照刚才的规范。tomcat,mariadb等等,都去复制infracon上的设定就可以了,但是呢这里的infracon时我们随机启动的一个容器,其实将来真正做基础架构支撑容器,他们需要一个专门的镜像文件来做,互联网有专门这种镜像文件,就是让你去做基础架构支撑容器的镜像,它不用启动,你只需要创建出来就行。)
[root@localhost ~]# docker inspect nginx
        "Mounts": [
            {
                "Type": "bind",
                "Source": "/data/infracon/volume",
                "Destination": "/data/web/html",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }

dockerfile

  各位先想必应该记得我们之前如果安装一个nginx的话,安装完以后我们说过很多次,通常不会运行在默认配置下,对吧,那因此我们通常需要去改一改他的配置文件,或者定义模块化配置文件,然后启动服务。那为什么nginx的默认配置不符合我们的需要,很显然,不同的生产场景,他们所需要用的参数确实各不相同。因此对方只能用一个默认的认为,适用于大多数普遍常见情形的,或者适用于较小主机资源情形的这么一个设定来启动服务。      同样的逻辑,各位试想象一下,如果我们去,从dockerhub当中,去拖下来一个nginx镜像,去启动nginx容器的时候,请问这个镜像内的nginx容器的配置文件一定就会符合我们的需要吗?应该说一定不会是吧。OK,基本上不可能完全符合我们需要的,可是我们就必须要去改它的配置,那怎么改呢?我们此前曾经做到的,无非就是启动容器,然后使用docker exec连进容器,然后在内部再执行命令是吧,但是有没有发现,这反而更麻烦了。以往可以直接在启动之前在宿主机上编辑就完了,因为我们是在宿主机上启动。现在启动与容器当中我还得先把它启动起来对再去改,改完以后还要reload或者重启。这事儿变得更加麻烦了。      那当然还有另外一种方式,假设我们把它对应的那个配置文件的路径做成存储卷从我们宿主机上加载文件,这个时候我们在宿主机上编辑,也能够让它立即生效。启动之前先把它编辑好,就是容器启动之前,通过事先在宿主机上找一目录把配置文件准备好,然后启动容器时,把那个容器内的应用程序默认加载配置文件的那个路径,与宿主机上的目录建立关联关系。然后去启动容器,它也能够加载到我们在外面定制的这个文件了,对吧,这是第二种方式,我们使用存储卷,那这样子就能够启动完以后就立即直接生效了。而且但这样一来还有坏处在于,我们在宿主机当中所做的编辑,能不能让他立即生效, 直接生效啊?比如我们后来启动完以后发现有些参数还是需要改,改完以后依然需要重启才能生效,对不对,第二种方式,使用存储卷,那还有没有其他方式呢?      当然有一个说简单不简单,但也比较简单的方式,那就是自己做镜像。我们曾经解释过,做镜像的方式当中,常用的有两种,第一种叫基于容器做,是吧,用一个基础镜像,它的配置可能不符合我们需要怎么办呢?我先把它启动起来,交互式连入镜像以后做修改,改完以后他的改的结果一定是保存最上的可写层中的,对吧,这个时候我们就可以给它保存为一个新镜像,然后我们再去创建容器时,根据我们自己所创建的镜像来启动。那这是一种基于容器做镜像的方式。当然不管它基于什么,你可以理解为叫自制镜像的方式,或者自定义镜像的方式来解决问题。基于基于容器只是其中的一种方式,仅此而已。但这种方式也有缺陷,大家发现你做的镜像也是把这个文件直接写死在镜像中的,对吧,那如果我想再改,还是改不了,对吧,运行过程当中去修改配置的需求,可能作为运维来讲,变更不就是日常操作吗,很多时候,也有可能需要随时进行修改,他依然解决不了问题。而且这种费尽镜像的设置方式,最悲惨的地方在什么地方呢?假如现在有三个环境,你懂的,开发,测试,线上,很有可能他们所需要的配置是各不相同的。因为它们的环境规模不一样,对服务器配置也不一样,那使用的设置也不一样。那因此我们需要为此做至少三个镜像。那如果nginx你现 在镜像内用的是1.12版,后来想更新到1.14版呢,三个都要重做,是不是,这才三个环节。而且在同一个环境当中,你别忘了你的nginx有的地方当反代,有的地方当图片服务器,有的是用来当静态内容服务器只是反代了一个tamcat假如说,他们的配置能一样吗?也不一样。于是也成为了一个矩阵了,假如一共有三种环境,每一种环境中需要三个配置,就是M乘N的关系,至少要做九个镜像。一次更新九个。      所以直接把配置备进镜像这种方式显然也是不妥当的。但是为什么我们也需要自制镜像呢?原因很简单,因为还是那句话,别人编译做的通用镜像,未必那个应用程序自己的特性和配置是符合我们需要的,对不对?未必是符合的,所以我们得需要去封装应用程序,更何况很多时候我们去部署在生产环境中的应用程序不是公开的,有可能是我们公司自研的,互联网就没有现成的镜像存在啊。显然需要自制镜像。这种需求都是存在的,所以自制镜像不是为了解决配置不稳定的问题。虽然说在一定程度上能帮我们去解决,假如你的环境确实很简单,就需要我们一点一点简单的配置。那么自制镜像备进配置也不算是什么错误,或者说不能接受的方案。 事实上docker在配置文件这个层级上解决问题的方案是什么呢?      我们一般是这么来做的。就算我们不是为云原生开发的服务,比如以nginx为例,假设无论nginx这个镜像启动为容器以后只做一件事,就是配置一个虚拟主机,提供一个虚拟服务server。我不去提供太多的虚拟主机,但这个虚拟主机,他所服务的主机名叫什么,监听的端口是什么,家目录或者文档根路径在什么地方,在不同的环境中可能各不一样,对吧?但是它的配置文件格式是不是固定的?OK,那既然如此,我们能不能这么做,把它的配置文件生成一个简单的.

  server.conf  /etc/nginx/conf.d/
  {
        server-name $NGX_SERVER_NAME:
        listen $NGX_IP:$NGINX_PORT:
        root $DOC_ROOT:
  }
        比如像server.conf放在什么/etc/nginx/conf.d目录下,把它做成模板,就类似于模板一样,那比如对应的那server,我们要监听的地址server_name我们通过一个变量来获取,就叫做$NGX_SERVER_NAME这个变量名字稍微长一点,主要能做 到见名知意的,好,这是第一个,我们也不用做的过于复杂。      第二listen我们要监听在哪个端口上$NGX_IP:$NGINX_PORT我们要引用这两个变量。最后直接最简单的一个来个root,引用$DOC_ROOT变量完了.假如我们没有更复杂的配置,就这么一些配置。那因此为了让这个配置生成起来更加简单一点,于是我们把这个文件内部的这些,可以接收变量并实现变量替换的方式来实现,什么意思呢?当用户拿着这个容器镜像启动为容器时,我们要先在容器的内部的这个主进程,在容器内的主进程启动之前先启动一个别的程序,这个程序根据这个镜像中的这个文件,以及用户启动为容器时,把镜像启动为容器时向容器传递的环境变量,就这几个环境变量要给他传值。你不给他传值,他有默认值比如$NGINX_PORT他有默认值,ip应该是*表示,或者什么0.0.0.0什么的,没有默认值的,你就必须要传参数才行,就像变量传值那些。而后这个程序就把用户传递进来的每一个变量的值替换在这个文件中,并把它保存为/etc/nginx/conf.d/下的server.conf。保存完了这个程序,在启动主进程,他这个程序就可以退出,我们知道有一个进程启动另外一个进程并替换当前进程,我们使用Exec命令,启动一个子进程,但子进程是把当前进程直接替换掉了,而且顶了当前那个进程的ID号,对不对,就类似这种方式他也能实现,先把他为主进程,临时主进程,是帮忙给他预设环境的,预设完环境以后由真正的主进程它(临时主进程)给覆盖,这就是为什么说,通过环境变量能配置服务的原因。大家发现这么一来也就意味着说我们将来基于一个镜像启动多个容器时,让容器拥有不同配置的状态服务,无非就是向他传变量,而且对应你这个容器启动时还要能处理变量,把变量替换为主进程的配置信息才行。而默认情况下,我们nginx自己并不支持这个功能的。所以我们自己做镜像就来做这个事情,你也得把这个文件模板或者框架准备好,同时这个处理命令,这个处理程序要设置好,处理完以后由她再启动nginx就能做到去替换配置信息,从而实现自定义能够接受参数以配置环境变量传递参数,提供配置信息的这么一个能够更好的应用或者适用于多种不同环境的镜像。这是为什么说对于容器来讲,通过环境变量配置是至关重要的。      那些被称为叫云原生的cloud native(云原生)的应用程序,他们通常就是设置为类似这种,它的配置文件天生就是这种格式的,直接可以接受变量,环境变量,直接替换。      而且更主要的是不说他配置文件天生这种格式,而是说这个程序启动时它完全可以做到不用读配置文件,我直接去加载当前系统之上的环境变量就能获得这个配置了,甚至都不用读配置文件就行,直接支持通过环境变量获取配置,而不通过配置文件。可以做到这种地步,那也就意味着我们就不用再去处理这个文件,去替换到配置文件中了,直接传他就直接能用,就更OK了。当然这只是其中表现之一了。大家有没有发现曾经说过,把应用运行在容器中,在某种程度上来讲不是简单而是更复杂了,对吧,因为配置过程就颠覆了以往用一个所谓的,我们知道linux重要哲学思想之一,叫用文本文件保存程序的配置信息,从而可以使得用一个所谓的文本编辑器就能够做到配置几乎所有的应用程序,对吧,但容器化时代,这种方式就被颠覆了。我不建议去改配置文件,而是通过传环境变量来解决问题。

关于Dockerfile

  About Dockerfile(关于Dockerfile(所以说从这些角度来讲,Dockerfile就是一个纯文本文件里边包含了一些指令而已,这些指令是Dockerfile当中在Dockerfile制作时规定的支持的指令,不过区区一二十个。将来就把这些指令简单的堆起来只要你理解它的逻辑和语法格式,就能够去写Dockerfile))

        Dockerfile is nothing but the source code forbuilding Docker images(Dockerfile只是构建Docker映像的源代码      (Dockerfile意思仅仅是,就是我们用来构建docker镜像的源代。但这源码你不用惊慌,它不是编程的源码,因为它里面只是一些基本的指令,它没有任何跟代码相关的什么控制语句,没有什么条件判断之类的就仅仅是一些文本指令。))

              Docker can build images automatically by reading the instructions from a Dockerfile(Docker可以通过阅读Dockerfile中的指令来自动构建映像)

              A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image(Dockerfile是一个文本文档,其中包含用户可以在命令行上调用以组装映像的所有命令)

              Using docker build users can create an automated build that executes several command-line instructions in succession(使用docker build的用户可以创建自动执行的构建,该构建可以连续执行多个命令行指令)

Dockerfile格式

  Dockerfile Format(Dockerfile格式)
        Format(格式(其实整个Dockerfile 就两类语句组成。))
              # Comment(#注释(注释信息,#开头。))
              INSTR UCTION arguments(INSTR UCTION参数(指令,加指令的参数。一行一个指令,一般是这样子的。而指令自身一行写不下,你可以续行没问题,但一次通常只给一个指令。))

        The instruction is not case-sensitive(该指令不区分大小写)
              However, convention is for them to be UPPERCASE to distinguish them from arguments more easily(但是,约定是大写,以便更轻松地将它们与参数区分开)

        Docker runs instructions in a Dockerfile in order(Docker按顺序在Dockerfile中运行指令)

        The first instruction must be “FROM” in order to specify the Base Image from which you are building(第一条指令必须是“FROM”,以指定要从中构建的基础映像(所以第一个非注释行,必须要是FROM指令,它用来指定做当前镜像要基于哪个基础镜像来实现。所以我们所有镜像制作过程都是建立在某个已存在的已有的镜像基础之上,如果没有你用dockerfile是做不了的,我们必须要手动去去做,那个就麻烦大了。))

  当我们去做dockerfile时,他应该是这样的,大家知道我们在使用操作系统的时候,每时每刻都应该处在某个目录下,我们把它称为叫工作目录,也叫当前目录,对不对,我们做docker镜像时不一定非得是当前目录,但它必须在某一个特定的目录下进行,要做docker镜像,基于dockerfile来做,他的工作格式是,找一个专门的目录,专用目录,在这个目录中放进了你的dockerfile。而且docker file文件首字母必须大写,这是第一个要求,还有工作目录专用的。      第二里面得有一个docker file文件首字母要大写,就是文件名首字母大写的dockerfile文件。接着如果在dockerfile中说我们要向镜像内打包进去很多很多很多很多很多文件,那这些文件你必须做好以后放在当前工作目录下,比如说我们基于dockerfile去做镜像时,它所引用的文件的路径不能是这个工作目录的父目录,只能是基于起始于自己这个目录往下走的路径。因此你的所有文件都得先拿进来,如果你要用到的话,比如我们要安装一个二rpm包打算,你rpm包放这,提供的配置文件也放到这儿,等等等等都在这儿,可以使他子目录中的内容,但不能使父目录,不能超出这个目录边界。那如果我们这里边放进了很多文件,也可以这样子做。比如我打算把很多文件都包含进去,那有可能我做个目录,子目录,然后呢我说把这个子目录给你包含进来就可以了。这个子目录比如有一百个文件,但这一百个文件当中其实有三个,假设啊就是假设就有三个,我们认为说没必要放进来,那既然它是用不到的文件放进来,我们的镜像文件突然增大了,还浪费用户上传下载时的这个带宽,对不对,还浪费我们的存储体系。那既然如此,我要一个一个包含吗,得写九十七个,我要写个目录吧,这三个又不能都包含进来,怎么办?OK,dockerfile还支持在这底下做一个隐藏文件。注意做一个单独的专用的隐私文件,叫dockerignore,你懂的,ignore叫忽略。      

dockerignore文件

  dockerignore file(dockerignore文件)

        Before the docker CLI sends the context to the docker daemon, it looks for a fle named .dockerignore in the root directory of the context(在Docker CLI将上下文发送到docker守护程序之前,它将在上下文的根目录中查找名为.dockerignore的文件。)

        If this file exists, the CLI modifies the context to exclude files and directories that match patterns in it(如果此文件存在,则CLI会修改上下文以排除与其中的模式匹配的文件和目录)

        The CLI interprets the .dockerignore file as a newline-separated list of patterns similar to the file globs of Unix shells(CLI将.dockerignore文件解释为以换行符分隔的模式列表,类似于Unix shell的文件组)

              它又是一个文本文件,在这个文本文件中你可以写进了一些文件路径,一行一个一般,当然虽然叫一行一个,但是这一行当中我们可以使用通配符,它就意味着可以代表多个了。dockerignore以为这什么呢?所有在打包时的文件中,但凡写在dockerignore文件中的那个路径,在打包时都不包含进去。所以把它排出去就相当于清单,叫排除名单或者文件排除列表,从而把这些路径指示的文件排除在外了。      那于是接下来我们就可以使用docker命令,针对这一目录,通过读取这个dockerfile来做docker镜像了,而这个命令叫docker build,做好镜像打好标签,推到仓库里边儿就可以用。我们这里在做镜像,就不需要事先基于基础镜像先启动容器了。      但是要注意的是docker build这个过程,他只不过是代替我们,完成一个步骤,大家记得我们刚才说过,制作镜像的方式当中有一种方式是基于容器来制作,对吧,其实大家知道容器是怎么启动的,无非就是基于镜像启动嘛,联合挂载,然后呢在上面添加一个可写层,你的所有写操作都放在这个可写层内容当中了,然后我们做镜像时,就把它保存起来就行了。那很显然大家知道,我们在可写层上所执行的写操作,是不是要交互式连进这个容器里边来执行命令来完成的。那请问当你交互式接入这个容器里边来执行命令的时候,这个命令到底是你宿主机的命令,所有可执行命令,还是你容器里的所有可执行命令?容器的。所以我们这里边所执行的所有shell命令,应该是你的容器自身支持存在的可执行程序文件,那么其中命令。那同样的逻辑docker build或者基于dockerfile做镜像时,你不用自己启动容器了。但这个过程是由谁来完成的?docker build来完成的,其实docker build还要自己隐藏式的起一个容器,它的做法跟我们人工起一个容器效果其实没什么区别,只不过你不需要人工的显示启动而已。      那么因此在dockerfile中我们可以执行很多shell命令的,但这个shell命令,不是你宿主机的命令,而是底层这个镜像中所包含的命令。镜像中如果没有这个命令,你想在dockerfile中去运行一些shell程序是做不到的,这个先跟大家说清楚。所以所有制作环境是底层镜像启动容器时所能够提供给我们的环境。      好,那有这些概念,基本上就能够来了解怎么做镜像了。而制作镜像过程当中,我们还可以使用环境变量。

环境变量

  Environment replacement(环境变量)

        Environment variables (declared with the ENV statement) can also be used in certain instructions as variables to be interpreted by the Dockerfile(环境变量(用ENV语句声明)也可以在某些指令中用作由Dockerfile解释的变量)

        Environment variables are notated in the Dockerfile either with $variable name or ${variable name}(环境变量在Dockerfile中使用$ variable name或$ {variable name}标记)

        The ${variable name} syntax also supports a few of the standard bash modifiers($ {variable name}语法还支持一些标准的bash修饰符)

              ${variable:-word}; indicates that if variable is set then the result will be that value. If variable is not set then word will be the result.($ {variable:-word};表示如果设置了变量,那么结果将是该值。 如果未设置变量,则单词为结果。)

              ${variable:+word}; indicates that if variable is set then word will be the result,otherwise the result is the empty string.($ {variable:+ word};表示如果设置了变量,则word将为结果,否则结果为空字符串。)

                    那既然能用环境变量,他也能做环境变量替换。注意这个环境变量可不是启动镜像为容器所用的环境变量,这里的环境变量是我们使用docker build做镜像时可用的环境变量。因为毕竟做镜像就要基于镜像先启容器来实现,那起容器就可以叫容器传回变量,而在docker build或者叫dockerfile中,能用到的环境变量跟我们shell环境变量非常相像无非就是配个名儿,加$符来引用,赋值时,变量名直接等于赋一值,引用时有两种格式,加不加花括号都行,什么时候加?在bash中应该学过。      而后这里还支持bash中的两种变量替换的特殊格式${variable:-word}和 ${variable:+word}。      第一种格式是variable:冒号-减word加一个字符串,这意味着什么,表示如果这个变量variable未设置,未初始化,就表示没有这个变量,或者有这个变量但他的值为空,则表示我要引用的是word这个子串,意思是你没有值没关系,我给你设一个默认值,就相当于这个变量有个默认值。默认值就是由word字串所表示的内容,这是用的最多的一种方案。各位在写shell脚本时,这是一个从外部从其他地方加载变量作为我们内容,在其他地方可能不存在的时候,一个特别常见的用法,比如我们认为我们可以引用一个变量叫做$NAME的时候,但是这个变量可能别人没有声明,这个变量的值就为空了,怎么办,可以使用:-tom,显示的就是tom。      但如果NAME有值的等于jerry,再次使用的时候他就不用tom这个默认值了,而使用jerry这个值,这种高级用法,+号的意义刚好与-号的意义相反。意思是这个变量如果有值,这显示tom,变量没值那就没了,那就算了,就是要判定这个变量到底是不是为空,或者是未设定。
                          [root@localhost ~]# echo ${NAME:-tom}
                          tom
                          [root@localhost ~]# NAME=jerry
                          [root@localhost ~]# echo ${NAME:-tom}
                          jerry

Dockerfile 使用说明

FROM

  Dockerfile Instructions(Dockerfile 使用说明)

        FROM(这是Dockerfile Instructions(使用说明)的第一条指令)
              FROM指令是最重要的一个且必须为Dockerfile文件开篇的第一个非注释行,用于为映像文件构建过程指定基准镜像(就表示我要基于哪个镜像来做),后续的指令运行于此基准镜像所提供的运行环境

              实践中,基准镜像可以是任何可用镜像文件,默认情况下, docker build会在docker主机上查找指定的镜像文件,在其不存在时,则会从Docker Hub Registry上拉取所需的镜像文件(当然也可能不是docker hub因为如果你指明了repository服务器地址那他有可能是别的Registry)

                    如果找不到指定的镜像文件, docker build会返回一个错误信息

        Syntax(句法(FROM的语法格式是))
              FROM <repository>[:<tag>]或(FROM <repository>指明在哪个仓库中,未指明默认就是docker hub,[:<tag>]加载哪个镜像,这个标签是可省的,如果用名字引用镜像的话,如果使用的话表示使用这个仓库中标签为latest的镜像)
              FROM <resository>@<digest>(<resository>指明仓库,@用艾特打头指明镜像的哈希码也可以引用镜像)
                    <reposotiry>: 指定作为base image的名称;
                    <tag>: base image的标签,为可选项,省略时赎认为latest;
# 创建工作目录(文件首字母一定要大写)
[root@localhost ~]# ls
anaconda-ks.cfg
[root@localhost ~]# mkdir img1
[root@localhost ~]# cd img1/
[root@localhost img1]# ls
[root@localhost img1]# vi Dockerfile
# Description: test image      # 加个注释,意思是这是什么镜像,加个Description(描述)
FROM busybox:latest      # 写第一条FROM,给仓库名,仓库名其实就是镜像应用程序名是吧,比如基于busybox来做,后面加标签,标签不加默认就是latest,当然也可以使用FROM busybox@后面跟上哈希码什么的,建议使用哈希码的这种格式更安全一点,但是我们基于这个基础镜像接下来要做什么呢?后续是需要在这个镜像基础之上做真正有效的做改进的、定制的操做,那后续指令才最关键。

MAINTANIER

  已经废弃了,但是在很多的别人写的这个Dockerfile中,你还是能看到的,依然有人在用。这是一个可选选项,但大多数人都会附带上去。不过他已经被depreacted了,没关系我们可以有其他办法来提供,意思是他要替换成另外一个指令叫LABLE指令,LABLE是标签,而对标签来讲,它可以提供各种各样的kv信息,作者只是其中一项。所以它MAINTANIER有了更宽泛的使用领域。

  MAINTANIER (depreacted不推荐使用)      (MAINTANIER表示镜像的维护者,镜像作者 )
        用于让Dockerfile制作者提供本人的详细信息

        Dockerfile并不限制MAINTAINER指令可在出现的位置,但推荐将其放置于FROM指令之后

  Syntax(语法)
        MAINTAINER <authtor's detail>

              <author's detail>可是任何文本信息,但约定俗成地使用作者名称及邮件地址

              MAINTAINER "magedu<mage@magedu.com>"(格式是magedu 名称加<mage@magedu.com>联系方式)

LABEL

  在较新版本的docker上应该是docker1.7以后的版本吧,如果你在centos7上使用,就是用yum install安装的docker应该是支持的

  The LABEL instruction adds metadata to an image(LABEL指令将元数据添加到镜像(而LABEL它是用让用户去为一个镜像指定各种各样的元数据,这个元数据都是键值对儿,kv格式,对吧))
        Syntax: LABEL <key>=<value> <key>=<value> <key>=<value>...(语法:LABEL <键> = <值> <键> = <值> <键> = <值> ...(可以有多个kv项))

        The LABEL instruction adds metadata to an image.(LABEL指令将元数据添加到镜像)

        A LABEL is a key-value pair.(LABEL是键值对。)

        To include spaces within a LABEL value, use quotes and backslashes as you would in command-line parsing.(要在LABEL值中包含空格,请像在命令行分析中一样使用引号和反斜杠。)

        An image can have more than one label.(一个镜像可以有多个标签。)

        You can specify multiple labels on a single line.(您可以在一行上指定多个标签。)
# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"      # 在新版中应该是兼容MAINTANIER的,我们指明用户信息和联系方式
# LABEL maintainer="haoran <haoran@haorankeji.com>"      # 也可以写成LABEL,后面跟上maintainer(这个值可以自己随便写)等于用户信息和联系方式,我们只是用一个就行,把这个注释掉,这是第二个指令,这两个指令其实都没有真正的去做镜像的意义。这里先不演示

COPY

  copy指令是用来做什么,各位应该从名字就知道,复制文件的,说白了就是把宿主机当中的文件给它打包提供到目标镜像中去。就相当于从宿主机的当前工作目录当中把某一个文件或者某些个文件复制到目标镜像的文件系统中。

  COPY
        用于从Docker主机复制文件至创建的新映像文件

        Syntax(语法)
              COPY <src>...<dest>或      (<src>复制源文件,<dest>为哪个目标文件)
              COPY ["<src>"..."<dest>"]      (或者使用列表格式给出["<src>"...指定N个源文件,"<dest>"目标文件,目标文件路径一般是绝对路径。因为你的源文件是你的工作目录,而dest则是指生成的目标镜像上的路径,那这个路径我从当前是找不到的,对不对,所以我们必须要给他使用绝对路径。如果想使用相对路径,只能是以后面讲的另外一个dockerfile中的指令WORKDIR所指明的目录为起始路径,否者我们就不能使用相对的)
                    <src>:要复制的源文件或目录,支持使用通配符      (源文件路径也支持通配符,源文件路径一般是相对路径)
                    <dest>: 目标路径,即正在创建的image的文件系统路径;建议为<dest>使用绝对路径,否则,COPY指定则以WORKDIR为其起始路径;
              注意:在路径中有空白字符时,通常使用第二种格式      (另外如果路径中无论是src还是dest中间有空白字符时,通常使用第二种格式,因为使用引号引用起来的不易产生歧义,否则有空白字符隔开的两个字串会被当作是两个文件。)

        文件复制准则(就像我们之前在linux之上学copy命令一样,这里有几个基本复制准则。)
              <src>必须是build上下文中的路径,不能是其父目录中的文件      (就是必需时工作目录,向下的所有子目录,不能是工作目录向上的父目录)

              如果<src>是目录,则其内部文件或子目录会被递归复制,但<src>目录自身不会被复制      (如果<src>是目录,则递归复制目录中的文件,不需要加-R选项,但是,但是,但是<src>目录自身不会被复制,意思是他把src中的文件复制过去了。)

              如果指定了多个<src>,或在<src>中使用了通配符,则<dest>必须是一个目录,且必须以/结尾      (这个容易理解,只要源有多个,目标必需要是目录,因为不可能将多个文件在复制时合成一个对吧,而且这里有特殊要求目录必需以/结尾吗,用来标识它就是目录,不加斜线是不对的,是会报错的,是语法错误)

              如果<dest>事先不存在,它将会被自动创建,这包括其父目录路径
复制文件
# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"
# LABEL maintainer="haoran <haoran@haorankeji.com>"
COPY index.html /data/web/html/      # 使用COPY指令,复制当前系统上,假如我有一个文件叫index.html复制到/data/web/html/,这个斜线一定要加。这个文件要在你当前工作目录中存在

# 在工作目录创建文件(现在有了这个文件,一定要放在你的工作目录,就给你的dockerfile所在为同一个目录,或者是dockerfile所在同一个目录的子目录中,否则任何其他路径都不行。      接着我们就可以做镜像了,有一个指令就够了,里面能复制文件,就能够解决一个定制的问题了。)
[root@localhost ~]# ls
anaconda-ks.cfg  img1
[root@localhost ~]# cd img1/
[root@localhost img1]# vi index.html
<h1>Busybox httpd server.</h1>


# 制作镜像(-t我们可以直接打标签,前面是仓库名,后面是标签名 ,然后指定目录./当前目录)
[root@localhost img1]# docker build -t tinyhttpd:v0.1-1 ./ 


# 查看镜像(它里面应该有我们打包进来的文件的)
[root@localhost img1]# docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
tinyhttpd            v0.1-1              e0177de0e900        3 minutes ago       1.23MB

# 基于这个镜像,起一个容器验证一下(这个镜像默认是启动shell程序的,我们只是要看看有没有这个目录,看一下目录在不在就行了,没必要进入交互式模式, 可以在这个后面写一个命令,在docker run的时候可以改变镜像中默认使用命令,转而运行别的命令,比如cat一下/data/web/html/index.html注意这是容器内的,这个命令看完他就结束了,进程一推出容器就停了,容器一停就--rm了)
[root@localhost img1]#  docker run --name tinyweb1 --rm tinyhttpd:v0.1-1 cat /data/web/html/index.html
<h1>Busybox httpd server.</h1>

复制目录
# 复制一个目录到当前工作目录,目录中有很多文件(把yum.repos.d下的所有文件打包进目标镜像中,也放在/etc的/yum.repos.d/目录下,)
[root@localhost img1]# cp -r /etc/yum.repos.d/ ./
[root@localhost img1]# ls yum.repos.d/
CentOS-Base.repo  CentOS-Debuginfo.repo  CentOS-Media.repo    CentOS-Vault.repo
CentOS-CR.repo    CentOS-fasttrack.repo  CentOS-Sources.repo  docker-ce.repo

# 编辑dockerfile,添加copy指令,copy指令可以有多个没有问题,可以连续用多次,但是呢在这儿开始要需要强调一个概念,在dockerfile中请惜字如金,意思是你的每一条指令都会生成一个新的镜像层,一条指令一层,一条指令一层什么概念,你明白的,因此如果可以把两个指令合成一条,一定要写成一条,而不要写成多条。千万要记得,层越多,很显然,那将来联合挂载时,效率也差是不是。但是我们这两条COPY因为它的目标不一样,所以只能写成多个。
[root@localhost img1]# vi Dockerfile
# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"
# LABEL maintainer="haoran <haoran@haorankeji.com>"
COPY index.html /data/web/html/
COPY yum.repos.d /etc/yum.repos.d/      # COPY当前工作目录(目标目录)yum.repos.d这是目录,当你指定目录的时候。他不是复制目录本身,复制目录下的文件。复制到/etc/下yum.repos.d/因为目录名需要存在,以斜线结尾。

# 在build一次构建镜像,版本0.1-2
[root@localhost img1]# docker build -t tinyhttpd:v0.1-2 ./ 
Sending build context to Docker daemon   21.5kB
Step 1/4 : FROM busybox:latest
 ---> f0b02e9d092d
Step 2/4 : MAINTAINER "haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> 3f686ae096ac
Step 3/4 : COPY index.html /data/web/html/
 ---> Using cache
 ---> e0177de0e900
Step 4/4 : COPY yum.repos.d /etc/yum.repos.d/      (做到第四步,COPY这个目录中的文件)
 ---> da462eb75b0d
Successfully built da462eb75b0d
Successfully tagged tinyhttpd:v0.1-2


# 验证一下,这次是v0.1-2镜像,看一下这个目录下的所有文件,它的确是复制了这个目录中的所有文件过去,一定要记得,如果源有多个,目标已经要是目录
[root@localhost img1]#  docker run --name tinyweb1 --rm tinyhttpd:v0.1-2 ls /etc/yum.repos.d/
CentOS-Base.repo
CentOS-CR.repo
CentOS-Debuginfo.repo
CentOS-Media.repo
CentOS-Sources.repo
CentOS-Vault.repo
CentOS-fasttrack.repo
docker-ce.repo

ADD

  它的功能跟copy很相似,也无非是把宿主机上的文件或目录打包进目标镜像文件中,但是ADD支持URL路径,意思是如果你的宿主机在执行docker build过程中,能访问到网络上了某服务器,它可以自动基于URL的方式把你所指定的文件,引用,下载到本地并打包进镜像文件中。      第二个,当我们使用ADD指令时,如果后面跟的要复制的源是一个tar.gz的文件,就是tar文件,不一定是.gz,他也可能是xz之类的,只要你的tar打包或者压缩的文件,就是打包后压缩的或者只打包没压缩的文件,那么它会自动把这个文件展开为目录。这是ADD所具有而COPY所不具备的能力。但除此之外,余下的ADD跟COPY都一样。不过这里还要特别强调的是,如果ADD所引用的tar文件不是本地的,是通过url获取到的tar文件讲不会自动展开。

  ADD
        ADD指令类似于COPY指令, ADD支持使用TAR文件和URL路径
        Syntax
              ADD <sre>...<dest> 或
              ADD ["<src>"..."<dest>"]
  操作准则
        同COPY指令

        如果<rc>为URL且<dest>不以/结尾,则<src>指定的文件将被下载并直接被创建为<dest>; 如果<dest>以/结尾,则文件名URL指定的文件将被直接下载并保存为<dst>/<filename>

        如果<sre>是一个本地系统上的压缩格式的tar文件,它将被展开为一个目录,其行为类似于"tar -x"命令;然而,通过URL获取到的tar文件将不会自动展开;
        如果<sre>有多个,或其间接或直接使用了通配符,则<dest>必须是一个以/结尾的目录路径;如果<dest>不以/结尾,则其被视作一个普通文件, <sre>的内容将被直接写入到<dest>;
验证远程url下载到本地tar文件,没有展开
# 复制一个tar打包的文件链接,写到dockerfile中
[root@localhost img1]# vi Dockerfile
# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"
# LABEL maintainer="haoran <haoran@haorankeji.com>"
COPY index.html /data/web/html/
COPY yum.repos.d /etc/yum.repos.d/
ADD http://nginx.org/download/nginx-1.19.4.tar.gz.asc /usr/local/src/      # 使用ADD,把url路径写到这里,他支持使用url,但是他自会下载不会展开的,如果是本地就会展开,复制到/usr/local/src/目录下,这个目录即便不存在,他也是会创建的

# 在build一次构建镜像,版本0.1-3
[root@localhost img1]# docker build -t tinyhttpd:v0.1-3 ./ 
Sending build context to Docker daemon   21.5kB
Step 1/5 : FROM busybox:latest
 ---> f0b02e9d092d
Step 2/5 : MAINTAINER "haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> 3f686ae096ac
Step 3/5 : COPY index.html /data/web/html/
 ---> Using cache
 ---> e0177de0e900
Step 4/5 : COPY yum.repos.d /etc/yum.repos.d/
 ---> Using cache
 ---> da462eb75b0d
Step 5/5 : ADD http://nginx.org/download/nginx-1.19.4.tar.gz.asc /usr/local/src/      (可以看到第五个过程,下载文件)
Downloading [==================================================>]     455B/455B
 ---> eb5207ff4464
Successfully built eb5207ff4464
Successfully tagged tinyhttpd:v0.1-3

# 验证一下(可以看到没有被展开)
[root@localhost img1]# docker run --name tinyweb1 --rm tinyhttpd:v0.1-3 ls /usr/local/src
nginx-1.19.4.tar.gz.asc

验证本地tar文件,并展开
# 先人为的在宿主机上将tar文件下载下来
[root@localhost img1]# wget http://nginx.org/download/nginx-1.19.4.tar.gz

# 放在我们build 的上下文中,就是工作目录中
[root@localhost img1]# ls
Dockerfile  index.html  nginx-1.19.4.tar.gz  yum.repos.d

# 编辑dockerfile
# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"
# LABEL maintainer="haoran <haoran@haorankeji.com>"
COPY index.html /data/web/html/
COPY yum.repos.d /etc/yum.repos.d/
# ADD http://nginx.org/download/nginx-1.19.4.tar.gz.asc /usr/local/src/      # 现注释掉add下载url文件
ADD nginx-1.19.4.tar.gz /usr/local/src/      # 换成add本地的文件,只写文件名,复制到目录/usr/local/src/,他会展开放到这个目录当中

# 再次docker build,版本是0.0-4
[root@localhost img1]# docker build -t tinyhttpd:v0.1-4 ./ 
Sending build context to Docker daemon  1.077MB
Step 1/5 : FROM busybox:latest
 ---> f0b02e9d092d
Step 2/5 : MAINTAINER "haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> 3f686ae096ac
Step 3/5 : COPY index.html /data/web/html/
 ---> Using cache
 ---> e0177de0e900
Step 4/5 : COPY yum.repos.d /etc/yum.repos.d/
 ---> Using cache
 ---> da462eb75b0d
Step 5/5 : ADD nginx-1.19.4.tar.gz /usr/local/src/
 ---> e0ddf15436d1
Successfully built e0ddf15436d1
Successfully tagged tinyhttpd:v0.1-4

# 验证一下,命令一样,改一下镜像版本,可以看到nginx-1.19.4已经是目录了,可以跟上目录
[root@localhost img1]# docker run --name tinyweb1 --rm tinyhttpd:v0.1-4 ls /usr/local/src
nginx-1.19.4

[root@localhost img1]# docker run --name tinyweb1 --rm tinyhttpd:v0.1-4 ls /usr/local/src/nginx-1.19.4
CHANGES
CHANGES.ru
LICENSE
README
auto
conf
configure
contrib
html
man
src

WORKDIR

  这个指令容易理解,就是用来指明工作目录的,它可以指多次,每一次只影响从他开始向后的这些指令。比如说像刚才我们在复制时使用的/usr/local/src这个目录,假如把他当做工作目录来用。

  WORKDIR
        用于为Dockerfile中所有的RUN, CMD, ENTRYPOINT, COPY和ADD指定设定工作目录

        Syntax(语法)
              WORKDIR <dirpath>
                    在Dockerfile文件中。 WORKIDIR指令可出现多次,其路径也可以为相对路径,不过,其是相对此前一个WORKIDIR指令指定路径
                    另外, WORKIDIR也可调用由ENV指定定义的变量

              例如
                    WORKDIR /var/log
                    WORKDIR $STATEPATH
# 编辑dockerfile文件
[root@localhost img1]# vi Dockerfile
# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"
# LABEL maintainer="haoran <haoran@haorankeji.com>"
COPY index.html /data/web/html/
COPY yum.repos.d /etc/yum.repos.d/
# ADD http://nginx.org/download/nginx-1.19.4.tar.gz.asc /usr/local/src/
WORKDIR /usr/local/       # 这里指定/usr/local/src为工作目录,当你的指令做操作时,如果做了所谓的相对路径引用,逆序往上找WORKDIR,离他最近的,找到第一个WORKDIR所指定位置就是他的工作目录。WORKDIR可以指定多个

ADD nginx-1.19.4.tar.gz ./src     # ADD指定的目录路径就可以是当前目录,就相当于/usr/local/src这个目录

VOLUME

  VOLUME叫卷,你懂的,它可以让镜像中直接定义好卷,只要你基于这个镜像启动容器自动拥有卷,但这儿只能设置容器中的卷目录,指定的格式,我们曾经讲过卷有两种格式,绑定挂载卷和docker管理的卷。其中绑定挂载卷有一特点叫既能指容器中的路径,又能明确指宿主机上的目录,对吧,但是在dockerfile中的镜像中,自动指定卷时,我们一般只能指定挂载点,而不能指定宿主机上的,所以它只能使用叫做docker管理的卷。

  VOLUME
        用于在image中创建一个挂载点目录,以挂载Docker host上的卷或其它容器上的卷

        Syntax(语法)
              VOLUME <mountpoint> 或
              VOLUME ["<mountpoint>"]

        如果挂载点目录路径下此前在文件存在, docker run命令会在卷挂载完成后将此前的所有文件复制到新挂载的卷中
# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"
# LABEL maintainer="haoran <haoran@haorankeji.com>"
COPY index.html /data/web/html/
COPY yum.repos.d /etc/yum.repos.d/
# ADD http://nginx.org/download/nginx-1.19.4.tar.gz.asc /usr/local/src/
WORKDIR /usr/local/src

ADD nginx-1.19.4.tar.gz ./

VOLUME /data/mysql/      # 将/data/mysql要跟宿主机建立起关联关系,之后我们在这个容器内,如果定义的是mysql,可以把这个指定为自己的数据存放目录


# 构建镜像

[root@localhost img1]# docker build -t tinyhttpd:v0.1-5 ./ 
Sending build context to Docker daemon  1.077MB
Step 1/7 : FROM busybox:latest
 ---> f0b02e9d092d
Step 2/7 : MAINTAINER "haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> 3f686ae096ac
Step 3/7 : COPY index.html /data/web/html/
 ---> Using cache
 ---> e0177de0e900
Step 4/7 : COPY yum.repos.d /etc/yum.repos.d/
 ---> Using cache
 ---> da462eb75b0d
Step 5/7 : WORKDIR /usr/local/
 ---> Running in d570b67953f9
Removing intermediate container d570b67953f9
 ---> ceef56776e73
Step 6/7 : ADD nginx-1.19.4.tar.gz ./src      (刚才那个ADD指令,我们使用./src成功了,因为他是相对于WORKDIR指定的工作目录来指定的)
 ---> db9b7a9e906a
Step 7/7 : VOLUME /data/mysql/      (卷,关联目录,没有问题)
 ---> Running in 330c8f76f2f0
Removing intermediate container 330c8f76f2f0
 ---> 471f49a749ed
Successfully built 471f49a749ed
Successfully tagged tinyhttpd:v0.1-5

# 启动容器查看效果,看看他是不是一个卷,在内部使用mount命令,看看有没有我们指定的/data/mysql。      也可以先让容器执行sleep,后睡60秒,睡会,使用docker inspect查看mounts显示的更清晰。      而我们在启动容器时有没有发现,压根就没有指定-v之类的对不对,因此,只要镜像中定义了存储卷,事实上当你创建为容器时,它会自动拥有相关的存储卷。
[root@localhost img1]# docker run --name tinyweb1 --rm tinyhttpd:v0.1-5 mount
/dev/mapper/centos-root on /data/mysql type xfs (rw,seclabel,relatime,attr2,inode64,noquota)


[root@localhost img1]# docker inspect tinyweb1
        "Mounts": [
            {
                "Type": "volume",
                "Name": "2b6bcdc258debd9eb4dbba1ac05dbfca0aed85a1be19f8c400ea95816554b8e2",
                "Source": "/var/lib/docker/volumes/2b6bcdc258debd9eb4dbba1ac05dbfca0aed85a1be19f8c400ea95816554b8e2/_data",      (关联的宿主机的卷)
                "Destination": "/data/mysql",      ( 容器内的/data/mysql)
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],

EXPOSE

  说白了就是我们之前使用docker run -p 暴露端口,想等于我们使用NAT桥时,把容器中的服务监听的端口开放给宿主机之外的其他客户端访问的,相当于自动给我们生成了一个DNAT规则,而我们刚才演示过程中一直没有使用-p 去暴露端口,但事实上我们也可以在镜像当中,如果这个镜像中运行的应用,将来是要监听某一服务向外提供服务的应用程序。而后我们也期望它能够在主机之外的位置被访问到放到,那我们可以直接在里面指定暴露端口。而语法格式也非常简单

  EXPOSE
        用于为容器打开指定要监听的端口以实现与外部通信

        Syntax(语法)
              EXPOSE <port>[/<protocol] [<port>[<protocol>]...]      (我们使用EXPOSE指明 <port>暴露谁,但是你只能指明暴露谁,而且他是动态绑定的,也就是说他要绑定的宿主机的哪个地址的哪个端口上,你是没法指的。首先这个镜像我们将来启动为容器时在哪个宿主机上,是不确定的,对不对,所以你没法指定地址,因为在哪个宿主机上都没定,宿主机上有什么地址就更不定了,这是第一个。第二宿主机有哪些端口是空闲的,你能知道吗?因此这儿我们仅能指定自己暴露什么端口,而后它需要动态绑定至宿主机的随机端口和所有地址。      但是有时候可能这种可能性,虽然容器中的应用一定会提供监听端口的功能。比如我们容器中运行的是个httpd,他监听的是80,但是请问 容器时,他一定需要暴露到外部去吗?之前说过一个容器中的服务可以被当前宿主机之上,工作在同一个桥上的其他容器访问没问题的,你可以不用暴露的,因为大家在同一个私网中,对不对,甚至他也能够被你的宿主机,就是docker host所访问,这是没问题的。所以说如果你需要暴露到外部去,那他应该暴露。      那如果不需要暴露呢,比如我基于这个镜像启动一个容器,我没打算暴露,只是为了当前主机上访问的时候,那你写在镜像中,直接暴露,这反而是一种危险操作,不是吗?所以说大家一定要记得,我们写在这个文件中的端口暴露并不会直接暴露,我们是只说你可以暴露他,但不会直接暴露的。什么时候暴露呢?      大家还记得我们说docker run时有一个杠大P选项(-P),你加上-P不用指谁了,它自动读取镜像中,你指明的暴露哪个端口,就能给你暴露出去了。只需要指定杠大P就完了,不需指 别的。      因为杠大P表示暴露所有要暴露的端口。)
                    <protocol>用于指定传输层协议,可为ucp或udp二者之一,默认为TCP协议

        EXPOSE指令可一次指定多个端口(也可以暴露多个端口),例如
              EXPOSE 11211/udp 11211/tcp
# 编辑dockerfile
# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"
# LABEL maintainer="haoran <haoran@haorankeji.com>"
COPY index.html /data/web/html/
COPY yum.repos.d /etc/yum.repos.d/
# ADD http://nginx.org/download/nginx-1.19.4.tar.gz.asc /usr/local/src/
WORKDIR /usr/local/

ADD nginx-1.19.4.tar.gz ./src

VOLUME /data/mysql/

EXPOSE 80/TCP      # 不加协议默认也是tcp


# 在重新做镜像
[root@localhost img1]# docker build -t tinyhttpd:v0.1-6 ./ 
Sending build context to Docker daemon  1.077MB
Step 1/8 : FROM busybox:latest
 ---> f0b02e9d092d
Step 2/8 : MAINTAINER "haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> 3f686ae096ac
Step 3/8 : COPY index.html /data/web/html/
 ---> Using cache
 ---> e0177de0e900
Step 4/8 : COPY yum.repos.d /etc/yum.repos.d/
 ---> Using cache
 ---> da462eb75b0d
Step 5/8 : WORKDIR /usr/local/
 ---> Using cache
 ---> ceef56776e73
Step 6/8 : ADD nginx-1.19.4.tar.gz ./src
 ---> Using cache
 ---> db9b7a9e906a
Step 7/8 : VOLUME /data/mysql/
 ---> Using cache
 ---> 471f49a749ed
Step 8/8 : EXPOSE 80/TCP
 ---> Running in b956b8fe5071
Removing intermediate container b956b8fe5071
 ---> 78502fe4c104
Successfully built 78502fe4c104
Successfully tagged tinyhttpd:v0.1-6

# 基于这个镜像来启动服务(不运行镜像默认程序而运行我们指定的httpd程序,-f并把它运行在前台,-h指定工作目录,现在应该已经监听了80端口了,可以访问一下试试)
[root@localhost img1]# docker run --name tinyweb1 --rm tinyhttpd:v0.1-6 /bin/httpd -f -h /data/web/html


# 验证一下,并访问一下试试(这个容器的ip地址是172.17.0.3,访问测试没有问题,web服务正常响应了,因为我们说过正常情况下你的容器本身就能够被宿主机访问,因为大家在同一个桥上而且有相关地址,对不对,这是没有问题的)
[root@localhost img1]# docker inspect tinyweb1
                    "IPAddress": "172.17.0.3",


[root@localhost img1]# curl 172.17.0.3
<h1>Busybox httpd server.</h1>


# 查看暴露端口(可以看到此时我们没有暴漏端口,所以说你就算在镜像中指定了暴露端口,他也未必真暴露了。我们把刚才那个容器给它停掉,再重新加上一个选项就可以暴露了。)
[root@localhost img1]# docker port tinyweb1

# 杀掉容器,重启启动,加上-大写P选项,暴露端口,并查看监听的端口(如果容器跑不起来,重启容器),可以看到端口监听在32768,我们可以使用宿主机ip加上32768端口访问了.      http://192.168.162.139:32768/ 访问正常。      有没有发现我们使用杠大P指定暴露的端口时压根儿没指定暴露那个端口,对吧,因为镜像中有,所以它知道暴露哪个      所以在你没有指定要暴露时,他只是说叫待暴露的端口,而不会真正暴露。      除非你在运行容器时加上杠大P选项。      当然我们基于这样的镜像启动容器时,你也可以在额外使用杠小p暴露其他端口,比如像暴露镜像中没定义的端口,也是可以的。      所以镜像中指定的暴露端口叫默认暴露端口。你不想暴漏默认的额外去自定义也是没问题的。
[root@localhost img1]# docker kill tinyweb1
tinyweb1


[root@localhost img1]# docker run --name tinyweb1 --rm -P  tinyhttpd:v0.1-6 /bin/httpd -f -h /data/web/html

[root@localhost img1]# docker port tinyweb1
80/tcp -> 0.0.0.0:32768

ENV

  ENV
        用于,镜像定义所需的环境变量,并可被Dockerfile文件中位于其后的其它指令(如ENV(意思时env可嵌套,调用env中定义的变量),ADD,COPY等)所调用

        调用格式为$variable_namc或${variable_name}(调用格式跟我们开篇说讲到dockerfile中所支持的格式一样的,它也支持两种格式)

        Syntax(语法)
              ENV <key> <valuc> 或      (定义环境变量时标识在这儿定义完以后也可直接赋值,也可以不赋值,一般而言我们要赋值的话,env,<key>指定变量名,我们把它称之为key,<valuc>然后是变量值,这表示一次只定义一个变量。)
              ENV <key>=<value>...      (如果你想一次定义多个变量,如果有多个变量需要定义,尽量写在一个指令中,因为每一个指令会生成一个新层的,对吧。 那第二种格式ENV <key>等于<value>,点点点意思是可定义多个)

        第一种格式中,<key>之后的所有内容均会被视作其<value>的组成部分(所以中间有空白字符什么的,你都不用担心,也不用加引号。只有第一个空白字符左侧的当做key,第一个空白字符向右侧的通通当<value>,除非加引号,他有别的识别格式。),因此,一次只能设置一个变量;

        第二种格式可用一次设置多个变量,每个变量为一个"<key>=<value>"的键值对,如果<value>中包含空格,可以以反斜线(\)进行转义,也可通过对<value>加引号进行标识;另外,反斜线也可用于续行(假如你要定义多个变量,我们要分开进行表示,我让大家看得很清晰,这是用序号和一横只写一个。):

        定义多个变量时,建议使用第二种方式,以便在同一层中完成所有功能
# 编辑dockerfile

# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"
# LABEL maintainer="haoran <haoran@haorankeji.com>"
ENV DOC_ROOT=/data/web/html/ \       # evn加个变量,变量名叫doc_root(这样表示文档根目录,也可以写成大写的),其值假如只定义一个,写在后面就行了。比如为DOC_ROOT /data/web/html。      但是如果一次定义多个环境变量的话也可以就使用DOC_ROOT=/data/web/html单独赋值,每一个变量名都可以加引号,后面使用反斜线\续行      这样我们可以把它写在多行当中,每一个变量都清晰可变。
    WEB_SERVER_PACKAGE="nginx-1.19.4"      # 应该还可以有web的程序包WEB_SERVER_PACKAGE指明包名nginx-1.19.4,就是引用我们程序包组名称,我们这里没有加后缀.tar.gz 这样就使得,之后这个名称是可以换的,但是也可以写全也没有问题,这样两个变量都有了


COPY index.html ${DOC_ROOT:-/data/web/html/}       # 而后我们复制时,可以直接复制为$DOC_ROOT就行了,但是万一DOC_ROOT没有值呢,我们可以使用${DOC_ROOT:-/data/web/html/ }指明默认值
COPY yum.repos.d /etc/yum.repos.d/
# ADD http://nginx.org/download/nginx-1.19.4.tar.gz.asc /usr/local/src/
WORKDIR /usr/local/

ADD ${WEB_SERVER_PACKAGE}.tar.gz ./src      #我们到底添加的是哪个nginx呀,也可以不用直接些这个名称了,写成${WEB_SERVER_PACKAGE}在补上.tar.gz

VOLUME /data/mysql/

EXPOSE 80/TCP


# 构建镜像(版本是v0.1-7)
[root@localhost img1]# docker build -t tinyhttpd:v0.1-7 ./ 
Sending build context to Docker daemon  1.077MB
Step 1/9 : FROM busybox:latest
 ---> f0b02e9d092d
Step 2/9 : MAINTAINER "haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> 3f686ae096ac
Step 3/9 : ENV DOC_ROOT=/data/web/html/     WEB_SERVER_PACKAGE="nginx-1.19.4"
 ---> Running in 3decb2275748
Removing intermediate container 3decb2275748
 ---> 8f7b91e3fcf2
Step 4/9 : COPY index.html ${DOC_ROOT:-/data/web/html/}
 ---> 280c06358819
Step 5/9 : COPY yum.repos.d /etc/yum.repos.d/
 ---> abee4311094f
Step 6/9 : WORKDIR /usr/local/
 ---> Running in 2d2723c69ceb
Removing intermediate container 2d2723c69ceb
 ---> 20c85fdcb746
Step 7/9 : ADD ${WEB_SERVER_PACKAGE}.tar.gz ./src
 ---> 85ddad7d9fa6
Step 8/9 : VOLUME /data/mysql/
 ---> Running in 560e66ba72d3
Removing intermediate container 560e66ba72d3
 ---> 36a1243174e0
Step 9/9 : EXPOSE 80/TCP
 ---> Running in 173071d34c36
Removing intermediate container 173071d34c36
 ---> 66bbcbaac6b5
Successfully built 66bbcbaac6b5
Successfully tagged tinyhttpd:v0.1-7

# 验证一下(文件确实在,并且已展开,这样玩好像也没有什么太大的意义,我们在docker run的时候是可以向变量传值的使用-e选项,可以设定变量值)
[root@localhost img1]# docker run --name tinyweb1 --rm -P  tinyhttpd:v0.1-7 ls /usr/local/src/
nginx-1.19.4
[root@localhost img1]# docker run --name tinyweb1 --rm -P  tinyhttpd:v0.1-7 ls /usr/local/src/nginx-1.19.4
CHANGES
CHANGES.ru
LICENSE
README
auto
conf
configure
contrib
html
man
src

[root@localhost img1]# docker run --name tinyweb1 --rm -P  tinyhttpd:v0.1-7 ls /data/web/html
index.html


docker run时进行变量替换
  我们在docker run的时候是可以向变量传值的使用-e选项,可以设定变量值。      我们的系统镜像构建和启动容器是两个不同的过程,我们有目录执行build能做成镜像,而后run是把这个镜像在启动为容器。所以说这是两个不同的步骤,而且它是两个不同的结构。你的变量替换发生哪个阶段,这个很关键,能判定出来。所以像刚才我们定义的变量,在docker file中,它是发生在第一阶段,在build过程中执行的,还是在run的过程执行的。ok,这是在build的过程中执行的,但是我们把他启动为容器时,在这个容器中我们执行命令叫printenv,这个命令是输出本地变量信息的,可以看到是有DOC_ROOT和WEB_SERVER_PACKAGE变量名的,所以你在dockerfile中所定义的所有环境变量,是可以在启动容器以后直接在容器中使用的变量或者引用变量,变量是被注入到容器中的,所以我们此处给它重新赋值是没有问题的。但它会不会影响到我们所看到的结果呢?比如现在nginx是-1.19.4,换成-1.15.2,注意啊是运行为容器时换的,因为build的时候我们在dockerfile中定义的是nginx-1.19.4,但我们启动为docker run的时候能不能给他重新赋值
# 查看容器内的本地变量信息(可以看到有DOC_ROOT和WEB_SERVER_PACKAGE)
[root@localhost img1]# docker run --name tinyweb1 --rm -P  tinyhttpd:v0.1-7 printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=55101cb4918c
DOC_ROOT=/data/web/html/
WEB_SERVER_PACKAGE=nginx-1.19.4
HOME=/root

# 容器启动时,指定环境变量,使用-e选项,可以使用docker run --help查看,直接指明某个环境变量等于什么值(可以看到WEB_SERVER_PACKAGE=nginx-1.15.1,所以这就是向命令行中,在初始化镜像为容器时,可直接给他向环境变量赋值,从而能够接收环境信息的。      但是我们目录/usr/local/src/下,它下载的程序包内容版本可以看到还是在dockerfile中定义的nginx-1.19.4版本,因为这里的替换是在你的docker build中就完成了,你dockerfile中的值,在docker build的时候已经结束过了,做成镜像,已经成了事实了,所以我们后来run的时候再去改它的版本号也只是显示他的变量值变了,但并不会影响docker build过程,因为它毕竟docker run已经是第二阶段了,      有些环境变量假如说在第二阶段会用到的,你在第二阶段中传值,它的确是可以起到所谓重新配置等作用的。)
[root@localhost img1]# docker run --name tinyweb1 --rm -P  -e WEB_SERVER_PACKAGE="nginx-1.15.1" tinyhttpd:v0.1-7 printenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=7b374a4cc773
WEB_SERVER_PACKAGE=nginx-1.15.1
DOC_ROOT=/data/web/html/
HOME=/root


[root@localhost img1]# docker run --name tinyweb1 --rm -P  -e WEB_SERVER_PACKAGE="nginx-1.15.1" tinyhttpd:v0.1-7 ls /usr/local/src/
nginx-1.19.4

RUN和CMD

  他们两个都是运行命令的,但是这个两者所运行命令的时间点不一样。我们一个镜像,从开始制作到最后启动为容器,它要经历两个阶段,第一做镜像的阶段叫docker build的过程,对吧,第二是将镜像启动容器的阶段,我把它称作叫docker run。而后这两个阶段执行过程当中都可以让我们去运行shell命令的。      各位知道,第二阶段命令我们用过很多次了,我们甚至还可以改他运行命令是什么,比如改为ls  /usr/local/src/      所以把一个镜像启动为容器时,要运行的命令,就ls  /usr/local/src/这个,大家知道我们如果不在这儿写,它会运行默认那个命令,对吧。      默认那个命令谁来定义呀?CMD定义。      所以说CMD适用于定义把镜像启动为容器时,如果你没指运行什么命令,而默认CMD有运行的命令。      当你去基于dockerfile构建镜像时要运行的命令将在docker build中运行,那是不是很奇怪,build的时候运行命令干什么。      就基于刚才所讲的上下文,你应该能明白,假如说dockerfile这里ADD的时候,不是ADD了一个本地的文件,我ADD的就是个URL,我们知道这个url默认,下载下来的即便是tar.gz也不会自动展开,对不对,但我需要展开并编译安装,该怎么办, 使用RUN和tar,前提是这个基础镜像中要有tar命令,没有的话就是运行不了,所以你的运行所有命令是基于基础镜像所提供的环境,运行的命令 ,所以在这个情况之下我们就可以手动把它展开了。嗯因此我们要编译安装nginx,最起码应该有编译安装环境才能编译。而我们busybox是肯定没有开发环境,你这编译是编译不了的,但做一些别的操作是没问题的,比如把它展开呀,改改名什么之类的, 没问题。
RUN
   RUN
        用于指定docker build过程中运行的程序,其可以是任何命令

  Syntax
        RUN <command>或
        RUN ["<executable>", "<paraml>", "<param2>"]

  第一种格式中, <command>通常是一个shell命令(系统默认会把你给定的命令作为shell的子进程来运行,他会自动先启动shell,把他传给shell当参数来运行),且以"/bin/sh -c"来运行它,这意味着此进程在容器中的PID不为1,不能接收Unix信号,因此,当使用docker stop <container>命令停止容器时,此进程接收不到SIGTERM信号;

  第二种语法格式中的参数是一个JSON格式的数组,其中<executable >(就是可执行程序文件的路径)为要运行的命令,后面的<paramN>为传递给命令的选项或参数;然而,此种格式指定的命令不会以"/bin/sh -c"来发起(意思是不以shell启动,他们直接有内核创建,因此不支持各种各样的shell操作符),因此常见的shell操作如变量替换以及通配符(?,*等)替换将不会进行;不过,如果要运行的命令依赖于此shell特性的话,可以将其替换为类似下面的格式
        RUN ["/bin/bash", "-c","<executable>", "<paraml>"      (如果我们想让他支持shell操作符,可以使用这种格式,"/bin/bash", "-c"我们手动把他启动为shell子进程,CMD也支持类似的格式)

  注意:json数组中,要使用双引号

        RUN指令是在build构建镜像时使用,是可以运行多次的,同时如果多个命令彼此之间有关联关系,建议你在一条RUN当中把多个command写进来,就根据我们刚才建议的格式,比如使用RUN COMMAND 1,然后&& \续行,如果还有COMMAND 2,这样去写,这样才能保证如果你其中某一命令错了,这个&&的意思是前一个成功了,执行后一个,否者就终止了,它会报错停下来不会再做后面的步骤。      那现在各位做一个nginx镜像,应该基于一个centos,就很简单了。      假如我们基于最新版的centos作一个安装了nginx镜像, FROM centos,而后执行RUN yum -y install epel-release因为它默认没有安装epel仓库的,安装完以后第二步 && yum makecache 一下,然后再&& yum install nginx      其实大多数的镜像在实现基于基础镜像做应用程序安装时都是编译安装的,而不是使用yum安装的,当然你使用yum也没有什么错,但是要注意,yum安装时会生成很多缓存,所以安装完nginx别忘了最好要把缓存目录删除掉,以节约镜像空间,把步骤想全了,没必要的文件千万不要做进镜像,不然这样会极大的增加你的镜像的体积的
# 编辑dockerfile文件
[root@localhost img1]# vi Dockerfile
# Description: test image
FROM busybox:latest
MAINTAINER "haoran <haoran@haorankeji.com>"
# LABEL maintainer="haoran <haoran@haorankeji.com>"
ENV DOC_ROOT=/data/web/html/ \
    WEB_SERVER_PACKAGE="nginx-1.19.4.tar.gz"      # 把我们把变量名改成nginx-1.19.4.tar.gz更方便引用

COPY index.html ${DOC_ROOT:-/data/web/html/}
COPY yum.repos.d /etc/yum.repos.d/
ADD http://nginx.org/download/${WEB_SERVER_PACKAGE} /usr/local/src/      # 把ADD注释取消,使用这个ADD下载的url名改为引用变量
WORKDIR /usr/local/

# ADD ${WEB_SERVER_PACKAGE}.tar.gz ./src      # 注释掉安装本地ADD

VOLUME /data/mysql/

EXPOSE 80/TCP

RUN cd /usr/local/src && \      # 定义RUN,我们先去cd 切换目录到/usr/local/src去展开这个目录下一下载的文件,切换后当交互式进入容器时,就会自动在这个工作目录下。      &&如果不安德,需要在写一个run,但是我们说话指令启的越多,层就越多。      然后反斜线\续行,看的明白每一行一个命令  
    tar -xf ${WEB_SERVER_PACKAGE}      # tar -x 去展开WEB_SERVER_PACKAGE这个环境变量引用调用,直接展开到当前目录下,不用加-C


# 构建镜像,版本v0.1-8
[root@localhost img1]# docker build -t tinyhttpd:v0.1-8 ./ 
Sending build context to Docker daemon  1.078MB
Step 1/10 : FROM busybox:latest
 ---> f0b02e9d092d
Step 2/10 : MAINTAINER "haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> 3f686ae096ac
Step 3/10 : ENV DOC_ROOT=/data/web/html/     WEB_SERVER_PACKAGE="nginx-1.19.4.tar.gz"
 ---> Running in 5d593c0ef423
Removing intermediate container 5d593c0ef423
 ---> df86eaf367cd
Step 4/10 : COPY index.html ${DOC_ROOT:-/data/web/html/}
 ---> 2a0ac75c2277
Step 5/10 : COPY yum.repos.d /etc/yum.repos.d/
 ---> 9b1b44bcd8bd
Step 6/10 : ADD http://nginx.org/download/${WEB_SERVER_PACKAGE} /usr/local/src/
Downloading [==================================================>]  1.055MB/1.055MB
 ---> c3ed6fcad5a0
Step 7/10 : WORKDIR /usr/local/
 ---> Running in 03bbc980464c
Removing intermediate container 03bbc980464c
 ---> d974646f5349
Step 8/10 : VOLUME /data/mysql/
 ---> Running in af22422ba730
Removing intermediate container af22422ba730
 ---> 6dd0b26221ca
Step 9/10 : EXPOSE 80/TCP
 ---> Running in 7c5aa0ef4274
Removing intermediate container 7c5aa0ef4274
 ---> 97928048c2c6
Step 10/10 : RUN cd /usr/local/src &&     tar -xf ${WEB_SERVER_PACKAGE}
 ---> Running in 0f6262c4ebb3
Removing intermediate container 0f6262c4ebb3
 ---> c12bc6b2725c
Successfully built c12bc6b2725c
Successfully tagged tinyhttpd:v0.1-8


# 验证一下(可以看到nginx-1.19.4.tar.gz是下载的压缩包,nginx-1.19.4是展开的文件)
[root@localhost img1]# docker run --name tinyweb1 --rm -P  -e WEB_SERVER_PACKAGE="nginx-1.15.1" tinyhttpd:v0.1-8 ls /usr/local/src/
nginx-1.19.4
nginx-1.19.4.tar.gz

CMD
  CMD
        类似于RUN指令,CMD指令也可用于运行任何命令或应用程序,不过,二者的运行时间点不同

              RUN指令运行于映像文件构建过程中,而CMD指令运行于基于Dockerfile构建出的新映像文件启动一个容器时

              CMD指令的首要目的在于为启动的容器指定默认要运行的程序,且其运行结束后,容器也将终止;不过, CMD指定的命令其可以被docker run的命令行选项所覆盖

              在Dockerfile中可以存在多个CMD指令,但仅最后一个会生效

        Syntax(语法)
              CMD <command> 或      (第一种直接给命令,自动把他运行为shell子进程,因此这样写有一个极大的坏处在于他的id不为1。因此无法使用docker stop去停止他,因为它接收不到信号,接信号的都是进程号为1的进程,因为它是一个超管进程)

              CMD ["<executable>", "<paraml>", "<param2>"] 或      (我们也可以使用列表格式写进程了,我们运行应用时,通常应该有可执行程序、选项,选项参数、命令参数等共同组成,对吧,使用第一种方式在CMD后面直接给出就行,那么如果某一个参数中间有空白字符的话,      他是直接启动为id为1的进程的,所以可处理可接收并处理我们的shell信号或者我们系统发过来的信号。) 

              CMD ["<param1>","<param2>"]      (第三种格式,只有选项,参数没有命令,他要结合另外的指令ENTRYPOINT才能被使用)

        前两种语法格式的意义同RUN

        第三种则用于为ENTRYPOINT指令提供默认参数

              前面我们讲到制作dockerfile或者基于dockerfile来制作docker镜像时,是那我们要考虑其镜像由dockerfile制作成型,以及再将docker镜像启动为docker容器时的两个过程阶段当中所分别需要运行的命令有什么样的区别,好的,前面我们讲到过,在dockerfile中有个著名的指令叫RUN,它能够用于指定要在docker build过程当中执行的命令,对吧,这是我们构建成为一个docker镜像时,需要让镜像完整而做的一些操作。那因此他们在镜像制作完成的时候就已经运行结束了,这个镜像可以被推到公共仓库中随后供很多的用户来使用。而后用户使用docker run将其初始化为容器的时候,这些命令是不可能在运行的,大家一定要记得。      而后在docker的上下文当中有一个重要的特点就在于一个容器只是为了运行单个程序或者叫单个应用。那么到底应该运行哪个程序或者什么应用,一般而言,能拥有生产能力的应用,通常应该是一个经常在我们宿主机上按传统管理方式让其运行为后台的守护进程程序,对不对,比如像什么nginx、redis、mysql等等都是如此。那更要注意的是,我们此前去启动并运行这些服务时,都是在什么场景下去启动他们的,我们一直在讲,每一个进程都应该是某个进程的子进程,对不对,除了init之外,其实init是由内核启动的。那么请问假设我们现在手动启动nginx让它运行起来了,我们在命令行当中给定了一个nginx,这个时候他应该运行的是,作为谁的子进程存在。      我们这以往这事干得很多了,对吧,如果不使用systemctl来启动服务的话,我们可以直接在命令行中激活并运行nginx,请问此时nginx是谁的子进程?他是shell子进程,我们用户启动并创建进程的接口,我们说过很多次,应该是shell这也是我们能够与操作系统交互的接口。所以事实上我们打开这个命令行提示符,就相当于正在运行一个shell进程。而后在命令行之下,我们所创建的任何进程都应该属于shell的子进程。而且有些进程它可能会直接占据当前shell的终端设备,对不对,我们把它剥离与当前终端的,至少让他通过后台去运行,应该用&符号,在命令后面加个&符号,我们应该都很熟悉了。但是加个&符号并不能剥离与当前shell的关系,意思是它的父进程依然是启动它的shell,因此如果我们现在启动一个命令以后直接退出了shell,那么任何一进程终止时,会顺道先把它的所有子进程销毁。      那也就意味着,就看上去你已经启动了一个nginx,并且启动把它运行到后台去了,它其实能在你退出shell以后继续进行吗?不能,      之前讲过要实现这个功能还应该再执行nohub 加上command 在加&符号,这就是送到后台,并且期望它剥离于当前shell进程的关系,你可以想象成就表示把启动这个命令的父进程安排给init了,说白了就是自己家生了个娃儿,但给他找了一个新的父进程,而后以便自己驾鹤西游的时候,它不会成为一个孤儿就是道理。      不然的话,在我们系统进程运行领域当中,曾经说过,从来都是白发人送黑发人,万一白发人先走了,意外先走了,而没有给他的后代安排好的话,也没把他的后代带走的话,那他的后代就成了孤儿进程,就留在你的整个系统之上也不会释放内存,也不会终止了。      那为什么要说这么多,各位想象一下,我们在容器当中要启动一个进程,刚才一直讲的概念,运行且只运行一个主要的进程,那这个进程是直接由内核创建启动,还是把它托管给一个shell来启动,就特别关键了。      请问能不能让他委托给内核直接来创建并启动呢,他就是这个用户空间ID号为1的进程,我们说过很多次了对吧。所以很显然ID为1的应该是内核启动的,但是由内核启动进程是我们过去学习过程当中所生成的很多使用经验,是不可以在此处使用。比如说他们经常会使用什么ls /var/*会使用这样的命令对把,那还有可能会,还有很多其他的命令行替换的命令,各位应该知道*是什么东西,是通配符没错,它是shell提供的功能。那也就意味着说我们在shell命令行下去执行此类命令的时候,它能正常运行,而且能够正常被解析。但一旦这个命令不是有shell启动的,这样做,就没法运行,内核并不知道*是什么。所以如果我们在一个用户空间当中,不是作为shell的子进程去启动一个程序,这种各种特性,所谓命令行展开,管道,输入,输出重定向等功能通通不在可用。这些功能你如果不打算基于shell来启动,那他们都不能用了,而要基于shell来启动。请问你的这个主机上,还是这个用户空间ID为1的进程吗?不是啊,假如说我们在容器当中希望能够基于shell的方式来启动一个主进程,他通常是这样的,你应该先在这个用户空间启动shell,shell的id号为1,而后基于shell在启动,我们自己的主进程,对不对?现在shell能退出吗?shell一退出,他的子进程也挂了,所以我还得基于shell启动以剥离终端的方式来启动。      一剥离终端以后,你会发现,主进程id号不为1。      不过此前讲过,我们有一个办法来解决这个问题,用shell来启动也行,shell的id为1也行,但是需要运行exec命令,这样一来结果是什么?exec顶替shell的id为1,取代shell进程,shell退出他就成为id为1的进程。      很多时候呢我们在容器当中启动一进程时可以不基于shell直接启动进程,也可以基于shell来启动,但要基于shell启动,并且不违背这个主进程一定是ID为1的进程,这种条件和关系。那么我们可以通过exec COMMAND方式来实现.                        那接下来我们就可以说一说CMD指令了,刚刚讲过,我们有两个能运行命令的dockerfile指令可用。第一叫run,但它是在docker build过程中执行的,第二叫CMD他是定义一个镜像文件启动为容器时,默认要运行的程序,而docker容器默认只运行一个程序,请问CMD能不能出现多个?我可不可以在一个dockerfile文件中给多个CMD,你给了也没用,是这意思吧?没错,CMD其实可以给多个,但是只有最后一个生效。显然什么叫默认,什么叫只能用一个,在此之处就可以反映出来。但run不是在docker build过程当中,你有多个run指令,运行完第一个他就运行第二个,然后运行第三个逐一运行。而在docker run过程中要执行CMD,只有一个有效。这是CMD与RUN的两个最大不同之处。第一,他们运行的时间点不同,第二,他们的运行行为也不同。
第一种格式
# 为了区分与run的区别,重新做一个镜像,创建目录 
[root@localhost img1]# cd
[root@localhost ~]# mkdir img2
[root@localhost ~]# cd img2
[root@localhost img2]# 

# 定义Dockerfile
[root@localhost img2]# vi Dockerfile
FROM busybox      # 我们依然基于busybox来构建
LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"      # maintainer可以使用label来替代,可以写其他的键值,比如app="httpd"

ENV WEB_DOC_ROOT="/data/web/html/"      # 定义一个变量,是网页路径

RUN mkdir -p $WEB_DOC_ROOT && \      # 当写run的时候,这种命令没加中括号他会自动启动为shell的子进程的,或者此时的 > 输出重定向可以使用,目录不存在mkdir -p创建目录 & \ 换行
     echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html      #echo进一段字符串,到调用变量的目录下的文件名,

CMD /bin/httpd -f -h ${WEB_DOC_ROOT}      # CMD在docker build中不会运行,在把这个制作的目标镜像,运行为容器时,它会运行的。CMD /bin/httpd -f -h ${WEB_DOC_ROOT请问cmd我们使用这种格式,他是不是以shell子进程来启动这个命令的}



# 构建镜像,版本时v0.2-1
[root@localhost img2]# docker build -t tinyhttpd:v0.2-1 ./
Sending build context to Docker daemon  2.048kB
Step 1/5 : FROM busybox
 ---> f0b02e9d092d
Step 2/5 : LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"
 ---> Using cache
 ---> b33b8a9de40c
Step 3/5 : ENV WEB_DOC_ROOT="/data/web/html/"
 ---> Running in b7d6c9c33cdc
Removing intermediate container b7d6c9c33cdc
 ---> b141419fff0d
Step 4/5 : RUN mkdir -p $WEB_DOC_ROOT &&      echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html
 ---> Running in 1ded0adbf3b7
Removing intermediate container 1ded0adbf3b7
 ---> 8222095ed688
Step 5/5 : CMD /bin/httpd -f -h ${WEB_DOC_ROOT}
 ---> Running in b077311cf532
Removing intermediate container b077311cf532
 ---> 5856b119480d
Successfully built 5856b119480d
Successfully tagged tinyhttpd:v0.2-1

# 查看镜像详情(他会显示cmd的内容)
[root@localhost img2]# docker image inspect tinyhttpd:v0.2-1
            "Cmd": [
                "/bin/sh",
                "-c",      (他自动使用/bin/sh -c 来启动你给的"/bin/httpd -f -h ${WEB_DOC_ROOT}",从这儿就能看出来)
                "/bin/httpd -f -h ${WEB_DOC_ROOT}"
            ],



# 启动为容器,看看效果(在这个镜像当中我们 已经定义了默认运行的程序不再是shell了,而是httpd。以往我们加上-it他就自动进入交互式接口了,现在加上-it他就没有用,我们没有定义暴露端口,这里指定-P没有用,      他卡在这儿了,为什么呢?我们加了-it的,但是他默认运行的程序是httpd,所以你加-it没有用,httpd哪有什么交互式接口) 
[root@localhost img1]# docker run --name tinyweb2 -it --rm -P tinyhttpd:v0.2-1

# 查看容器(可以看到我们的容器tinyweb2运行的是httpd,显然他不是shell,他是shell的子进程,所以默认是没有交互式接口,加-it也是进不去的,但是我们可以使用exec这个命令,在额外登陆进入)
[root@localhost img1]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
f1f3a75df934        tinyhttpd:v0.2-1    "/bin/sh -c '/bin/ht…"   About a minute ago   Up About a minute                       tinyweb2

# 在新终端,使用exec登陆进容器,使用ps,对应的/bin/httpd程序id号为1,它默认用它来启动是为了避免它id不是1,他应该做替换操作。 他默认就执行了exec的替换,所以你看上去我们启动了httpd进程id依然为1,是为了确保这个容器,能自动接受unix信号,当你执行docker stop的时候能停掉,但这并不妨碍他的确启动为/bin/sh的子进程的,而这我们也验证了确实镜像的默认启动以后,运行命令被我们改掉了,而且他的shell解析(/data/web/html/)是不是也是成功的,这是我们调用的环境变量,这个变量是我在dockerfile中定义的,因此它会自动注入到环境变量中来。
[root@localhost img2]# docker exec -it tinyweb2 /bin/sh
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/httpd -f -h /data/web/html/      (我们在run时才可以调用,而这个命令确实是在run时才执行,所以也验证了我们说过,有些环境变量在run的时候也是可以被使用,但有一些在build过程中就已经执行过了,比如下载程序包。这个逻辑一定要搞清楚,不然你将来自己做镜像时还会重来重新思考这个问题。变量在build的时候创建,以存在,在run的时候可以被调用)
    6 root      0:00 /bin/sh
   11 root      0:00 ps
/ # printenv
WEB_DOC_ROOT=/data/web/html/
HOSTNAME=f1f3a75df934
SHLVL=1
HOME=/root
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/

第二种格式
# 编辑dockerfile文件,放在中括号当中,做json数组(列表)的格式来使用。
[root@localhost img2]# vi Dockerfile 
FROM busybox
LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"

ENV WEB_DOC_ROOT="/data/web/html/"

RUN mkdir -p $WEB_DOC_ROOT && \
     echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html

# CMD /bin/httpd -f -h ${WEB_DOC_ROOT}

CMD ["/bin/httpd","-f","-h ${WEB_DOC_ROOT}"]      # 第一个是要执行的程序/bin/httpd,第二个-f,第三个-h,-h的参数可以跟-f写在一起,也可以分开写,但是次序要放对了,因为他调用时就是按照次序调用的,这是json数组格式的,我说过这种格式默认并不会以shell子进程的方式来解析,对吧,所以他不会解析变量



# 构建镜像,版本v0.2-2
[root@localhost img2]# docker build -t tinyhttpd:v0.2-2 ./
Sending build context to Docker daemon  2.048kB
Step 1/5 : FROM busybox
 ---> f0b02e9d092d
Step 2/5 : LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"
 ---> Using cache
 ---> b33b8a9de40c
Step 3/5 : ENV WEB_DOC_ROOT="/data/web/html/"
 ---> Using cache
 ---> b141419fff0d
Step 4/5 : RUN mkdir -p $WEB_DOC_ROOT &&      echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html
 ---> Using cache
 ---> 8222095ed688
Step 5/5 : CMD ["/bin/httpd","-f","-h ${WEB_DOC_ROOT}"]
 ---> Running in d81320b7acba
Removing intermediate container d81320b7acba
 ---> d9e725f2f82b
Successfully built d9e725f2f82b
Successfully tagged tinyhttpd:v0.2-2


# 查看镜像的详细信息,检查一下镜像文件的内部格式,可以看到没有/bin/sh了,直接显示这个命令本身
[root@localhost img2]# docker image inspect tinyhttpd:v0.2-2
            "Cmd": [
                "/bin/httpd",
                "-f",
                "-h ${WEB_DOC_ROOT}"
            ],

# 启动为容器(报错了,为什么?我们不止一次强调说,在第二种格式中,它默认并不会运行为shell的子进程。因此我刚才特别强调说${WEB_DOC_ROOT}这是shell变量,因此既然不会启动shell子进程,让内核是否清楚这到底是什么东西,他会把它当作目录路径,-h ${WEB_DOC_ROOT}这里肯定是一个路径,他会认为说这个路径不存,所以直接报错。      当然我们也可以手动把他运行为shell子进程)
[root@localhost img1]# docker run --name tinyweb2 -it --rm -P tinyhttpd:v0.2-2
httpd: can't change directory to ' ${WEB_DOC_ROOT}': No such file or directory




# 编辑dockerfile文件,定义使用/bin/sh 使用-c选项来定义
[root@localhost img2]# vi Dockerfile 
FROM busybox
LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"

ENV WEB_DOC_ROOT="/data/web/html/"

RUN mkdir -p $WEB_DOC_ROOT && \
     echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html

# CMD /bin/httpd -f -h ${WEB_DOC_ROOT}

CMD ["/bin/sh","-c","/bin/httpd","-f","-h ${WEB_DOC_ROOT}"]      # 定义使用/bin/sh 使用-c选项来定义


# 构建镜像,版本v0.2-3
[root@localhost img2]# docker build -t tinyhttpd:v0.2-3 ./
Sending build context to Docker daemon  2.048kB
Step 1/5 : FROM busybox
 ---> f0b02e9d092d
Step 2/5 : LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"
 ---> Using cache
 ---> b33b8a9de40c
Step 3/5 : ENV WEB_DOC_ROOT="/data/web/html/"
 ---> Using cache
 ---> b141419fff0d
Step 4/5 : RUN mkdir -p $WEB_DOC_ROOT &&      echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html
 ---> Using cache
 ---> 8222095ed688
Step 5/5 : CMD ["/bin/sh","-c","/bin/httpd","-f","-h ${WEB_DOC_ROOT}"]
 ---> Running in bde391d73403
Removing intermediate container bde391d73403
 ---> 2e955810853a
Successfully built 2e955810853a
Successfully tagged tinyhttpd:v0.2-3

# 启动容器(这次没报什么错,而且直接退出终端了,退出终端会有什么问题呢,使用docker ps -a 可以看到,这个容器的状态是Exited退出了,可能是/bin/sh -c后,id号导致的启动不起来,没有解决)
[root@localhost img2]# docker run --name tinyweb2 -P tinyhttpd:v0.2-3
[root@localhost img2]# 

[root@localhost img2]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS               NAMES
6153f6160282        99a6f87b6797        "/bin/sh -c 'mkdir -…"   2 hours ago         Exited (1) 2 hours ago                          reverent_joliot

第三种格式
  只有参数没有命令,当你定义ENTRYPOINT的时候,再定义CMD两个混合定义有用吗?在dockerfile当中我们可以定义多个CMD或ENTRYPOINT但只有最后一个生效,但是CMD和ENTRYPOINT都同时定义了呢?那应该谁生效。它其实不是前后关系。而是刚才说了,cmd的内容将当作参数被传给ENTRYPOINT后面指定的命令,这就是CMD第三种格式的意义,第三种格式它表示用于为ENTRYPOINT提供默认参数,也就是你给的无论是什么都会被当作参数传递给ENTRYPOINT指定的命令。而且第三种格式,需要使用数组(列表)的格式CMD ["/bin/httpd -f -h ${WEB_DOC_ROOT}"],完全当做参数传递给ENTRYPOINT。 而这样写的时候我们说过,他的环境变量它默认是不会被解析的,是吧,但是保留这个把环境变量,测试一下。 然后把ENTRYPOINT要运行的命令改成/bin/sh -c。能看懂什么意思吧,把CMD当做参数,传递给ENTRYPOINT。因为我们的目的就是为了能够以shell的来启动它。而CMD ["/bin/httpd","-f","-h "]这种方式给出时实际上是不会以/bin/sh运行的。我们组合ENTRYPOINT和CMD同时来完成启动这个CMD的目的,      现在我再问一遍,如果此时我们把它创建为镜像以后,再启动为容器,要运行的命令应该是什么?
# 编辑dockerfile
[root@localhost img2]# vi Dockerfile
FROM busybox
LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"

ENV WEB_DOC_ROOT="/data/web/html/"

RUN mkdir -p $WEB_DOC_ROOT && \
echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html

# CMD /bin/httpd -f -h ${WEB_DOC_ROOT}

CMD ["/bin/httpd","-f","-h ${WEB_DOC_ROOT}"] # 需要使用数组的格式CMD ["/bin/httpd -f -h ${WEB_DOC_ROOT}"],完全当做参数传递给ENTRYPOINT
ENTRYPOINT /bin/sh -c # 组合CMD当做参数运行真正运行的是/bin/sh -c "/bin/httpd","-f","-h ${WEB_DOC_ROOT}"

# 构建镜像,版本v0.2-6
[root@localhost img2]# docker build -t tinyhttpd:v0.2-6 ./
Sending build context to Docker daemon 2.048kB
Step 1/6 : FROM busybox
---> f0b02e9d092d
Step 2/6 : LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"
---> Using cache
---> b33b8a9de40c
Step 3/6 : ENV WEB_DOC_ROOT="/data/web/html/"
---> Using cache
---> b141419fff0d
Step 4/6 : RUN mkdir -p $WEB_DOC_ROOT && echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html
---> Using cache
---> 8222095ed688
Step 5/6 : CMD ["/bin/httpd","-f","-h ${WEB_DOC_ROOT}"]
---> Using cache
---> d9e725f2f82b
Step 6/6 : ENTRYPOINT /bin/sh -c
---> Running in f14817afcb9d
Removing intermediate container f14817afcb9d
---> f6cd724f0b05
Successfully built f6cd724f0b05
Successfully tagged tinyhttpd:v0.2-6

# 查看镜像详情,他到底运行的命令是什么?
[root@localhost img2]# docker image inspect tinyhttpd:v0.2-6

            "Cmd": [
                "/bin/httpd",
                "-f",
                "-h ${WEB_DOC_ROOT}"
            ],
            "Image": "sha256:d9e725f2f82be966b2a96500bf64d2127e46f927d996b44ab5a99d8f16599a89",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": [      #(这里可以看到Entrypoint。下面"/bin/sh","-c",然后后面 "/bin/sh -c"把"/bin/httpd"他当参数,在传给"/bin/sh -c",可以发现/bin/sh切换为两次,为什么?原因dockerfile中ENTRYPOINT /bin/sh -c本来这种格式,就表示你给的命令要用/bin/sh来运行。所以你又指定了/bin/sh这个命令。他的意思就是表示以/bin/sh -c来运行/bin/sh -c,那因此如果我们的目标就是运行/bin/sh -c的,此时也使用ENTRYPOINT ["/bin/sh","-c"]这种格式,明确定义它就不会再用/bin/sh -c的方式。就是运行我们自己指定的"/bin/sh","-c"了)
                "/bin/sh",
                "-c",
                "/bin/sh -c"
            ],

# 运行我们自己指定的"/bin/sh","-c",编辑dockerfile
[root@localhost img2]# vi Dockerfile
FROM busybox
LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"

ENV WEB_DOC_ROOT="/data/web/html/"

RUN mkdir -p $WEB_DOC_ROOT && \
     echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html

# CMD /bin/httpd -f -h ${WEB_DOC_ROOT}

CMD ["/bin/httpd","-f","-h ${WEB_DOC_ROOT}"]
ENTRYPOINT ["/bin/sh","-c"]      #(运行我们自己指定的"/bin/sh","-c")

# 构建镜像,版本v0.2-7
[root@localhost img2]# docker build -t tinyhttpd:v0.2-7 ./
Sending build context to Docker daemon  2.048kB
Step 1/6 : FROM busybox
 ---> f0b02e9d092d
Step 2/6 : LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"
 ---> Using cache
 ---> b33b8a9de40c
Step 3/6 : ENV WEB_DOC_ROOT="/data/web/html/"
 ---> Using cache
 ---> b141419fff0d
Step 4/6 : RUN mkdir -p $WEB_DOC_ROOT &&      echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html
 ---> Using cache
 ---> 8222095ed688
Step 5/6 : CMD ["/bin/httpd","-f","-h ${WEB_DOC_ROOT}"]
 ---> Using cache
 ---> d9e725f2f82b
Step 6/6 : ENTRYPOINT ["/bin/sh",”-c“]
 ---> Running in f90a5958cf8d
Removing intermediate container f90a5958cf8d
 ---> 81aea67a4690
Successfully built 81aea67a4690
Successfully tagged tinyhttpd:v0.2-7

# 查看镜像详情
[root@localhost img2]# docker image inspect tinyhttpd:v0.2-7
            "Cmd": [      #(CMD也是我们自己定义的)
                "/bin/httpd",
                "-f",
                "-h ${WEB_DOC_ROOT}"
            ],
            "Image": "sha256:d9e725f2f82be966b2a96500bf64d2127e46f927d996b44ab5a99d8f16599a89",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": [      #(先看Entrypoint是我们定义的"/bin/sh","-c")
                "/bin/sh",
                "-c"
            ],

# 启动容器(也可以启动,不过这种格式依然会有问题,当我们指定为特定格式来运行时,由于里边有了什么环境变量什么之类的,你交叉引用以后,可能有的变量识别不了。所以导致启动是失败的,因为它一启动就停止了就相当于,这个不用管它了。就算这个容器启动起来停止了,使用docker ps -a 能看到这个容器执行的命令,确实是以/bin/sh -c 运行的/bin/httpd,没有问题)
[root@localhost img2]# docker run --name tinyweb2 -it -P tinyhttpd:v0.2-7
[root@localhost img2]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS               NAMES
d337c5d5d0dc        tinyhttpd:v0.2-7    "/bin/sh -c /bin/htt…"   19 seconds ago      Exited (0) 19 seconds ago                       tinyweb2

# 删除容器
[root@localhost img2]# docker rm tinyweb2
tinyweb2

# 在启动容器,这次的启动时传个选项,比如传个ls /data。你认为他会运行httpd还是ls?运行的时ls,用引号把ls /data引用起来,把他们当做一个参数传进来。      有没有发现,当我们传一个参数的时候,这个参数会不会覆盖我的CMD,dockerfile我们这里本来是有CMD的,CMD是定义的用作默认参数传给Entrypoint对吧,什么叫默认呢,没事指定时就用它,这就是默认。      当你定义Entrypoint时在命令行传的参数会当什么?会当做参数传递给Entrypoint,那Entrypoint就表示有自己的参数了呗,那因此默认的还用不用?把CMD覆盖了, 因为Entrypoint除非使用--Entrypoint选项,否者是不能被覆盖的,就这个逻辑
[root@localhost img2]# docker run --name tinyweb2 -it -P tinyhttpd:v0.2-7 "ls /data"
web

ENTRYPOINT

  在说明ENTRYPOINT之前,首先我们曾经用过很多次这种命令格式,应该有印象,比如我们去执行docker run的时候,明明他应该运行的是httpd命令对吧, 但我可以执行ls /data/web/html/([root@localhost ~]# docker run --name tinyweb2 -it --rm -P tinyhttpd:v0.2-3 ls /data/web/html/)这表示更改了什么?这个镜像中默认要运行的程序了,对吧,它没有运行httpd转而运行谁了?ls /data/web/html/运行了你给定的命令了,当然我也自己手动在给httpd命令也是可以的([root@localhost ~]# docker run --name tinyweb2 -it --rm -P tinyhttpd:v0.2-3 /bin/httpd -f -h /data/web/html) 所以对于我们自己做的镜像呢。默认在镜像中指定要在运行容器时,运行的命令是可以被覆盖的,就是说当我们执行docker run的时候,可以不去运行我们指定在镜像当中默认定义的要运行的命令,可运行为其他命令。我们用自己给定的命令来覆盖镜像中默认要运行的命令是没问题的,但是如果有时候我们不允许覆盖怎么办呢? CMD做不到,但ENTRYPOINT可以。 所以现在回过头来,各位能理解了,当你定义ENTRYPOINT的时候,再定义CMD两个混合定义有用吗?在dockerfile当中我们可以定义多个CMD或ENTRYPOINT但只有最后一个生效,但是CMD和ENTRYPOINT都同时定义了呢?那应该谁生效。它其实不是前后关系。而是刚才说了,cmd的内容将当作参数被传给ENTRYPOINT后面指定的命令,这就是CMD第三种格式的意义

  ENTRYPOINT
        类似CMD指令的功能,用于为容器指定默认运行程序,从而使得容器像是一个单独的可执行程序

        与CMD不同的是,由ENTRYPOINT启动的程序不会被docker run命令行指定的参数所覆盖,而且,这些命令行参数会被当作参数传递ENTRYPOINT指定的程序

              不过, docker run命令的--entrypoint选项的参数可覆盖ENTRYPOINT指令指定的程序
        Syntax
              ENTR YPOINT <command
              ENTRYPOINT ["<executable>", "<paraml>", "<param2>"]

        docker run命令传入的命令参数会覆盖CMD指令的内容并且附加到ENTRYPOINT命令最后做为其参数使用

        Dockerfile文件中也可以存在多个ENTRYPOINT指令,但仅有最后一个会生效
# 编辑dockerfile
[root@localhost img2]# vi Dockerfile
FROM busybox
LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"

ENV WEB_DOC_ROOT="/data/web/html/"

RUN mkdir -p $WEB_DOC_ROOT && \
echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html

# CMD /bin/httpd -f -h ${WEB_DOC_ROOT}

# CMD ["/bin/sh","-c","/bin/httpd","-f","-h ${WEB_DOC_ROOT}"] # 为了不出什么问题,我们吧这个CMD注释掉。
ENTRYPOINT /bin/httpd -f -h ${WEB_DOC_ROOT} # 改成ENTRYPOINT,后面还是此前CMD要执行的命令,他应该是运行web服务的对吧


# 构建镜像,版本v0.2-5
[root@localhost ~]# cd img2
[root@localhost img2]# docker build -t tinyhttpd:v0.2-5 ./
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM busybox
---> f0b02e9d092d
Step 2/5 : LABEL maintainer="haoran <hoaran@haorankeji.com>" app="httpd"
---> Using cache
---> b33b8a9de40c
Step 3/5 : ENV WEB_DOC_ROOT="/data/web/html/"
---> Using cache
---> b141419fff0d
Step 4/5 : RUN mkdir -p $WEB_DOC_ROOT && echo '<h1>Busybox httpd server.</h1>' > ${WEB_DOC_ROOT}/index.html
---> Using cache
---> 8222095ed688
Step 5/5 : ENTRYPOINT /bin/httpd -f -h ${WEB_DOC_ROOT}
---> Running in c68d3c9282b1
Removing intermediate container c68d3c9282b1
---> 81435af87fc8
Successfully built 81435af87fc8
Successfully tagged tinyhttpd:v0.2-5

# 启动这个容器(没有任何问题,这个容器已经正常运行起来,我们是可以访问这个web服务的。好的,接下来我们把这个容器给关掉。接着各位知道我们也可以在后面执行ls /data/web/html/此前我们经常说不 运行一个镜像中默认运行的命令,我在后面给定命令就行了,回车。这里没有运行ls,而且更有意思的是,其实它真正运行的命令是/bin/httpd -f -h /data/web/html默认你使用ENTRYPOINT指定的命令,并且把ls /data/web/html/它当参数并附在/bin/httpd -f -h /data/web/html ls /data/web/html/这个后面。只不过httpd识别不了这些字串,没有理他而已。)
[root@localhost img2]# docker run --name tinyweb2 -it --rm -P tinyhttpd:v0.2-5

[root@localhost img2]# docker kill tinyweb2
tinyweb2

[root@localhost img2]# docker run --name tinyweb2 -it --rm -P tinyhttpd:v0.2-5 ls /data/web/html/



# 就是想要运行自己的命令,怎么办,其实就算你使用了ENTRYPOINT定义也可以被覆盖,但你必须要明确知道你自己在做什么。意思是如果是那样的话,你是明确知道自己需求的,必须要使用专门的选项来指定命令的叫--entrypoint。可以用docker run --help查看,这才表示--entrypoint你后面这给的是个命令,这个命令可以覆盖ENTRYPOINT指定的命令。 你只需要知道它是能覆盖的就行了,但多数情况下我们都不会去覆盖它。
[root@localhost img2]# docker run --name tinyweb2 -it --rm -P --entrypoint=/bin/ls tinyhttpd:v0.2-5
bin data dev etc home proc root sys tmp usr var
[root@localhost img2]# docker run --name tinyweb2 -it --rm -P --entrypoint=ls tinyhttpd:v0.2-5
bin data dev etc home proc root sys tmp usr var

总结
  既然ENTRYPOINT有这个功能,CMD也有这功能,我们为什么非要启动这两项?通过刚才我传参数这种方式,各位应该大体都明白了,对吧,多数情况下ENTRYPOINT是用来指定你的shell,就是你打算对谁作为用来启动别的进程的一个父进程,另外的命令呢?在命令行中最后给的参数会当作它的子进程来启动,这样子我们就可以灵活地传参数,并且能够被shell所解析了,这是第一点。      第二点,我们此前不止一次的强调,容器接受配置要靠什么?环境变量,没有别的更好的方式,是吧,至少到此刻我们知道的对不对, 假如我们现在要运行的是nginx,我们要给nginx提供一个配置文件,该怎么实现,或者是让nginx能够灵活接受配置,这里来做第三个镜像。      我们希望接下来我们nginx镜像能够通过环境变量接受参数来决定它监听的地址、端口、等,然后我们做这些为了更加简化。我没基于之前用的nginx:1.14-alpine作为基础镜像来创建。
# 创建工作目录
[root@localhost img2]# cd 
[root@localhost ~]# mkdir img3


# 编写entrypoint.sh文件脚本
[root@localhost img3]# vi entrypoint.sh
#!/bin/sh      # 这个脚本时/bin/sh执行的,没有/bin/bash,这是一个alpine,没有bash只有sh
#
cat > /etc/nginx/conf.d/www.conf << EOF      # 它要执行的任务是什么呢,执行cat输出到,使用覆盖重定向,写到/etc/nginx/conf.d/目录下,文件比如叫www.conf << EOF
server {
        server_name $HOSTNAME;      # 内容是,第一个server_name我们使用$HOSTNAME;第一个变量$HOSTNAME,HOSTNAME如果没有命令,你可以是默认值,不过没关系因为容器HISTNAME一定会有的
        listen ${IP:-0.0.0.0}:${PORT:-80};      # 第二个,使用listen,listen到哪使用变量,假如就叫监听的地址和端口${IP}:${PORT},ip和端口加上默认值${IP:-0.0.0.0}:${PORT:-80}
        root ${NGX_DOC_ROOT:-/usr/share/nginx/html};      # root为一个变量${NGX_DOC_ROOT},如果用户没有传值,也没关系可以定义为${NGX_DOC_ROOT:-/usr/share/nginx/html/},nginx默认的就在这个路径下
}
EOF      # 后面EOF,能看到什么意思把,哪这些变量如果没有设定怎么办,比如ip可以有默认值



# 给文件执行权限
[root@localhost img3]# chmod -x entrypoint.sh 


# 验证一下容器内的nginx程序路径,和配置文件路径
[root@localhost img3]# docker run --name nginx1 -it --rm nginx:1.14-alpine /bin/sh
/ # which nginx
/usr/sbin/nginx
/ # ls /etc/nginx
conf.d                  fastcgi_params          koi-win                 modules                 scgi_params             uwsgi_params.default
fastcgi.conf            fastcgi_params.default  mime.types              nginx.conf              scgi_params.default     win-utf
fastcgi.conf.default    koi-utf                 mime.types.default      nginx.conf.default      uwsgi_params


# 编辑dockerfile(dockerfile不要使用单引号,否者起不来)
[root@localhost ~]# vi Dockerfile
FROM nginx:1.14-alpine      # 使用from nginx:1.14-alpine用小镜像
LABEL maintainer="haoran <haoran@haorankeji.com>"      # label定义maintainer等于作者名haoran,联系方式邮箱<haoran@haorankeji.com>

ENV NGX_DOC_ROOT="/data/web/html/"      # env定义一个环境变量ngx_doc_root等于我们给他设定一个路径为/data/web/html/

ADD index.html ${NGX_DOC_ROOT}      # 添加网页,我们指定了/data/web/html目录,但是没有指定网页

ADD entrypoint.sh /bin/      # 接着我们期望的是,我们等会运行nginx的时候,这个nginx能够通过接受变量来生成配置文件,而且这个变量可以在启动容器时进行传递。嗯该怎么做呢?我们指定nginx的配置文件一般放在哪?在/etc/nginx/下叫nginx.conf.      这一次我们这样来执行程序,我们给当前系统额外添加一个文件,在这儿呢我额外去ADD添加个一个文件,我们知道ADD是复制文件的,对吧,复制文件放在指定系统上,系统启动时执行,把当前系统上有个叫entrypoint.sh文件脚本(如不这个脚本不在,提前编写),复制到/bin/目录下去

CMD ["/usr/sbin/nginx","-g","daemon off;"]      # CMD定义要运行的程序,运行什么,对于nginx来讲,很显然我们要运行的命令应该是/usr/sbin/下的nginx,使用选项-g表示设定个全局选项(全局参数),我们让nginx运行在前台,nginx中有一个参数叫demo,off,demo就是设定为守护进程的,off就是不要设定为守护进程,每一个指令都应该用分号结尾,这是nginx文件格式。 这里没指配置文件,他会自动加载/etc/nginx/nginx.conf,而nginx.conf会 自动包含,conf.d下所有以.conf结尾的文件
ENTRYPOINT ["/bin/entrypoint.sh"]      # 定义ENTRYPOINT,对于他来讲,他要运行什么?要运行/bin/entrypoint.sh意思是我们用这个entrypoint.s脚本,通过读取环境变量会把他生成配置文件。而ENTRYPOINT是先要运行的,而CMD是要当做参数传递给他的。所以ENTRYPOINT现运行完,在运行CMD的指令。      但是对于这个entrypoint.sh脚本来讲,它怎么调用"/usr/sbin/nginx","-g","daemon off"这个呢?调用不了,但是我们确实会把他当参数传递给这个脚本,这个脚本要引用这个参数并运行它。所以我们接下来要做的是改脚本。      我们做的操作是什么?首先用/bin/entrypoint.sh这个脚本,帮他初始化出来一个配置文件,这个脚本会运行一个shell进程,在里面开始初始化配置文件,配置文件初始化完以后,接下来做什么?开始运行"/usr/sbin/nginx","-g","daemon off"他,而且还把他当做顶替自己当前这个进程的进程,也就意味着如果nginx启动了,shell进程也就退出了,nginx就成为了整个容器中的唯一进程,而且是主进程

# 创建网页文件
[root@localhost img3]# vi index.html
<h1>New Doc Root for Nginx</h1>


# 改entrypoint.sh脚本
[root@localhost img3]# vi entrypoint.sh
#!/bin/sh
#
cat > /etc/nginx/conf.d/www.conf << EOF
{
        server_name $HOSTNAME;
        listen ${IP:-0.0.0.0}:{PORT:-80};
        root ${NGX_DOC_ROOT:-/usr/share/nginx/html/};
}
EOF

exec "$@"      # 我们运行$@什么意思?$@表示当前脚本执行的所有参数。所以你传的参数是什么,我就把它运行什么,而且还要替换当前进程,还要运行exec,(exec什么意思呢,比如exec ls 在shell中执行ls,ls结束后不返回原来的shell中了)也就是说它以shell进程执行了cat命令,但是cat命令执行结束后不会到之前的shell中,那么容器内的nginx程序就不是/bin/sh启动的了,成了id为1的主进程,着就可以事先定义容器的环境,这就是我们要实现的效果

# 构建镜像,版本v0.3-1,      ./对当前目录来做镜像
[root@localhost img3]# docker build -t myweb:v0.3-1 ./
Sending build context to Docker daemon  4.096kB
Step 1/7 : FROM nginx:1.14-alpine
 ---> 8a2fb25a19f5
Step 2/7 : LABEL maintainer="haoran <haoran@haorankeji.com>"
 ---> Running in 72041cbac3fe
Removing intermediate container 72041cbac3fe
 ---> 8e576886245e
Step 3/7 : ENV NGX_DOC_ROOT="/data/web/html"
 ---> Running in 90b5c410101c
Removing intermediate container 90b5c410101c
 ---> 9b99231e9c8a
Step 4/7 : ADD index.html ${NGX_DOC_ROOT}
 ---> 6e91b51ff93f
Step 5/7 : ADD entrypoint.sh /bin/
 ---> 4b8b136a4c08
Step 6/7 : CMD ["/usr/sbin/nginx","-g","daemon off;"]
 ---> Running in 474824a0d7a7
Removing intermediate container 474824a0d7a7
 ---> b6e2cf24f19a
Step 7/7 : ENTRYPOINT ["/bin/entrypoint.sh"]
 ---> Running in 2db3997ad712
Removing intermediate container 2db3997ad712
 ---> 99aa230ffff0
Successfully built 99aa230ffff0
Successfully tagged myweb:v0.3-1



# 启动容器(我想现在各位应该明白,这些变量有什么用了,为什么用脚本,用一个事先专门定于的entrypoint脚本来启动应用程序,因为他可以帮我们应用程序事先去设定容器环境的)
[root@localhost img3]# docker run --name myweb1 -it -P myweb:v0.3-1


# 在新终端进行验证
[root@localhost img3]# docker exec -it myweb1 /bin/sh
/ # cat /etc/nginx/conf.d/www.conf       #(这个生成配置文件没有问题)
server {
        server_name 541932da1532;      #(只有server_name看着怪怪的对吧,因为它使用的是容器名,就是容器id)
        listen 0.0.0.0:80;
        root /data/web/html;
}

/ # ls /data/web/html/
index.html      # 我们自己定义的index.html文件也存在


/ # netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN            #(80端口监听也没有问题)

/ # wget -O - -q localhost      # (通过本地访问web服务没有问题,但是这个访问的是nginx默认主机,显示Welcome to nginx!。为什么呢?我们自己定义了的。因为你使用的是localhost它并不对我们定义的主机名,而我们 用的主机名是server_name定义的 541932da1532是这个名字,因此我们要访问这个主机。)
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

/ # wget -O - -q 541932da1532;
<h1>New Doc Root for Nginx</h1>      #(没有问题,我们的主机名,就是一个变量传递进来的,可以更改,这并不是我们目的。刚刚说过有环境变量的最大好处在于我们可以在run的时候直接给他生成配置文件。)

# 杀掉容器
[root@localhost img3]# docker kill myweb1
myweb1
[root@localhost img3]# docker rm myweb1
myweb1

# 再次启动容器,传递变量(我们知道刚才传递的是0.0.0.0的80端口对吧,那我们让他监听8080呢,可不可以更改网页目录,就是NGX_DOC_ROOT变量指定的,都可以的。所以在这启动的时候我们可以使用-e传变量了,传递端口指定为8080端口)
[root@localhost img3]# docker run --name myweb1 -it -P -e "PORT=8080" myweb:v0.3-1

# 在新终端验证(已经监听了8080,看明白了把,所以有了entrypoint脚本以后,我们就可以愉快的生成应用程序的配置信息了,但前提是我们要有一个应用程序,帮我们去生成配置文件模板。我们再看进程使用ps,可以看到nginx的id号还是1,它是1的原因在于,我们知道我们在dockerfile中虽然ENTRYPOINT启动的/bin/entrypoint.sh,但在entrypoint脚本的最后我们使用了exec实现了依然可以确保nginx做主进程id号为1。     此前曾经留了一个疑问说,如果我们系统环境有这么多种,比如有什么开发环境、生产环境,还有测试环境,于是我们要通过将配置文件备进入容器当中。那么我们不得不是基于一个矩阵生成九个镜像。因为每一个环境当中,我们可能还有作为,什么代理服务器啊,作为什么什么web服务器啊,作为调度器什么之类的配置,对吧,以后我们就可以只做简单做两三个镜像就行了,比如做代理服务器一个镜像,作为图片服务器一个镜像,然后作为只是和tomcat整合的一个镜像,假设。 每一个镜像上面,内部我们可以通过entrypoint脚本这种形式,把关键需要修改的那一部分的,比如监听的地址端口,要使用其他配置等等作为参数放置在你所生成配置文件当中,让你的脚本能通过读取或处理这些环境变量并生成配置文件。于是我们在docker run的时候,再run的那一刻把环境变量给它传值就能解决问题了。     大家也发现了,我们通过看一下这个dockerhub之上的mysql,随便找个示例都行。这种著名的服务程序,其实它在启动时,大多数都是使用entrypoint脚本,并接受CMD参数以后启动服务的,而之所以这么做的原因,现在应该解释清楚了)
[root@localhost img3]# docker exec -it myweb1 /bin/sh
/ # netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN  

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 nginx: master process /usr/sbin/nginx -g daemon off;
    8 nginx     0:00 nginx: worker process
    9 root      0:00 /bin/sh
   15 root      0:00 ps

USER

  USER
        用于指定运行image时的或运行Dockerfile中任何RUN,CMD或ENTR YPOINT指令指定的程序时的用户名或UID

        默认情况下, container的运行身份为root用户

        Syntax
              USER <UID)> <UserName>      (当我们指定一个uid或者UserName的时候,那么在容器启动以后,其主进程就以这个指定用户身份运行。但是请确保你的容器中存在这个用户,不然的话可能会有问题的。)

              需要注意的是, <UID>可以为任意数字,但实践中其必须为/etc/passwd中某用户的有效UID,否则, docker run命令将运行失败      (你的容器到系统当中,在你的容器中的/etc/passwd文件中得有你所指定的用户,不然的话会报错了。root用户默认就算没有/etc/passwd文件也没有问题,因为root用户是属于内核的,没有配置文件,也没有账号文件依然不受影响。)

  像刚才我们启动nginx的时候,可以发现它的属主是root,子进程才是nginx,那如果说我们希望这个整个主进程的运行都是以某个普通用户的身份来执行,甚至于在docker build就是做镜像的过程中,也应该以普通用户的身份而不是管理员的身份来运行。那么我们应该给他设置USER指令,但要确保USER有相关的所有权限。这个就不演示了,这个很简单

  HEALTHCHECK
        The HEALTHCHECK instruction tells Docker how to test a container to check that it is still working      (HEALTHCHECK指令告诉Docker如何测试容器以检查其是否仍在工作)

        This can detect cases such as a web server that is stuck in an infinite loop and unable to handle new connections, even though the server process is still running      (这可以检测到某些情况,例如Web服务器陷入无限循环并且无法处理新连接,即使服务器进程仍在运行)

        The HEALTHCHECK instruction has two forms:      (HEALTHCHECK指令具有两种形式:)

              HEALTHCHECK [OPTIONS] CMD command (check container health by running a command inside the container)      (HEALTHCHECK IOPTIONSI CMD命令(通过在容器内部运行命令来检查容器的运行状况)(HEALTHCHECK这个指令让我们去定义一个command与CMD ,注意这个CMDD是一个固定的关键词,再CMD之后指定一个命令,这个命令用于检查你的主进程工作状态健康与否。使用HEALTHCHECK的时候他的语法格式,这后面还可以加上OPTIONS,也就是说我们做健康的检测是它不是一蹴而就的事情,或者不是一次性的事情,而是什么?周期性任务。你第一次刚启动时检查一切都正常,但后来一不小心你的DOC_ROOT(工作目录,网页目录)文件,被一个bug给删除了,给清空了。那后面访问,是不是也不正常了。因此要持续性检测,比如每隔五秒钟或者每隔三十秒钟,探测一次每隔三十秒钟,探测一次,那到底每隔多长时间检测一次,我可以自己定义。))

              HEALTHCHECK NONE (disable any healthcheck inherited from the base image)      (HEALTHCHECK NONE(禁用从基本映像继承的任何健康检查)(当然如果我们不要做健康的检测,你也可以选HEALTHCHECK NONE ,拒绝任何的健康状态检测,包括它默认的健康中的检测机制。))

        
  HEALTHCHECK(2)
        The options that can appear before CMD are:      (在CMD之前可以显示的选项是:)
              --interval=DURATION (default: 30s)      (--interval-DURATION(默认值:30s))
              --timeout=DURATION (default: 30s)      (--timeout = DURATION(默认值:30秒,(当你发一次请求,服务器一直没响应,我们不能在这一直等他,对不对,所以定义什么?超时时长三十秒)))
              --start-period=DURATION (default: 0s)      (--start-period = DURATION(默认值:0s,(各位正常的健康的检测是这么干的。当你容器一启动,健康检测就开始了,但你刚起的时候这个nginx还没启动起来呢,假如说,容器已经启动了,但容器的进程启动可能它的初始化啊什么的需要一点时间,对吧,假如需要五秒钟,你有没有想过这个时候你上来容器一启动,健康检测就立即进行了,他不是等到主进程启动,那这一次检测,是不是一定是失败的,你一失败就把它kill掉了,他就永远也起不来了,能明白吧,所以这主机进程自我初始化较慢的时候,你认为可能需要三秒钟或者五秒钟,那怎么办呢?我们这个检测可以等这么一段时间,等主进程初始化完以后再去检测,所以等多长时间就叫--start-period。就是你的docker run命令成功了,但我要再等一下,等认为主进程也初始化完成了,在做检测,默认他等不等?不等,好的,如果主进程起的很快,不等是没问题的。但如果像tomcat这种,他去部署一个应用程序,一个内部的jsp站点的话恐怕0秒钟是不行的,你可以尽量等长一点十秒钟以后再检测都是应该的。)))
              --retries=N (default: 3)      (--retries = N(默认值:3,(retries我们也说过,检查一次失败就认为对方失败了,那这也太武断了,对吧,因此我们要检查多少次,默认是三次。)))
        The command“ s exit status indicates the health status of the container. The possible values are:      (该命令的退出状态指示容器的健康状态。 可能的值为:(而后当你指定的这个状态检测命令检测请求发出时,响应只有这三种情况,0表示成功,1表示不检查,2是预留的就没什么意义,大概有这三种返回值。))
              0: success — the container is healthy and ready for use      (0:成功 -容器健康且可以使用)
              1: unhealthy -the container is not working correctly      (1:不健康 -容器无法正常工作)
              2: reserved -do not use this exit code      (2:保留 -请勿使用此退出代码)
        For example      (例如(HEALTHCHECK,--interval=5m每隔五分钟检查一次,--timeout=3s超时时间是3秒,那么我们使用CMD命令,CMD是关键词就放在这里,后面才是命令使用curl对localhost做检测,成功了就成功,exit 1失败了我们只能直接退出而状态为1,OK,这样子就不会依赖于主进程运行与否来判定他健康与否,而是要依赖他是否真正能够正常提供服务来判定的健康与否的 。))
              HEALTHCHECK --interval=5m --timeout=3s \      
                    CMD curl -f http://localhost/ || exit 1



                    当我们基于某个镜像启动一个容器后,这个容器什么时候会退出,什么时候会运行,只要容器的主进程没停止或者没转向后台,那这个容器就不会停,对吧,此前一直在讲,我们的容器就是为了运行单一进程的,所以进程一终止容器就终止了,而进程一停,容器就停了,那么只要进程不停,容器就不会停,对吧,所以我们在里面比如启动了一个nginx,一直处于运行状态,但你有没有想过,有没有可能会发现这种情形。nginx进程运行好好的。但是一不小心你的DOC_ROOT(工作目录,网页目录)指错了,而那个DOC_ROOT(工作目录,网页目录)也确实目录是存在的,大家知道nginx就算你指错DOC_ROOT(工作目录,网页目录)他肯定也不会报错,对吧,至少能运行,但是各位有没有想过,这时候我们客户端去访问网页还能访问到吗?访问不到了,那但是你的容器正常运行的。所以我们平时docker引擎去判定,docker容器健康与否,并不是指这里边的主进程能不能正常提供服务,而仅仅看它是不是再运行的,是这样子跑的。那因此他的这种判断机制并不是真正意义上,确定了我的节点都是健康的,我们需要其他的工具来帮忙监控。比如你可以使用curl靠命令在本机对他发请求,对他发请求,如果请求主页,而且主页的显示内容比如说状态码是200的,而且其中内容确实是你所期望的,那我就认为它是健康的,不认为不健康,而不是看一个进程运行与否,就要判断他健康与否。进程可能启动以后陷入死循环了,因为bug,但服务照样没停下来,对不对,那我们仍然是健康的默认情况下,那如果我们要使用一个外部的其他的能够更好的去测试它的健康状态的工具测一下,再来决定看健康与否,那肯定要精准的多。这里各位应该明白,所以HEALTHCHECK就是让我们来做这个事情的 
# 编辑上面之前的dockerfile
[root@localhost ~]# cd img3
[root@localhost img3]# ls
Dockerfile  entrypoint.sh  index.html
[root@localhost img3]# vi Dockerfile 
FROM nginx:1.14-alpine
LABEL maintainer="haoran <haoran@haorankeji.com>"

ENV NGX_DOC_ROOT="/data/web/html/"

ADD index.html ${NGX_DOC_ROOT}
ADD entrypoint.sh /bin/

EXPOSE 80/tcp      # 使用暴露端口

HEALTHCHECK --start-period=3s CMD wget -O - -q http://${IP:-0.0.0.0}:${PORT:-80}      # 添加HEALTHCHECK指令,使用--interval默认是30秒对吧,就30秒好了,--timeout默认30秒也就30秒好了,就不指了,而后使用--start-period等于3秒钟吧,3秒以后在做检测,--retries默认3次,也使用默认值好了,这三个都使用默认值都不指了也是可以的,后面CMD我们使用什么命令来做检测呢,要确保是容器内有的命令,这里使用wget 这里我们使用-O -显示到屏幕上是没有必要的,你可以给他保存在文件中,或者干脆不加-O了,但是可能会有什么问题,那我们姑且给他留着吧,-q这表示静默模式,而后我们去请求,我们之前定义过地址的变量是吧,直接调用就行了,给个默认值0.0.0.0所有地址,对于端口来讲,如果没有值,默认就使用80

CMD ["/usr/sbin/nginx","-g","daemon off;"]
ENTRYPOINT ["/bin/entrypoint.sh"]


# 构建镜像,版本是v0.3-2
[root@localhost img3]# docker build -t myweb:v0.3-2 ./
Sending build context to Docker daemon  4.096kB
Step 1/9 : FROM nginx:1.14-alpine
 ---> 8a2fb25a19f5
Step 2/9 : LABEL maintainer="haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> 52a1b7eebb86
Step 3/9 : ENV NGX_DOC_ROOT="/data/web/html/"
 ---> Using cache
 ---> ea2913223b1b
Step 4/9 : ADD index.html ${NGX_DOC_ROOT}
 ---> Using cache
 ---> 2b3a35429c52
Step 5/9 : ADD entrypoint.sh /bin/
 ---> Using cache
 ---> 7b3647d14b50
Step 6/9 : EXPOSE 80/tcp
 ---> Running in 163c6f36249c
Removing intermediate container 163c6f36249c
 ---> ae6967c029a4
Step 7/9 : HEALTHCHECK --start-period=3s CMD wget -O - -q http://${IP:-0.0.0.0}:${PORT:-80}
 ---> Running in 37ed2bba7d53
Removing intermediate container 37ed2bba7d53
 ---> 0ef6927fdb16
Step 8/9 : CMD ["/usr/sbin/nginx","-g","daemon off;"]
 ---> Running in 450c2640a7da
Removing intermediate container 450c2640a7da
 ---> 8564f9531a37
Step 9/9 : ENTRYPOINT ["/bin/entrypoint.sh"]
 ---> Running in 584b8a07e82d
Removing intermediate container 584b8a07e82d
 ---> 2c09caced93a
Successfully built 2c09caced93a
Successfully tagged myweb:v0.3-2


# 启动容器,验证,(之前启动过容器杀掉,重新启动)我们定义了它是三秒钟以后在做探测,是吧。可以看到每30秒钟检测一次,状态是200
[root@localhost img3]# docker kill myweb1
myweb1

[root@localhost img3]# docker run --name myweb1 --rm -P -e "PORT=8080" myweb:v0.3-2
127.0.0.1 - - [16/Nov/2020:15:34:36 +0000] "GET / HTTP/1.1" 200 32 "-" "Wget" "-"
127.0.0.1 - - [16/Nov/2020:15:35:06 +0000] "GET / HTTP/1.1" 200 32 "-" "Wget" "-"
127.0.0.1 - - [16/Nov/2020:15:35:36 +0000] "GET / HTTP/1.1" 200 32 "-" "Wget" "-"
127.0.0.1 - - [16/Nov/2020:15:36:06 +0000] "GET / HTTP/1.1" 200 32 "-" "Wget" "-"
127.0.0.1 - - [16/Nov/2020:15:36:36 +0000] "GET / HTTP/1.1" 200 32 "-" "Wget" "-"
127.0.0.1 - - [16/Nov/2020:15:37:06 +0000] "GET / HTTP/1.1" 200 32 "-" "Wget" "-"
127.0.0.1 - - [16/Nov/2020:15:37:36 +0000] "GET / HTTP/1.1" 200 32 "-" "Wget" "-"



# 在新终端交互式登陆进来,查看端口,可以看到我们使用80端口去请求主页也没有任何问题,我们在dockerfile中定义的是${PORT}默认是80,PORT我们给他传的值是8080
[root@localhost ~]# docker exec -it myweb1 /bin/sh
/ # netstat -tnl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     


# 我们有意写一个检测时不同的端口,看他会不会报失败,杀掉之前的进程
[root@localhost ~]# docker kill myweb1
myweb1

# 修改dockerfile文件。
[root@localhost img3]# vi Dockerfile 
FROM nginx:1.14-alpine
LABEL maintainer="haoran <haoran@haorankeji.com>"

ENV NGX_DOC_ROOT="/data/web/html/"

ADD index.html ${NGX_DOC_ROOT}
ADD entrypoint.sh /bin/

EXPOSE 80/tcp

HEALTHCHECK --start-period=3s CMD wget -O - -q http://${IP:-0.0.0.0}:10080/      #(这次我们为了测试效果,这里端口不调变量了,直接给他一个固定端口,比如使用10080,这个端口一定不存在的,他一定失败的,那失败的他一定会报失败的)

CMD ["/usr/sbin/nginx","-g","daemon off;"]
ENTRYPOINT ["/bin/entrypoint.sh"]

# 构建镜像
[root@localhost img3]# docker build -t myweb:v0.3-3 ./
Sending build context to Docker daemon  4.096kB
Step 1/9 : FROM nginx:1.14-alpine
 ---> 8a2fb25a19f5
Step 2/9 : LABEL maintainer="haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> 52a1b7eebb86
Step 3/9 : ENV NGX_DOC_ROOT="/data/web/html/"
 ---> Using cache
 ---> ea2913223b1b
Step 4/9 : ADD index.html ${NGX_DOC_ROOT}
 ---> Using cache
 ---> 2b3a35429c52
Step 5/9 : ADD entrypoint.sh /bin/
 ---> Using cache
 ---> 7b3647d14b50
Step 6/9 : EXPOSE 80/tcp
 ---> Using cache
 ---> ae6967c029a4
Step 7/9 : HEALTHCHECK --start-period=3s CMD wget -O - -q http://${IP:-0.0.0.0}:${PORT:-80}
 ---> Using cache
 ---> 0ef6927fdb16
Step 8/9 : CMD ["/usr/sbin/nginx","-g","daemon off;"]
 ---> Using cache
 ---> 8564f9531a37
Step 9/9 : ENTRYPOINT ["/bin/entrypoint.sh"]
 ---> Using cache
 ---> 2c09caced93a
Successfully built 2c09caced93a
Successfully tagged myweb:v0.3-3


# 启动容器,我们系统上监听的有80,有8080,但就是没有10080,30秒钟以后
[root@localhost img3]# docker run --name myweb1 --rm -P -e "PORT=8080" myweb:v0.3-3


# 新终端查看容器运行状态,可以看到当前状态是starting运行状态
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS                   NAMES
061dc00c9fce        myweb:v0.3-3        "/bin/entrypoint.sh …"   3 seconds ago       Up 2 seconds (health: starting)   0.0.0.0:32786->80/tcp   myweb1


## 在等待几秒钟后,再次 docker container ls,就会看到健康状态变化为了 (healthy),(如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy(不良)))
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS                   NAMES
6743c3231b6d        myweb:v0.3-3        "/bin/entrypoint.sh …"   2 minutes ago       Up 2 minutes (unhealthy)   0.0.0.0:32785->80/tcp   myweb1

SHELL

  SHELL
        The SHELL instruction allows the default shell used for the shell form of commands to be overridden      (SHELL指令允许覆盖用于命令的shell形式的默认shell)

        The default shell on Linux is ["/bin/sh", "-c"]. and on Windows is ["cmd", "/S","/C"]      (Linux上的默认shell是["/bin/sh","-c"]。 在Windows上是["cmd","/S","/C"])

        The SHELL instruction must be written in JSON form in a Dockerfile      (必须在Dockerfile中以JSON形式编写SHELL指令)

              Syntax: SHELL ["executable", "parameters"]      (语法:SHELL [“可执行文件”,“参数”])

        The SHELL instruction can appear multiple times      (SHELL指令可以出现多次)

        Each SHELL instruction overrides all previous SHELL instructions, and affects all subsequent instructions      (每个SHELL指令将覆盖所有先前的SHELL指令,并影响所有后续的指令)

              我们之前说过,无论运行ENTRYPOINT还是CMD还是RUN的时候,如果我们指定的不是json数组格式,它默认都是以什么运行的?shell,而且是/bin/shell -c,还记得吧。如果你的那个基础镜像提供的不是/bin/shell呢,比如我们基础镜像是windows,这个windows需要是powershell是吧,而不是我们/bin/下的shell。像这个时候我们就要把它改成别的shell路径,所以shell这个这个指令,谁来定义运行程序默认要执行的要使用的shell程序。默认运行的是/bin/sh -c,我们可以给他改成别的比如windows下的CMD,"/S","/C"或者powershell什么之类的。只有在运行windows或者你的系统默认有更好的比如zsh或者什么shell而且没有链接的时候,额外去定义是没有问题的,否则使用/bin/sh即可。这个改的机会并不多,了解就行

STOPSIGNAL

  STOHSIGNAL
        The STOPSIGNAL instruction sets the system call signal that will be sent to the container to exit.      (STOPSIGNAL指令设置将被发送到容器退出的系统调用信号。)

        This sianal can be a valid unsigned number that matches a position in the kernl’ s syscall table, for instance 9, or a signal name in the format SIGNAME, for instance SIGKILL.      (该sianal可以是与kernl的系统调用表中的位置匹配的有效无符号数字,例如9,或者是格式为SIGNAME的信号名称,例如SIGKILL。)

        Syntax: STOPSIGNAL signal      (语法:STOPSIGNAL信号)

              我们刚才说过,进程号ID为1的是能够接收包括stop命令,从而能停止主进程的,主进程一停,那么容器就停了。这就是为什么docker stop能停容器的原因。他的这个stop命令发了一个信号是-15的信号,那如果我想换别的信号在这定义,STOPSIGNAL 加信号名signal换别的就行了,比如换成9,一运行docker stop相当于给他强行杀死了或者只有发9信号,它就会停止,发其他信号他就不停了。他只理解你定义的STOPSIGNAL ,这样大多数情况我们也不需要改,所以这个了解就行,知道有这两个指令下。

ARG

  ARG
        The ARG instruction defines a variable that users can pass at build-time to the builder with the docker build command using the--build-ara <varname>=<value> flag.      (ARG指令定义了一个变量,用户可以在构建时使用--build-ara <varname> = <value>标志使用docker build命令将其传递给构建器。)

        If a user specifies a build argument that was not defined in the Dockerfile, the build outputs a warning.      (如果用户指定了未在Dockerfile中定义的构建参数,则构建会输出警告。)

        Syntax: ARG <name>[=<default value>]      (语法:ARG <名称> = <默认值>)

        A Dockerfile may include one or more ARG instructions.      (Dockerfile可能包含一个或多个ARG指令。)

        An ARG instruction can optionally include a default value:      (ARG指令可以选择包含默认值:)

              ARG version-1.14      (ARG版本1.14)

              ARG user =mageedu      (ARG用户=mageedu)

                    ARG叫参数,这个参数跟我前面讲的ENV很相像,除了,ARG在什么时候用呢?ARG是一个变量,但这个变量只是在docker build过程中使用,而且能够为build命令执行时使用--build-arg传过来只在build中使用。那这个功能就使得一个docker file能够适用于较多的不同场景,尤其是应用程序版本变化时,直接传参数(不可取),就能确定我们应该基于这个dockerfile制作哪个版本 。
# 编辑dockerfile
[root@localhost img3]# vi Dockerfile 
FROM nginx:1.14-alpine      # 我们这里使用的基础镜像是nginx:1.14,以后我想换个更新镜像,nginx出新基础镜像了怎么办呢,是不是要改dockerfile,那么如果不行更改可以使用变量引用,但是这里比较悲催的是什么呢?FROM必须得是这一条指令,所以你要定义ARG,也要定义在FROM背后比如(FROM nginx:${suthor} ARG author="1.15-alpine"),所以不行,这个变量不会生效,因为这个变量不存在,调用的太早了。

ARG author="haoran <haoran@haorankeji.com>"      #  dockerfile制作过程中有很多地方可能都要去传变量的,比如说作者,把maintainer换换是不是也行啊。定义author等于一个值,这里是默认的,如果build过程中不手动指定就使用这个。
LABEL maintainer="${author}"      # maintainer直接调用author变量使用${author}

ENV NGX_DOC_ROOT="/data/web/html/"

ADD index.html ${NGX_DOC_ROOT}
ADD entrypoint.sh /bin/

EXPOSE 80/tcp

HEALTHCHECK --start-period=1s CMD wget -O - -q http://${IP:-0.0.0.0}:10080/ || exit 1

CMD ["/usr/sbin/nginx","-g","daemon off;"]
ENTRYPOINT ["/bin/entrypoint.sh"]

# 构建镜像,在build中使用选项--build-arg,如果不指使用的是docker定义的默认值,这里手动指定author为pony <pony@qq.com>
[root@localhost img3]# docker build --build-arg author="pony <pony@qq.com>" -t myweb:v0.3-4 ./
Sending build context to Docker daemon  4.096kB
Step 1/10 : FROM nginx:1.14-alpine
 ---> 8a2fb25a19f5
Step 2/10 : ARG author="haoran <haoran@haorankeji.com>"
 ---> Running in 1ebfe4c77d0a
Removing intermediate container 1ebfe4c77d0a
 ---> cef444fe738a
Step 3/10 : LABEL maintainer="${author}"
 ---> Running in f029335ffb33
Removing intermediate container f029335ffb33
 ---> e0f8969f3154
Step 4/10 : ENV NGX_DOC_ROOT="/data/web/html/"
 ---> Running in 86c82d9d2130
Removing intermediate container 86c82d9d2130
 ---> 549cfdf16766
Step 5/10 : ADD index.html ${NGX_DOC_ROOT}
 ---> 10e651dcf1a0
Step 6/10 : ADD entrypoint.sh /bin/
 ---> 180e5787ca4e
Step 7/10 : EXPOSE 80/tcp
 ---> Running in 9634768dc02e
Removing intermediate container 9634768dc02e
 ---> d41834d57afc
Step 8/10 : HEALTHCHECK --start-period=1s CMD wget -O - -q http://${IP:-0.0.0.0}:10080/ || exit 1
 ---> Running in 5e4616173042
Removing intermediate container 5e4616173042
 ---> 0d9f42eb21ac
Step 9/10 : CMD ["/usr/sbin/nginx","-g","daemon off;"]
 ---> Running in 38684ab5bc9d
Removing intermediate container 38684ab5bc9d
 ---> 5c5681ef3cc8
Step 10/10 : ENTRYPOINT ["/bin/entrypoint.sh"]
 ---> Running in e14622c225df
Removing intermediate container e14622c225df
 ---> 61e5c6cbafc5
Successfully built 61e5c6cbafc5
Successfully tagged myweb:v0.3-4



# 验证一下,查看镜像信息(元数据当中他的作者信息有没有改掉.可以看到作者信息改掉了)
[root@localhost ~]# docker image inspect myweb:v0.3-4
            "Labels": {
                "maintainer": "pony <pony@qq.com>"
            },

ONBUILD

  ONBUILD(ONBUILD就是在build时,意思是在build过程中)
        用于在Dockerfile中定义一个触发器(什么叫在dockerfile中定义一个触发器呢。我们刚才定义了dockerfile,dockerfile中你写的每一条指令是什么时候运行呢?在docker build时执行是吧,而nobuild是什么意思呢?nobuild,比如你在Dockerfile中写了个nobuild,nobuild这条指令是不会执行的。他不是在当前build中执行的,而是藏一个后门在其他人build中执行。什么意思呢?你有了dockerfile, build时候生成一个镜像文件了,OK,那么有没有人会基于你的镜像再去写dockerfile,说我基于一个镜像,from跟着是你做的镜像,有没有可能性啊?你做的镜像也可以被别人当做基础镜像使用的。而你做的镜像里边如果写个ONBUILD后面跟了个dockerfile的指令,这个指令在你自己做image时不会执行。而是当别人把你做的镜像当做基础镜像并写在dockerfile,在它的build时会执行,叫延时执行。所以我们说这是个触发器。)

        Dockerfile用于build映像文件,此映像文件亦可作为base image被另一个Dockerfile用作FROM指令的参数,并以之构建新的映像文件

        在后面的这个Dockerfile中的FROM指令在build过程中被执行时,将会“触发"创建其base image的Dockerfile文件中的ONBUILD指令定义的触发器

        Syntax(语法)

              ONBUILD <INSTRUCTION>      (ONBUILD 后面给的应该是一个正常的dockerfile的INSTRUCTION比如像CMD呀,RUN呀,一般是RUN,ADD也可以但是一般不会用,如果添加一个互联网的url就可以了,因为本地的,他可能没有可能会报错)

        尽管任何指令都可注册成为触发器指令(几乎dockerfile指令都能放在ONBUILD背后),但ONBUILD不能自我嵌套(但是要注意的是,第一ONBUILD不能自我嵌套比如说ONBUILD不能在写一个ONBUILD),且不会触发FROM和MAINTAINER指令(第二,ONBUILD一般不能使用FROM和MAINTAINER指令,其他指令都没问题,虽然没有问题但你使用COPY的时候,可能会有错误发生,因为别人build时肯可能没有你指定的源文件,所以多数情况下我们的ONBUILD多是执行RUN或者ADD通过url去下载,但是通过互联网下载也有问题,别人可能访问不了互联网呢,run时候大多没有问题,所以我们一般使用run

        使用包含ONBUILD指令的Dockerfile构建的镜像应该使用特殊的标签,例如ruby:2.0-onbuild

        在ONBUILD指令中使用ADD或COPY指令应该格外小心,因为新构建过程的上下文在缺少指定的源文件时会失败
# 编辑dockerfile
[root@localhost img3]# vi Dockerfile 
FROM nginx:1.14-alpine

ARG author="haoran <haoran@haorankeji.com>"
LABEL maintainer="${author}"

ENV NGX_DOC_ROOT="/data/web/html/"

ADD index.html ${NGX_DOC_ROOT}
ADD entrypoint.sh /bin/

EXPOSE 80/tcp

HEALTHCHECK --start-period=1s CMD wget -O - -q http://${IP:-0.0.0.0}:10080/ || exit 1

ONBUILD ADD http://nginx.org/download/nginx-1.18.0.tar.gz /usr/local/src/      # 添加ONBUILD意思是谁要基于我的镜像做,我们给他运行什么命令呢?使用ADD 给他下载一个文件,到/usr/local/src/目录下

CMD ["/usr/sbin/nginx","-g","daemon off;"]
ENTRYPOINT ["/bin/entrypoint.sh"]


# 构建镜像,构建过程中有一个ONBUILD 他执行的是ADD 下载一个文件,但是他下载了没有?没有,如果下载了,会有一个下载进度的是吧,所以他没有执行 
[root@localhost img3]# docker build --build-arg author="pony <pony@qq.com>" -t myweb:v0.3-5 ./
Sending build context to Docker daemon  4.096kB
Step 1/11 : FROM nginx:1.14-alpine
 ---> 8a2fb25a19f5
Step 2/11 : ARG author="haoran <haoran@haorankeji.com>"
 ---> Using cache
 ---> cef444fe738a
Step 3/11 : LABEL maintainer="${author}"
 ---> Using cache
 ---> e0f8969f3154
Step 4/11 : ENV NGX_DOC_ROOT="/data/web/html/"
 ---> Using cache
 ---> 549cfdf16766
Step 5/11 : ADD index.html ${NGX_DOC_ROOT}
 ---> Using cache
 ---> 10e651dcf1a0
Step 6/11 : ADD entrypoint.sh /bin/
 ---> Using cache
 ---> 180e5787ca4e
Step 7/11 : EXPOSE 80/tcp
 ---> Using cache
 ---> d41834d57afc
Step 8/11 : HEALTHCHECK --start-period=1s CMD wget -O - -q http://${IP:-0.0.0.0}:10080/ || exit 1
 ---> Using cache
 ---> 0d9f42eb21ac
Step 9/11 : ONBUILD ADD http://nginx.org/download/nginx-1.18.0.tar.gz /usr/local/src/
 ---> Running in 06f2eeda462a
Removing intermediate container 06f2eeda462a
 ---> 929bfb5b4433
Step 10/11 : CMD ["/usr/sbin/nginx","-g","daemon off;"]
 ---> Running in 55a6abe971bf
Removing intermediate container 55a6abe971bf
 ---> 98b575912f72
Step 11/11 : ENTRYPOINT ["/bin/entrypoint.sh"]
 ---> Running in 544676f59d30
Removing intermediate container 544676f59d30
 ---> bc83d4054d69
Successfully built bc83d4054d69
Successfully tagged myweb:v0.3-5

# 在做一个新的镜像,创建目录和文件,我们弄的简单一点,直接引用一个基础镜像,创建一个目录就行了,啥也没做,给他做成镜像
[root@localhost ~]# mkdir img4
[root@localhost ~]# cd img4
[root@localhost img4]# vi Dockerfile
FROM myweb:v0.3-5

RUN mkdir /tmp/test


# 构建镜像,可以看到构建镜像过程中在下载文件,因此我们基于这个镜像启动文件的时候,可以看到哪个tar包的
[root@localhost img4]# docker build -t test:v0.1-1 ./
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM myweb:v0.3-5
# Executing 1 build trigger
Downloading [==================================================>]   1.04MB/1.04MB
 ---> 601eb789f6ba
Step 2/2 : RUN mkdir /tmp/test
 ---> Running in b1403a4831a1
Removing intermediate container b1403a4831a1
 ---> 494e756a9f4e
Successfully built 494e756a9f4e
Successfully tagged test:v0.1-1

# 启动镜像,验证一下,看一下/usr/local/src/这个路径下是否有文件。有的,这就叫ONBUILD,在自己的dockerfile中写的指令,但不是在自己执行docker build过程中执行,而是被别人,基于他做base image时被触发。
[root@localhost img4]# docker run --name test1 --rm test:v0.1-1 ls /usr/local/src/
nginx-1.18.0.tar.gz

docker build

  docker build命令制作镜像时,可以指定很多选项,但是所有选项几乎都是可省的,但你要指明你的必要的build时,build目录路径是哪或者URL,对于我们来讲不用搞那么复杂,使用path,这个path就是你的dockerfile文件所在的父目录。
# 做完以后我们的镜像应该是只有ID号,做完镜像,镜像默认没有标签,也没有所属仓库,对不对,-t我们可以直接打标签,前面是仓库名,后面是标签名 ,然后指定目录./当前目录
[root@localhost img1]# docker build -t tinyhttpd:v0.1-1 ./ 
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM busybox:latest      (现在开始做镜像了,第一步先去分析FROM指令,获取busybox:latest基础镜像)
 ---> f0b02e9d092d
Step 2/3 : MAINTAINER "haoran <haoran@haorankeji.com>"      (第二步加上MAINTAINER )
 ---> Running in ecddc26b2cd2
Removing intermediate container ecddc26b2cd2
 ---> 3f686ae096ac
Step 3/3 : COPY index.html /data/web/html/      (第三步复制文件进进来)
 ---> e0177de0e900
Successfully built e0177de0e900      (告诉你做好了镜像)
Successfully tagged tinyhttpd:v0.1-1      (并打个标叫tinyhttpd:v0.1-1)

# 查看镜像(它里面应该有我们打包进来的文件的)
[root@localhost img1]# docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
tinyhttpd            v0.1-1              e0177de0e900        3 minutes ago       1.23MB



# docker build帮助
[root@localhost img1]# docker build -h
Flag shorthand -h has been deprecated, please use --help

Usage:  docker build [OPTIONS] PATH | URL | -

Build an image from a Dockerfile

Options:
      --add-host list           Add a custom host-to-IP mapping (host:ip)
      --build-arg list          Set build-time variables
      --cache-from strings      Images to consider as cache sources
      --cgroup-parent string    Optional parent cgroup for the container
      --compress                Compress the build context using gzip
      --cpu-period int          Limit the CPU CFS (Completely Fair Scheduler) period
      --cpu-quota int           Limit the CPU CFS (Completely Fair Scheduler) quota
  -c, --cpu-shares int          CPU shares (relative weight)      (CPU份额(相对重量))
      --cpuset-cpus string      CPUs in which to allow execution (0-3, 0,1)
      --cpuset-mems string      MEMs in which to allow execution (0-3, 0,1)
      --disable-content-trust   Skip image verification (default true)
  -f, --file string             Name of the Dockerfile (Default is 'PATH/Dockerfile')
      --force-rm                Always remove intermediate containers
      --iidfile string          Write the image ID to the file
      --isolation string        Container isolation technology
      --label list              Set metadata for an image
  -m, --memory bytes            Memory limit      (内存限制)
      --memory-swap bytes       Swap limit equal to memory plus swap: '-1' to enable unlimited swap
      --network string          Set the networking mode for the RUN instructions during build (default "default")
      --no-cache                Do not use cache when building the image
  -o, --output stringArray      Output destination (format: type=local,dest=path)
      --platform string         Set platform if server is multi-platform capable
      --progress string         Set type of progress output (auto, plain, tty). Use plain to show container output (default "auto")
      --pull                    Always attempt to pull a newer version of the image
  -q, --quiet                   Suppress the build output and print image ID on success
      --rm                      Remove intermediate containers after a successful build (default true)
      --secret stringArray      Secret file to expose to the build (only if BuildKit enabled): id=mysecret,src=/local/secret
      --security-opt strings    Security options
      --shm-size bytes          Size of /dev/shm
      --squash                  Squash newly built layers into a single new layer
      --ssh stringArray         SSH agent socket or keys to expose to the build (only if BuildKit enabled) (format:
                                default|<id>[=<socket>|<key>[,<key>]])
      --stream                  Stream attaches to server to negotiate build context
  -t, --tag list                Name and optionally a tag in the 'name:tag' format      (名称以及“ name:tag”格式的标签(可选))
      --target string           Set the target build stage to build.
      --ulimit ulimit           Ulimit options (default [])

podman

  什么是Podman?简而言之:alias docker -podman,是CentOs 8新集成的功能,未来代替docker

  Podman是一个为Kubernetes而生的开源的容器管理工具,原来是CRI-O (即容器运行时接口CRI和开放容器计划OCI)项目的一部分,后来被分离成一个单独的项目叫libpod,其可在大多数Linux平台上使用,它是一种无守护程序的容器引擎,用于在Linux系统上开发,管理和运行任何符合Open Container Initiative (OCI)标准的容器和容器镜像。

  Podman提供了一个与Docker兼容的命令行前端, Podman CLI里面87%的指令都和Docker CL相同,因此该前端可以简单地为Docker CL剔名,即" alias docker = podman"

  官方地址:https://podman.io/
  
  项目地址:https://github.com/containers/libpod

  
  Podman和docker不同之处
        docker需要在我们的系统上运行一个守护进程(docker daemon),而podman不需要
              (docker是有专门的服务的,服务名就叫dockerd(systemctl status dockerd)他是以专门的服务器的方式运行的,但是podman没有服务器架构,他就是一个程序,这个程序在必要的情况下启动,不需要的情况下就不启动,所以他没那么复杂,)
        启动容器的方式不同:
              docker c1i命令通过API跟Docker Engine (引擎)交互告诉它我想创建一个container,然后docken Engine才会调用ocI container runtime (runc)来启动一个container,这代表container的process(进程)不会是Docker cLr的chi1d process(子进程),而是Docker Engine的child process.
              Podman是直接给OCI containner runtime (runc)进行交互来创建container的,所以container process直接是podman的chi1d process.
              (他的启动是直接通过podman和你的容器直接打交道,而docker由于是基于c/s架构的,所以我们用户的应用程序他是通过docker引擎,docker引起说白了就是docker服务器来和容器进行通讯的,也就是说用户通过客户端先连接docker的引擎,然后在通过引擎在去和容器进行信息交换,这样的话就隔了一层他是通过引擎来实现的,      而podman他没有这个东西他可以直接进行交互,也就是说podman直接通过接口,直接连接到容器上和容器进行交互,所以他更直接更方便)
        因为docke有docker daemon,所以docker启动的容器支持--restart策略,但是podman不支持
        docker需要使用root用户来创建容器, podman既可以由root用户运行,也可以由非特权用户运行

安装podman

# centos8上进行安装,centos8默认没有docker(虽然没有docker的包但是可以安装(CentOS Linux release 8.1.1911 (Core)测试并不能安装),但是当在centos8上安装docker的时候,实际上他并不会真的给你装docker实际上安装的是podman-docker,而podman-docker实际上伪装的docker就是用podman来模拟docker实际上他并不是真的docker,安装后他实际上并没有这个docker包,他提供了一个docker程序但这个docker程序实际上是podman的一个脚本而已,他间接的调用了podman)
[root@localhost ~]# yum info docker
CentOS-8 - AppStream                                                                                 6.2 kB/s | 4.3 kB     00:00    
CentOS-8 - AppStream                                                                                 921 kB/s | 5.8 MB     00:06    
CentOS-8 - Base                                                                                      5.0 kB/s | 3.9 kB     00:00    
CentOS-8 - Base                                                                                      925 kB/s | 2.2 MB     00:02    
CentOS-8 - Extras                                                                                    1.3 kB/s | 1.5 kB     00:01    
CentOS-8 - Extras                                                                                     11 kB/s | 8.1 kB     00:00    
错误:没有匹配的软件包可以列出
[root@localhost ~]# yum info docker-ce
上次元数据过期检查:0:00:15 前,执行于 2020年10月27日 星期二 13时40分32秒。
错误:没有匹配的软件包可以列出

# 安装podman
[root@localhost ~]# dnf -y install podman

# 查看命令路径
root@localhost ~]# which podman
/usr/bin/podman

配置镜像源

  默认镜像库
        文件 /etc/containers/registries.conf 是设置镜像地址配置文件,默认会搜索 ['registry.access.redhat.com', 'registry.redhat.io', 'docker.io', 'quay.io']等几个镜像库。

        不过,这几个镜像库下载速度非常慢,可以说是基本无法顺利的将镜像拉下来。

        所以,需要修改修改镜像库配置文件,也就是大家说的镜像加速方案。

  镜像加速

        只需2个步骤实现镜像加速:

              1.改名并备份好文件:/etc/containers/registries.conf.bak

              2.再新建一个空的 registries.conf 文件,插入如下内容
                    unqualified-search-registries = ["docker.io"]

                    [[registry]]
                    prefix = "docker.io"
                    location = "******.mirror.aliyuncs.com"

              3.修改 location
                   网易:https://hub-mirror.c.163.com/

                   阿里云:https://<你的ID>.mirror.aliyuncs.com

                   七牛云加速器:https://reg-mirror.qiniu.com

              4.保存
# 修改podman镜像源(podman默认的镜像拉取镜像地址第一个是红帽的,从红帽去拉去,而且可能要都要涉及到红帽的一个付费用户才行的,最后一个才是正儿八经的docker的镜像仓库的源,所以把他这个顺序调整一下,把docker的镜像仓库的源调整到第一位置上去,如果不调整的话,他装软件他会优先跑到上去,结果红帽的连接不上去,就会导致安装特别慢)
[root@localhost ~]# vim /etc/containers/registries.conf 
registries = ['docker.io','registry.access.redhat.com', 'registry.redhat.io']

# 镜像加速
## 备份文件
[root@localhost ~]# cp /etc/containers/registries.conf /etc/containers/registries.conf.bak

## 编辑配置文件(直接删除或者注释掉之前的,或者以registries.conf命名的新文件,只需要这一段)
[root@localhost ~]# vim /etc/containers/registries.conf
unqualified-search-registries = ["docker.io"]
  
[[registry]]
prefix = "docker.io"
location = "hub-mirror.c.163.com/"

run跑一个mariadb容器

  启动容器及容器端口映射
        我们最终需要的实际上是宿主机暴露 3306 端口,因此将 MariaDB 容器的 3306 端口与宿主机的端口进行映射。

        容器的启动方式是 -d 后台守护进程启动,启动的时候需要指定 ROOT 密码或者是选择空 ROOT 密码。

              --name:给了一个 mariadb 的别名
              MYSQL_ROOT_PASSWORD:设置 root 密码
                    “数据库未初始化,密码没设置。你需要设置MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD三个中的任意一项”

                          其中 MYSQL_ROOT_PASSWORD即root账户的密码。

                                MYSQL_ALLOW_EMPTY_PASSWORD即允许密码为空。

                                MYSQL_RANDOM_ROOT_PASSWORD随机一个root账户密码。****


        启动成功之后,实际上就能够在在宿主机直接访问 mysql 了。
              保证宿主机安装了 mysql-client

        这里遇到一个问题,如果不指定 -h 为某个 ip(本机指定为 127.0.0.1 或 局域网 ip),则无法连接,如果指定 -h 为 localhost也无法连接
              $ mysql -uroot -proot
              报错:ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock' (2)
                    区别是mysql在使用-hlocalhost选择使用的连接方式为unix-socket,而你没有在mysql的配置文件中启用:socket=/var/run/mysqld/mysqld.sock,导致这个文件不存在而使连接失败;而-hIP的形式使用的是tcp-socket

              加ip连接:
                    $ mysql -h127.0.0.1 -uroot -proot
                    $ mysql -h192.168.1.130 -uroot -proot
# 启动容器及容器端口映射
[root@localhost ~]# podman  run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root --name mariadb mariadb:latest

# 查看容器状态并查看监听的地址和端口,并查看本地暴露出来的地址和端口
[root@localhost ~]# podman ps 
CONTAINER ID  IMAGE                             COMMAND  CREATED        STATUS            PORTS                   NAMES
295e75fecb73  docker.io/library/mariadb:latest  mysqld   5 minutes ago  Up 5 minutes ago  0.0.0.0:3306->3306/tcp  mariadb

[root@localhost ~]# netstat -tunlp |grep 3306
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN      31331/conmon      

# 访问数据库(需要实现安装mariadb客户端程序,dnf -y install mariadb)
[root@localhost ~]# mysql -h127.0.0.1 -uroot -proot

允许远程访问数据库(方法一,更换caching_sha2_password身份验证插件)

  问题:

        我在使用navicat 进行MySQL管理是出现了。2059 - authentication plugin 'caching_sha2_password' -navicat连接异常。这个错误。

        这个错误的原因是由于MySQL8.0之后的加密规则为caching_sha2_password.而在此之前的加密规则为mysql_native_password。

        可以将加密规则改成mysql_native_password来。



  C:\mysqldata>mysql -h 127.0.0.1 -P 13306 -uroot -p
  Enter password: ****
  ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded: ÕÒ²»µ½Ö¸¶¨µÄÄ£¿é¡£

        查看一下加密的方式
              MySQL [(none)]> show variables like 'default_authentication_plugin';
              +-------------------------------+-----------------------+
              | Variable_name                 | Value                 |
              +-------------------------------+-----------------------+
              | default_authentication_plugin | caching_sha2_password |
              +-------------------------------+-----------------------+
              1 row in set (0.02 sec)

        查看本地mysql用户的信息(可以看到root账户的加密方式是caching_sha2_password)
              MySQL [(none)]> select host,user,plugin from mysql.user;
              +------------+------------------+-----------------------+
              | host       | user             | plugin                |
              +------------+------------------+-----------------------+
              | %          | root             | mysql_native_password |
              | 172.17.0.2 | root             | caching_sha2_password |
              | localhost  | mysql.infoschema | caching_sha2_password |
              | localhost  | mysql.session    | caching_sha2_password |
              | localhost  | mysql.sys        | caching_sha2_password |
              | localhost  | root             | caching_sha2_password |
              +------------+------------------+-----------------------+
              6 rows in set (0.00 sec)

  解决方案:

        1.进入mysql容器

              docker exec -it mysql2 /bin/bash

        2.进入mysql

              mysql -uroot -pmima

        3.修改密码,指定加密规则

              ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root';

        4.远程访问数据库(修改后,访问容器内的数据库,就算是宿主机的也要使用网络地址127.0.0.1或本机ip,否者默认使用的是本地mysql.sock会连不上)

              mysql -uroot -h127.0.0.1 -proot 

停止容器

[root@ecs-kc1-large-2-linux-20200825091713 ~]# podman ps -a
CONTAINER ID  IMAGE                           COMMAND               CREATED      STATUS          PORTS                 NAMES
e01cb058cfd5  docker.io/library/nginx:1.16.0  nginx -g daemon o...  8 weeks ago  Up 8 weeks ago  0.0.0.0:8080->80/tcp  nginx
4d3f7adfaf95  docker.io/library/httpd:latest  httpd-foreground      8 weeks ago  Up 8 weeks ago  0.0.0.0:80->80/tcp    quirky_swartz
[root@ecs-kc1-large-2-linux-20200825091713 ~]# podman stop e01cb058cfd5 4d3f7adfaf95
e01cb058cfd54d503c78c56e8731e7dc29047d2af9971208504d1f61090a001f
4d3f7adfaf95c32fc8b5178f5ca54b70943f3e57ff4230bc8f25435b367e293a
[root@ecs-kc1-large-2-linux-20200825091713 ~]# podman ps -a
CONTAINER ID  IMAGE                           COMMAND               CREATED      STATUS                    PORTS                 NAMES
e01cb058cfd5  docker.io/library/nginx:1.16.0  nginx -g daemon o...  8 weeks ago  Exited (0) 3 seconds ago  0.0.0.0:8080->80/tcp  nginx
4d3f7adfaf95  docker.io/library/httpd:latest  httpd-foreground      8 weeks ago  Exited (0) 3 seconds ago  0.0.0.0:80->80/tcp    quirky_swartz


# 使用方法
[root@ecs-kc1-large-2-linux-20200825091713 ~]# podman stop --help
Stop one or more containers(停止一个或多个容器)

Description:(描述:)
  Stops one or more running containers.  The container name or ID can be used.(停止一个或多个运行中的容器。 可以使用容器名称或ID。)

  A timeout to forcibly stop the container can also be set but defaults to 10 seconds otherwise.(也可以设置强制停止容器的超时,否则默认为10秒。)

Usage:(用法:)
  podman stop [flags] CONTAINER [CONTAINER...](podman stop [标签] 容器 [容器...])

Examples:(例子:)
  podman stop ctrID
  podman stop --latest(podman stop --最新)
  podman stop --timeout 2 mywebserver 6e534f14da9d(podman stop --超时 2 容器 容器)

Flags:(标志)
  -a, --all            Stop all running containers(停止所有正在运行的容器)
  -l, --latest         Act on the latest container podman is aware of(有关最新的容器podman的信息)
      --time uint      Seconds to wait for stop before killing the container (default 10)(在终止容器之前等待停止的秒数(默认值为10))
  -t, --timeout uint   Seconds to wait for stop before killing the container (default 10)(在终止容器之前等待停止的秒数(默认为10))

容器内安装yum

  使用busybox容器实现安装yum(失败,失败原因,busybox提供的没有编译环境,python和yum的源码包可以通过下载使用,但是无法解决源码编译)
# 启动,进入busybox容器
[root@localhost ~]# docker  run --name mybusybox -it busybox:latest

# yum是由python实现的,先下载,安装python
~ # wget https://www.python.org/ftp/python/3.9.0/Python-3.9.0.tar.xz

# 解压,压缩方法要于解压方法保持一直,否则报错
~ # tar -xvf Python-3.9.0.tar.xz 

# 编译(报错,原因没有编译环境)
./configure --prefix=/usr/local --enable-shared
make
make install

# 下载最新的yum-3.2.28.tar.gz并解压
wget http://yum.baseurl.org/download/3.2/yum-3.2.28.tar.gz
tar xvf yum-3.2.28.tar.gz

# 进入目录,运行安装
~ # cd Python-3.9.0
~ # yummain.py install yum 

# 如果结果提示错误: CRITICAL:yum.cli:Config Error: Error accessing file for config file:///etc/ 可能是原来是缺少配置文件。在etc目录下面新建yum.conf文件,然后再次运行 yummain.py install yum,顺利完成安装。

# 最后更新系统。
yum check-update      #升级所有包,同时也升级软件和内核系统
yum update            #升级所有包,同时也升级软件和内核系统
yum -y upgrade        #只升级所有包,不升级软件和系统内核。
yum clean all         #列出所有可更新的软件清单命令

容器内安装netstat

[root@62c210d9160e /]# yum -y install net-tools
posted @ 2020-10-21 14:32  给文明以岁月  阅读(504)  评论(0编辑  收藏  举报
----------------------------------------------------------