Docker 知识点全面解析

Docker简介

为什么会有 Docker 出现?

一款产品开发完毕之后想要上线会经历很多步骤,从操作系统到运行环境、再到应用配置等等,都是开发团队和运维团队所需要关心的东西。同时这也是很多互联网公司都不得不面对的问题,特别是各种版本的迭代之后,不同版本环境的兼容,对运维人员都是考验。

环境配置如此麻烦,换一台机器,就要重来一次,费力费时。很多人想到,能不能从根本上解决问题,软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。所以Docker便出现了,它给出了一个标准化的解决方案,开发人员利用 Docker 可以消除 "明明在我的机器上运行的好好的" 这样的问题。

之前在服务器配置一个应用的运行环境,要安装各种软件。安装和配置这些东西有多麻烦就不说了,它还不能跨平台。假如我们是在 Windows 上安装的这些环境,到了 Linux 又得重新装。况且就算不跨操作系统,换另一台同样操作系统的服务器,要移植应用也是非常麻烦的。

传统上认为,软件开发 / 测试结束后,所产出的成果即是程序或是能够编译执行的二进制字节码等(java为例)。而为了让这些程序可以顺利执行,开发团队也得准备完整的部署文件,让维运团队得以部署应用程式,开发需要清楚的告诉运维部署团队,用的全部配置文件+所有软件环境。不过,即便如此,仍然常常发生部署失败的状况。Docker镜像的设计,使得Docker得以打破过去「程序即应用」的观念。透过镜像(images)将作业系统核心除外,运作应用程式所需要的系统环境,由下而上打包,达到应用程式跨平台间的无缝接轨运作。

Docker 理念

Docker是基于Go语言实现的云开源项目,Docker的主要目标是"Build,Ship and Run Any App,Anywhere",也就是通过对应用组件的封装、分发、部署、运行等生命周期的管理,使用户的APP(可以是一个WEB应用或数据库应用等等)及其运行环境能够做到 "一次封装,到处运行"。

Linux 容器技术的出现就解决了这样一个问题,而 Docker 就是在它的基础上发展过来的。将应用运行在 Docker 容器上面,而 Docker 容器在任何操作系统上都是一致的,这就实现了跨平台、跨服务器。只需要一次配置好环境,换到别的机子上就可以一键部署好,大大简化了操作。

虚拟机和容器

之前的虚拟机技术

虚拟机(virtual machine)就是带环境安装的一种解决方案,它可以在一种操作系统里面运行另一种操作系统,比如在Windows 系统里面运行 Linux 系统。应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其他部分毫无影响。这类虚拟机完美的运行了另一套系统,能够使应用程序,操作系统和硬件三者之间的逻辑不变。 但是它有如下缺点:

  • 1. 资源占用多
  • 2. 冗余步骤多
  • 3. 启动慢

容器虚拟化技术

由于前面虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。有了容器,就可以将软件运行所需的所有资源打包到一个隔离的容器中。容器与虚拟机不同,不需要捆绑一整套操作系统,只需要软件工作所需的库资源。系统因此而变得高效轻量并保证部署在任何环境中的软件都能始终如一地运行。

所以 传统虚拟机 和 容器 的区别就很明显了:

  • 传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程
  • 而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便; 每个容器之间互相隔离,每个容器有自己的文件系统 ,容器之间进程不会相互影响,能区分计算资源。

Docker特点

一次构建、到处运行。

  • 更快速的应用交付和部署

传统的应用开发完成后,需要提供一堆安装程序和配置说明文档,安装部署后需根据配置文档进行繁杂的配置才能正常运行。Docker化之后只需要交付少量容器镜像文件,在正式生产环境加载镜像并运行即可,应用安装配置在镜像里已经内置好,大大节省部署配置和测试验证时间。

  • 更便捷的升级和扩缩容

随着微服务架构和Docker的发展,大量的应用会通过微服务方式架构,应用的开发构建将变成搭乐高积木一样,每个Docker容器将变成一块“积木”,应用的升级将变得非常容易。当现有的容器不足以支撑业务处理时,可通过镜像运行新的容器进行快速扩容,使应用系统的扩容从原先的天级变成分钟级甚至秒级。

  • 更简单的系统运维

应用容器化运行后,生产环境运行的应用可与开发、测试环境的应用高度一致,容器会将应用程序相关的环境和状态完全封装起来,不会因为底层基础架构和操作系统的不一致性给应用带来影响,产生新的BUG。当出现程序异常时,也可以通过测试环境的相同容器进行快速定位和修复。

  • 更高效的计算资源利用

Docker是内核级虚拟化,其不像传统的虚拟化技术一样需要额外的 Hypervisor 支持,所以在一台物理机上可以运行很多个容器实例,可大大提升物理服务器的CPU和内存的利用率。

而现在有大量的企业都在应用 Docker,我们熟知的大型公司都在使用。比如:京东、腾讯,内部都在大量的使用 Docker,特别是腾讯。我们知道 Kubernetes(K8S)是一个容器编排管理工具,而腾讯则是对其进行了封装,也就是 TKE(Tencent K8S Engine),专门用来管理内部的容器。

Docker安装

首先 docker 肯定要安装在 Linux 服务器上,这里我以我阿里云上的 CentOS7 为例。安装方式:直接 yum install docker -y 即可。

安装完毕之后,我们来通过 systemctl start docker 命令来启动 docker。

如果后续操作发现报错:Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?,那么说明你没有启动 Docker,需要执行 systemctl start docker。

然后我们输入 docker version 即可查看 docker的版本。

然后我们来配置一下镜像加速,因为我们后面要不停地拉取镜像,而默认是从国外的网站进行拉取,所以速度会很慢。我们需要编辑 /etc/docker/daemon.json 文件,在里面输入如下内容:

{
  "registry-mirrors": ["https://ww7q1ht1.mirror.aliyuncs.com"]
}

表示从阿里云上面拉取镜像,当然这个链接需要去阿里云上获取,当然上面的链接你也可以使用。

然后别忘记重启 Docker,systemctl restart docker。

docker 安装之后如果不用了,那么要如何卸载呢?

  • 1. systemctl stop docker: 停止docker服务
  • 2. yum remove -y docker: 卸载docker
  • 3. rm -rf /var/lib/docker: 删除docker相关的残留文件

Docker核心概念和底层原理

核心概念

Docker主要有三个核心概念:

镜像(image)

Docker 镜像(Image)就是一个只读的模板。镜像可以用来创建 Docker 容器,一个镜像可以创建很多容器。

容器(container)

Docker 利用容器(Container)独立运行的一个或一组应用,容器是用镜像创建的运行实例。它可以被启动、开始、停止、删除,每个容器都是相互隔离的、保证安全的平台。可以把容器看做是一个简易版的 Linux 环境(包括root用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。容器的定义和镜像几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的(后面说)。

仓库(repository)

仓库(Repository)是集中存放镜像文件的场所,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag)。

仓库分为公开仓库(Public)和私有仓库(Private)两种形式。最大的公开仓库是 Docker Hub(https://hub.docker.com/) ,存放了数量庞大的镜像供用户下载。而国内的公开仓库则包括阿里云 、网易云等。

总结

需要正确的理解 镜像 / 容器 / 仓库 这几个概念,Docker 本身是一个容器运行载体或称之为管理引擎。我们把应用程序和配置依赖打包好形成一个可交付的运行环境,这个打包好的运行环境就叫做 image镜像文件。只有通过这个镜像文件才能生成 Docker 容器。image 文件可以看作是容器的模板,Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

而 image 文件生成的容器实例,本身也是一个文件。一个容器运行一种服务,当我们需要的时候,就可以通过 Docker 客户端创建一个对应的运行实例,也就是我们的容器。

至于仓储,就是放了一堆镜像的地方,我们可以把镜像发布到仓储中,需要的时候从仓储中拉下来就可以了。

底层原理

Docker 是怎么工作的?

Docker 是一个 Client-Server 结构的系统,Docker守护进程运行在主机上,然后我们通过 Socket 连接从客户端访问,守护进程从客户端接受命令并管理运行在主机上的容器。我们输入命令,Docker 通过 Client 将我们的命令传给 Server,然后守护进程来管理 Docker 所创建的容器,比如删除、重启等等。

所以 Docker 的 logo 很形象,一个鲸鱼飘在大海里,上面背着很多的集装箱。这个大海就是我们的宿主机,直接使用宿主机的资源,大鲸鱼就相当于是 Docker,鲸鱼上面的集装箱就是一个个的容器,里面运行着各种服务,而且每个集装箱都是相互隔离的,不会对其他的集装箱造成污染。

为什么 Docker 比虚拟机快

1)docker有着比虚拟机更少的抽象层。由于 Docker 不需要 Hypervisor 实现硬件资源虚拟化,运行在 Docker 容器上的程序使用的都是实际物理机的硬件资源。因此在CPU、内存利用率上 Docker 将会在效率上有明显优势。

2)Docker 利用的是宿主机的内核,而不需要 Guest OS。因此,当新建一个容器时,Docker 不需要和虚拟机一样重新加载一个操作系统内核。从而避免了引导、加载操作系统内核这个比较费时费资源的过程,当新建一个虚拟机时,虚拟机软件需要加载 Guest OS,这个新建过程是分钟级别的。而 Docker 由于直接利用宿主机的操作系统,则省略了这个过程,因此新建一个 Docker 容器只需要几秒钟。

以上是 Docker 的整体架构图,我们看到它和 Redis 是类似的,都是 Client-Server 架构。Docker 内部有一个守护进程,我们通过 Client 发送命令,服务端的守护进程来执行。比如:docker pull 镜像名 是拉取镜像,守护进程在接收到命令之后就会去指定的仓库中拉取指定的镜像,下载到本地;而 docker run 镜像名则是根据镜像创建一个容器,该容器就可以提供相应的服务。

镜像

下面我们来看看 Docker 的核心之一:镜像。

相关概念

镜像是什么?

镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时、库、环境变量和配置文件。

UnionFS(联合文件系统)

UnionFS(联合文件系统):UnionFS 是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。Union 文件系统是 Docker 镜像的基础,镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。

特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。

Docker镜像加载原理

Docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统UnionFS。bootfs(boot file system)主要包含 bootloader 和 kernel,bootloader 主要是引导加载 kernel,Linux 刚启动时会加载 bootfs 文件系统,在 Docker镜像的最底层是 bootfs。这一层与我们典型的Linux/Unix系统是一样的,包含 boot 加载器和内核。当 boot 加载完成之后整个内核就都在内存中了,此时内存的使用权已由 bootfs 转交给内核,此时系统也会卸载 bootfs。

rootfs(root file system),在 bootfs 之上。包含的就是典型 Linux 系统中的 /dev、/proc、/bin、/etc 等标准目录和文件。rootfs 就是各种不同的操作系统发行版,比如 Ubuntu,CentOS等等。

后面我们会拉取 centos 镜像,你会发现才两百多兆,可平时我们安装进虚拟机的CentOS都是好几个G才对啊?因为对于一个精简的OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了,因为底层直接用 Host(宿主机)的 kernel,自己只需要提供 rootfs 就行了。由此可见对于不同的linux发行版,bootfs 基本是一致的,rootfs会有差别,因此不同的发行版可以公用 bootfs。

为什么Docker镜像采用分层结构?

我们说 Docker 的镜像实际上是由一层一层的文件系统组成,那么为什么要采用这种分层的结构呢?其实最大的好处就是共享资源,比如:有多个镜像都从相同的 base 镜像构建而来,那么宿主机只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。因为镜像的每一层都可以被共享。

镜像的特点

Docker镜像都是只读的,当容器启动时,一个新的可写层被加载到镜像的顶部,这一层通常被称作 "容器层","容器层" 之下的都叫 "镜像层"。

镜像的常用命令

下面我们来看看和镜像相关的命令都有哪些,Docker 和 Redis 一样,都需要我们时刻和命令打交道。先来看几个基础命令。

查看 Docker 版本

docker version

[root@mea ~]# docker version
Client:
 Version:         1.13.1
 API version:     1.26
 Package version: docker-1.13.1-203.git0be3e21.el7.centos.x86_64
 Go version:      go1.10.3
 Git commit:      0be3e21/1.13.1
 Built:           Thu Nov 12 15:11:46 2020
 OS/Arch:         linux/amd64

Server:
 Version:         1.13.1
 API version:     1.26 (minimum version 1.12)
 Package version: docker-1.13.1-203.git0be3e21.el7.centos.x86_64
 Go version:      go1.10.3
 Git commit:      0be3e21/1.13.1
 Built:           Thu Nov 12 15:11:46 2020
 OS/Arch:         linux/amd64
 Experimental:    false

我们看到版本是 1.13.1,里面还有一个 Go version,它表示编译 Docker 的 Go 语言版本,因为 Docker 是使用 Go 语言编写的。

查看 Docker 整体信息

docker info

[root@mea ~]# docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 1.13.1
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: journald
Cgroup Driver: systemd
Plugins: 
 Volume: local
 Network: bridge host macvlan null overlay
Swarm: inactive
Runtimes: docker-runc runc
Default Runtime: docker-runc
...........
...........
...........

返回的信息非常多,包括镜像的数量,容器的数量,正在运行、暂停、中止的容器数量,还有操作系统的相关信息等等。

Docker 命令帮助手册

docker --help

以上就是一些基本命令,下面来正式介绍镜像相关的命令。

搜索镜像

docker search [options] 镜像名

里面的 STARTS 就类似于 GitHub 上的 star,越多表示越受欢迎。而且这个命令是有一些可选参数的:

  • --no-trunc: 显示完整的镜像描述, 我们看到图中的 DESCRIPTION 那一列, 后面有的是 ...

  • --filter=stars=n: 列出star数不小于n的镜像

下载镜像

docker pull 镜像名[:TAG], 下载对应版本的镜像

我们看到镜像是分层的,所以下载也是一层一层下载。

注意:拉取镜像的时候可以指定版本,不指定则默认拉取最新的。

查看镜像

docker images [options]

[root@mea ~]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
docker.io/mysql     5.6                 e61c95a98514        2 days ago          302 MB
docker.io/mysql     latest              ab2f358b8612        2 days ago          545 MB
docker.io/redis     latest              ef47f3b6dc11        2 days ago          104 MB
docker.io/nginx     latest              7baf28ea91eb        2 days ago          133 MB
docker.io/mongo     latest              3068f6bb852e        2 days ago          493 MB
docker.io/centos    latest              300e315adb2f        6 days ago          209 MB

解释一下里面的每一列:

  • REPOSITORY:表示镜像的仓库源
  • TAG:镜像的标签
  • IMAGE ID:镜像ID
  • CREATED:镜像创建时间
  • SIZE:镜像大小

同一仓库源可以有多个 TAG,代表这个仓库源的不同个版本,我们使用 REPOSITORY:TAG 来定义不同的镜像。如果你不指定一个镜像的版本标签,docker pull mysql:5.6 表示安装5.6版本的 mysql;如果不指定,则默认安装最新的 mysql,也就是 TAG 为 latest。

然后我们看到查看镜像的时候还可以指定可选参数:

  • -a:列出本地所有的镜像(含中间镜像层)
  • -q:只显示镜像的id
  • --digests:显示镜像的摘要信息
  • --no-trunc:显示完成的镜像信息

删除镜像

docker rmi -f 镜像id[:TAG]

删除镜像的时候如果没有指定 TAG,则表示删除最新的(TAG 为 latest);这里的 -f 表示强制删除,如果没有使用该镜像创建容器的话,那么加不加 -f 是没有区别的。但是一旦该使用该镜像创建了容器并且启动的话,那么要删除必须加上-f,否则报错。值得一提的是,即便删除了镜像,已经创建的容器也不会消失,并且仍然可以正常运行,因为这个容器已经被创建出来了。

[root@mea ~]# docker rmi -f mongo
Untagged: mongo:latest
Untagged: docker.io/mongo@sha256:02e9941ddcb949424fa4eb01f9d235da91a5b7b64feb5887eab77e1ef84a3bad
Deleted: sha256:3068f6bb852ef596c20ca7ccbcd34df94f02d6035a2d83aea1e20e1781ba19b8
Deleted: sha256:ad127f54246fa452c5e783d7895f5de32aa050ca113c26f33cd70d3e321e0b77
Deleted: sha256:df411d930f08ff19c01fff7498a7c161ffe353027d1ee0be99aef4150ef2f237
Deleted: sha256:9da06454ee6eff2960465ba510f4180810bd95d22caa65d958923aeccaa12a12
Deleted: sha256:4fc8092849352849654c290a66285e0b989f01e73a95f869a8ee9bb2eadaebf6
Deleted: sha256:4545ce32b03c37edb33aedeac6ad411047ed6dd94650697e2465db1a86507f93
Deleted: sha256:9abb615f3c3c97191bf206d3228b5bd0978e370394e71984a0d183efb0e0bd9c
Deleted: sha256:eb6c9f1a2a53c149123b96a258dcd25c29b0b8c1709b8a326bc6b9ebaa43dec8
Deleted: sha256:1acc9a7abd496942e1469e91d2f7d2b7d9e0c696f2b392ef1e85d2eaef91e96b
Deleted: sha256:2589aa72cefbcd28ecbf0798bf2ca438af965daedbbb434e8f92d4bb2d689831
Deleted: sha256:9459b6a89846db0723e467610b841e6833dbb2aae6133319a91f2f70c388afac
Deleted: sha256:9a9311f7fcddf94f7476ce89f9703e8360e8cf347ef486a280735f5cf98888cd
Deleted: sha256:b43408d5f11b7b2faf048ae4eb25c296536c571fb2f937b4f1c3883386e93d64

为什么会删除这么多,之前也说过,镜像是分层的,镜像下面还有镜像,但是对外显示的只有一层。至于为什么设计成分层,上面也说了,这是 Docker 镜像的设计原理,但是我们使用就当只有一层就行。

删除多个镜像:docker rmi -f 镜像名1:TAG 镜像名2:TAG······

删除全部镜像:docker rmi -f $(docker images -qa) 或者 docker images -qa | xargs docker rmi -f

容器

下面来看看容器,我们说镜像是用来创建容器的模板;而镜像显然是无法提供服务的,真正提供服务的是根据镜像创建的容器。

Docker 镜像都是只读的,而当基于镜像创建并启动一个容器时,一个新的可写层被加载到镜像的顶部。这一层通常被称作 "容器层","容器层" 之下的都叫 "镜像层"。

新建并启动容器

docker run [options] 镜像名

创建容器时,可选参数是非常重要的,我们来看一下。

  • --name="容器新名字": 为容器指定一个名称;
  • -d: 后台运行容器,并返回容器ID,也即启动守护式容器;
  • -i:以交互模式运行容器,通常与 -t 同时使用;
  • -t:为容器重新分配一个伪输入终端,通常与 -i 同时使用;
  • -P: 随机端口映射;
  • -p: 指定端口映射;

下面来通过交互式启动容器。

[root@mea ~]# docker run -it centos
[root@e8eafccef263 /]# ls
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
[root@e8eafccef263 /]# echo "hello world"
hello world
[root@e8eafccef263 /]# 

此时我们便进入了使用 Docker 启动的 centos,这个centos 只保留了最核心的部分,使用的资源都是宿主机的资源。

列出当前所有正在运行的容器

docker ps [options]

依旧先来看一下可选参数,如果不指定可选参数,则列出当前正在运行的容器。

  • -a: 列出当前所有正在运行的容器+历史上运行过的
  • -l: 显示最近创建的容器
  • -n count: 显示最近count个创建的容器
  • -q: 静默模式, 只显示容器编号
  • --no-trunc: 不截断输出

看一下里面的每一列:

  • container id:容器的id
  • image:容器是由哪个镜像创建的
  • created:创建时间
  • status:容器状态,是在运行啊,还是在多长时间之前退出
  • names:容器的名字

退出容器

退出容器有两种方式:

exit:容器停止、退出

[root@mea ~]# docker run -it centos
[root@b3c3876b264f /]# exit
exit
[root@mea ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
[root@mea ~]# 

exit 之后,使用 docker ps 查看,发现没有正在运行的容器。

ctrl+p+q:容器不停止、退出

[root@mea ~]# docker run -it centos
# 按下 ctrl+p+q
[root@39188248a72d /]# 
[root@mea ~]# 
[root@mea ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
39188248a72d        centos              "/bin/bash"         15 seconds ago      Up 14 seconds                           blissful_jang
[root@mea ~]# 

按下 ctrl+p+q 之后,使用 docker ps 发现之前的容器还在运行。

启动容器

docker start 容器id

启动之前创建的容器。

一开始是没有正在运行的容器的,但是这个容器确实被创建出来了,只不过退出了。我们通过最近创建的容器找到 id 或者 name,然后 docker start 容器id 进行启动。顺便一提这个id可能有点长,我们也可以只输入前几位,比如 6 位。

重启容器

docker restart 容器id

重新启动一个正在运行的容器,另外一般重新启动都是针对正在运行的容器来说,就像Windows,重新启动只有电脑开机之后,才有重新启动这一说。但是 docker restart 也可以对没有启动的容器使用,等于 start;同时 start 也可以对已经启动的容器来使用。

停止容器

docker stop 容器id

停止正在运行的容器,即便容器没有运行,也可以使用这个命令,会返回容器的id。

[root@mea ~]# docker run -it centos
[root@1e1045eec69a /]# exit
exit
[root@mea ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
[root@mea ~]# 
[root@mea ~]# docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
1e1045eec69a        centos              "/bin/bash"         9 seconds ago       Exited (0) 6 seconds ago                       nifty_liskov
[root@mea ~]# 
[root@mea ~]# docker stop 1e1045eec69a
1e1045eec69a
[root@mea ~]# 

强制停止容器

docker kill 容器id

和 docker stop 功能一样,但是更加粗暴;stop 类似于关机,kill 类似于拔电源。

删除容器

删除已停止的容器:docker rm 容器id,所以删除镜像是 rmi、删除容器是 rm。

[root@mea ~]# docker ps 
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
[root@mea ~]# docker ps -n 2
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
1e1045eec69a        centos              "/bin/bash"         5 minutes ago       Exited (0) 5 minutes ago                       nifty_liskov
[root@mea ~]# docker rm 1e10
1e10
[root@mea ~]# 

会返回删除的容器的id,当然也可以同时删除多个;但如果容器正在运行的话,则是删不掉的,如果想删掉一个正在运行的容器,则需要通过 docker rm -f 容器id 的方式来删除。

删除所有容器:docker rm -f $(docker ps -qa) 或者 docker ps -qa | xargs docker rm -f

直接删除是删不掉的,提示我们无法删除一个正在运行的容器,如果想删除应该先将容器停止掉,或者使用 -f 参数。

启动守护式容器

我们说启动容器的时候,指定 -i 参数则是以交互式方式启动;再指定 -t 的话,会分配一个伪终端,这两个参数一般搭配使用。如果我们指定 -i 但是不指定 -t 的话,看看会有什么结果:

[root@mea ~]# 
[root@mea ~]# docker run -i centos
ls /root
anaconda-ks.cfg
anaconda-post.log
original-ks.cfg


yoyoyo
/bin/bash: line 4: yoyoyo: command not found


ext
/bin/bash: line 7: ext: command not found


exit
[root@mea ~]# 

我们看到虽然也是交互式的,但是很明显终端没了。

而除了 -i 和 -t 之外,我们还可以指定为 -d,表示启动守护式容器。我们说一个容器就类似于一个精简的虚拟机,每个容器提供一种服务,而服务一般显然都是后台启动的。

[root@mea ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
[root@mea ~]# 
[root@mea ~]# docker run -d centos
5c8c89a6f62a99729767cb2791194006656437a2950c60322a968fbbfc3b5f45
[root@mea ~]# 
[root@mea ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
[root@mea ~]# docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
5c8c89a6f62a        centos              "/bin/bash"         56 seconds ago      Exited (0) 54 seconds ago                       suspicious_shockley
[root@mea ~]# 

我们发现在启动之后,使用docker ps并没有列出,而使用docker ps -l查看,发现已经退出了,这是怎么回事?

很重要的一点:Docker容器后台运行,就必须有一个前台进程;而容器运行的命令如果不是那些一直挂起的命令(比如运行top,tail),就是会自动退出的。

这个是 Docker 的机制问题,比如你的 web 容器,我们以nginx为例,正常情况下,我们配置启动服务只需要启动响应的 service 即可。例如:service nginx start,但是这样做,nginx为后台进程模式运行,就导致docker前台没有运行的应用,这样的容器后台启动后,会立即自杀,因为他觉得他没事可做了。所以,最佳的解决方案是,将你要运行的程序以前台进程的形式运行。所以像 nginx、redis等镜像在启动之后,内部的进程都是以前台方式启动的。

所以 docker run -d 镜像 或者 docker run 镜像之后,会直接返回一串id,因为容器启动之后会立即自杀,尽管如此,但是这个容器已经被创建出来了。但是当我们使用 -it 的时候不会自动退出,因为相当于创建了一个伪终端

查看容器日志

docker logs 容器id

也可以指定一些可选参数:

  • -t: 加入当前时间
  • -f: 类似于 tail -f, 跟随最新的日志打印
  • --tail 数字: 显示最后多少条

上面的语法我们后面会说,但是我们是使用 -d 启动的,不是说启动之后就会被杀死吗?如果启动之后退出,说明没有相应的前台进程,但是这里不一样,这里我们是写了一个死循环的。每隔两秒钟打印一次 "hello world"。

查看容器内部运行的进程

docker top 容器id

[root@mea ~]# docker top 654e228f4436
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                15589               15572               0                   17:46               ?                   00:00:00            /bin/sh -c while true;do echo hello world;sleep 2;done
root                15836               15589               0                   17:52               ?                   00:00:00            /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 2
[root@mea ~]# 

查看容器内部细节

docker inspect 容器id

返回的内容非常多,详细地描述了该容器。

进入正在运行的容器并与之交互

之前其实有一个问题没有说,是什么呢?那就是当我们使用ctrl+p+q的时候,会在不停止容器的情况下退出,容器会依旧在运行,但是如果我们想要再次进入已经停止的容器的话,该怎么办呢?

docker attach 容器id

[root@mea ~]# docker run -it centos
[root@08eb7761dca5 /]# ls
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
[root@08eb7761dca5 /]# 
[root@mea ~]# 
[root@mea ~]# docker attach 08eb7761dca5
[root@08eb7761dca5 /]# 

这个命令会直接启动命令行的终端,不会启动新的进程。

docker exec -it 容器id /bin/sh

[root@mea ~]# docker exec -it 08eb7761dca5 /bin/sh
sh-4.4# ls
bin  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
sh-4.4# pwd
/
sh-4.4# 

这个命令是在容器中打开新的终端,比如说,我们在 Linux 上开了一个终端,attach 是在原来的终端上执行,exec则是新打开了一个终端,并且可以启动一个新的进程。

并且我们可以直接执行 shell 命令:

[root@mea ~]# docker exec -it 08eb7761dca5 /bin/sh -c "ls /"
bin  etc   lib	  lost+found  mnt  proc  run   srv  tmp  var
dev  home  lib64  media       opt  root  sbin  sys  usr
[root@mea ~]# 

也就是开启一个新的终端,然后执行,只不过执行完之后自动回到宿主机。-c "" 可以同时写多个命令,中间使用分号隔开。

从容器内拷贝文件到主机上

比如我这个容器不想要了,但是我里面有一个很重要的文件,这个文件我想把它拿到宿主机上,该怎么做呢?

docker cp 容器id:容器路径 目的主机路径

[root@mea ~]# ls | grep 1.txt
[root@mea ~]# 
[root@mea ~]# docker exec -it 08eb7761dca5 /bin/sh -c "cd /root;touch 1.txt"
[root@mea ~]# 
[root@mea ~]# docker cp 08eb7761dca5:/root/1.txt /root
[root@mea ~]# 
[root@mea ~]# ls | grep 1.txt
1.txt
[root@mea ~]# 

镜像打包

说到拷贝文件,我想起了镜像。在镜像的时候忘记说了。如果没有网络,我们如何将镜像打包拷贝到另一个机器上呢,既然要拷贝到另一台机器上,肯定要先拷贝到本机上。

docker save 镜像名 > xxx.tar

[root@mea ~]# ll
total 0
[root@mea ~]# docker save nginx > nginx.tar
[root@mea ~]# ll
total 133880
-rw-r--r-- 1 root root 137091072 Dec 15 11:24 nginx.tar
[root@mea ~]# 

docker load < xxx.tar

将tar文件加载成镜像,在保存镜像是一般以 tar 结尾,可能有人发现在加载镜像的时候没有指定镜像名,这是因为 tar 文件中包含了镜像的所有信息。

[root@mea ~]# docker rmi -f nginx
Untagged: nginx:latest
Untagged: docker.io/nginx@sha256:31de7d2fd0e751685e57339d2b4a4aa175aea922e592d36a7078d72db0a45639
Deleted: sha256:7baf28ea91eb59a68b3b3a82873d413dc9f1b6e0b89d5ad627ad80154f546be5
Deleted: sha256:76568a8765bf6788bab06a4c283b5ca6669def93eba5c1f0fc887b159f6e1dc1
Deleted: sha256:ef3c14608491697162ea3cc095a1c0c27cd6d4053aa3ea09f91c756379ebc8d1
Deleted: sha256:b538d73544a75eec8322716997990d289f2581169a82b763a0e1037c669f167d
Deleted: sha256:89942a8c1027ff865a14b0a807142453493474831ebc47c312f05ec640b16254
[root@mea ~]# docker images | grep nginx
[root@mea ~]# 
[root@mea ~]# docker load < nginx.tar 
2111bafa5ce4: Loading layer [==================================================>] 64.55 MB/64.55 MB
f3ee98cb305c: Loading layer [==================================================>] 3.072 kB/3.072 kB
997bdb5b26cc: Loading layer [==================================================>] 4.096 kB/4.096 kB
ea6033164031: Loading layer [==================================================>] 3.584 kB/3.584 kB
Loaded image: docker.io/nginx:latest
[root@mea ~]# 
[root@mea ~]# docker images | grep nginx
docker.io/nginx     latest              7baf28ea91eb        3 days ago          133 MB
[root@mea ~]#

将镜像 nginx 删除,然后再 load,发现镜像 nginx 又回来了。

查看容器内部的变化

docker diff 容器id

查看一个镜像的形成历史

docker history 镜像名

暂停一个容器

docker pause 容器id

恢复暂停的容器

docker unpause 容器id

阻塞、直到容器退出、然后打印退出时候的状态值

docker wait 容器id

镜像commit:将一个容器变成一个镜像

docker commit -m="提交的容器信息" -a="作者" 容器id 要创建的镜像名[:TAG]

比如我们启动了一个容器,在这个容器里面我们做了相应的操作,我们想把当前这个已经做了操作的容器变成一个镜像。

**我们运行一下 nginx 镜像: **

[root@mea ~]# docker run -d -p 90:80 nginx
8d3d36eeaf5c4c28164f7ed61f3fb02f859461db3590651ba3eaf5394ada8041
[root@mea ~]# 
[root@mea ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
8d3d36eeaf5c        nginx               "/docker-entrypoin..."   4 seconds ago       Up 3 seconds        0.0.0.0:90->80/tcp   gallant_yalow
[root@mea ~]# 

注意一下这里的 -p 90:80,我们说一个容器就类似于一个小型的 CentOS,比如这里的nginx容器,它内部监听80端口,这个80端口指的是容器(小型CentOS)的80端口。而 -p 90:80 里面的 90 指的是和容器内部 80 端口绑定的宿主机的端口,因为外界不能直接访问容器,需要通过宿主机的 90 端口映射到容器的 80 端口,访问服务。

所以我们可以启动N个nginx,每个nginx都监听80端口,但是这个80端口是每个容器内部的80端口,它们是彼此隔离的。而和宿主机绑定的端口则不能重复,比如第一个nginx和宿主机的90端口绑定,那么第二个容器就不能再和90端口绑定了。

而我们从外界访问的话,只能通过90端口访问,因为要先访问到宿主机才能访问到容器。

[root@mea ~]# docker exec -it 8d3d36 /bin/sh
# cd /usr/share/nginx/html
# find -name 'index.html' | xargs perl -pi -e 's|nginx|夏色祭|g'
# exit
[root@mea ~]# docker commit -m="index.html被修改的nginx" -a="哟哟哟" 8d3d36 my_nginx:3.3
sha256:535ad9516d9cb4d0408de8520158cfcb38baa578cacad01fa88b73d05ac5c905
[root@mea ~]# docker images | grep my_nginx
my_nginx            3.3                 535ad9516d9c        10 seconds ago      133 MB
[root@mea ~]# 

我们进入容器,将里面的 index.html 给改掉,然后将其打包成镜像。然后启动我们新打包的镜像:

[root@mea ~]# docker run -d -p 100:80 my_nginx:3.3
ac1415d33481d03a7f906402a0c079322cc0b1aa957b546195f8b135060d7706
[root@mea ~]# 
[root@mea ~]# docker ps | grep my_nginx
ac1415d33481        my_nginx:3.3        "/docker-entrypoin..."   13 seconds ago      Up 13 seconds       0.0.0.0:100->80/tcp   eager_keller
[root@mea ~]# 

90端口被之前的nginx容器给占了,所以我们需要绑定其它的宿主机端口。另外关于启动镜像,如果镜像有 TAG(或者 TAG 不是 latest),那么启动的时候需要指定 TAG,因为默认启动的是 TAG 为 latest 的镜像。如果发现不存在此镜像,会自动从仓库中拉取。

我们将 "nginx" 换成了 "夏色祭",但是比较尴尬的是忘记编码了,不过这不重要。重要的是我们启动容器之后,没有做任何的修改,但是显示的内容变了,因为此镜像是由配置改变的容器commit得到的。

因此我们可以看到,除了可以用镜像生成容器之外,还可以将容器commit成一个镜像。

常用命令总结

部分命令会在后面介绍。

attach    Attach to a running container                 # 当前 shell 下 attach 连接指定运行镜像
build     Build an image from a Dockerfile              # 通过 Dockerfile 定制镜像
commit    Create a new image from a container changes   # 提交当前容器为新的镜像
cp        Copy files/folders from the containers filesystem to the host path   #从容器中拷贝指定文件或者目录到宿主机中
create    Create a new container                        # 创建一个新的容器,同 run,但不启动容器
diff      Inspect changes on a container's filesystem   # 查看 docker 容器变化
events    Get real time events from the server          # 从 docker 服务获取容器实时事件
exec      Run a command in an existing container        # 在已存在的容器上运行命令
export    Stream the contents of a container as a tar archive   # 导出容器的内容流作为一个 tar 归档文件[对应 import ]
history   Show the history of an image                  # 展示一个镜像形成历史
images    List images                                   # 列出系统当前镜像
import    Create a new filesystem image from the contents of a tarball # 从tar包中的内容创建一个新的文件系统映像[对应export]
info      Display system-wide information               # 显示系统相关信息
inspect   Return low-level information on a container   # 查看容器详细信息
kill      Kill a running container                      # kill 指定 docker 容器
load      Load an image from a tar archive              # 从一个 tar 包中加载一个镜像[对应 save]
login     Register or Login to the docker registry server    # 注册或者登陆一个 docker 源服务器
logout    Log out from a Docker registry server          # 从当前 Docker registry 退出
logs      Fetch the logs of a container                 # 输出当前容器日志信息
port      Lookup the public-facing port which is NAT-ed to PRIVATE_PORT    # 查看映射端口对应的容器内部源端口
pause     Pause all processes within a container        # 暂停容器
ps        List containers                               # 列出容器列表
pull      Pull an image or a repository from the docker registry server   # 从docker镜像源服务器拉取指定镜像或者库镜像
push      Push an image or a repository to the docker registry server    # 推送指定镜像或者库镜像至docker源服务器
restart   Restart a running container                   # 重启运行的容器
rm        Remove one or more containers                 # 移除一个或者多个容器
rmi       Remove one or more images             # 移除一个或多个镜像[无容器使用该镜像才可删除,否则需删除相关容器才可继续或 -f 强制删除]
run       Run a command in a new container              # 创建一个新的容器并运行一个命令
save      Save an image to a tar archive                # 保存一个镜像为一个 tar 包[对应 load]
search    Search for an image on the Docker Hub         # 在 docker hub 中搜索镜像
start     Start a stopped containers                    # 启动容器
stop      Stop a running containers                     # 停止容器
tag       Tag an image into a repository                # 给源中镜像打标签
top       Lookup the running processes of a container   # 查看容器中运行的进程信息
unpause   Unpause a paused container                    # 取消暂停容器
version   Show the docker version information           # 查看 docker 版本号
wait      Block until a container stops, then print its exit code   # 截取容器停止时的退出状态值

Docker容器数据卷

先来看看Docker的理念:

  • 将运用与运行的环境打包形成容器运行,运行可以伴随着容器,但是我们对数据的要求希望是持久化的
  • 容器之间希望有可能共享数据

Docker容器产生的数据,如果不通过docker commit生成新的镜像,使得数据做为镜像的一部分保存下来,那么当容器删除后,数据自然也就没有了。我们之前介绍了一个docker cp命令,可以将容器内的数据拷贝到宿主机当中,但是有没有更简单的办法呢?可以不用我们显式的调用命令,而是实现自动关联,我们在容器中新建文件或者修改文件可以自动地同步到宿主机当中呢?答案是可以的,在docker中我们使用卷的方式。

卷就是目录或文件,存在于一个或多个容器中,由Docker挂载到容器,但不属于联合文件系统,因此能够绕过Union File System提供一些用于持续存储或共享数据的特性。卷的设计目的就是数据的持久化,完全独立于容器的生存周期,因此Docker不会在容器删除时删除其挂载的数据卷:

  • 1. 数据卷可在容器之间共享或重用数据
  • 2. 卷中的更改可以直接生效
  • 3. 数据卷中的更改不会包含在镜像的更新中
  • 4. 数据卷的生命周期一直持续到没有容器使用它为止

核心就是:容器的持久化,以及容器间的继承+共享数据。

直接命令添加

docker run -it -v /宿主机绝对路径:/容器绝对路径 镜像名

[root@mea ~]# ll
total 0
[root@mea ~]# docker run -it -v /root/host:/container centos
[root@df66650cce72 /]# 
[root@df66650cce72 /]# ls | grep container
container
[root@df66650cce72 /]# exit
exit
[root@mea ~]# ll 
total 4
drwxr-xr-x 2 root root 4096 Dec 15 14:03 host
[root@mea ~]# 

一开始宿主机内没有任何内容,然后我们启动容器,将宿主机的 /root/host 和 容器的 /container 进行关联,显然这两个目录各自都不存在。但是当启动之后,它们就被自动创建了。

然后此时宿主机的 /root/host 和 容器的 /container 就实现了共享,在其中一个目录中做的任何修改都会影响到另一目录。

如果你在启动容器的时候发现失败了,提示没有权限,那么需要加上一个可选参数。

docker run -it --privileged=true -v /宿主机绝对路径:/容器绝对路径 镜像名

然后我们测试一下数据是否真的会共享:

[root@mea ~]# touch host/1.txt
[root@mea ~]# 
[root@mea ~]# docker start df66650cce72
df66650cce72
[root@mea ~]# docker exec -it df66650cce72 /bin/sh
sh-4.4# ls /container/
1.txt
sh-4.4# touch /container/2.txt
sh-4.4# exit
exit
[root@mea ~]# ll host/
total 0
-rw-r--r-- 1 root root 0 Dec 15 14:10 1.txt
-rw-r--r-- 1 root root 0 Dec 15 14:11 2.txt
[root@mea ~]# 

我们在宿主机的 host 的目录中创建 1.txt 文件,然后启动容器(注意:容器刚才是关闭的),查看 /container 目录,发现内部的 1.txt 被自动创建了。然后在容器的 /container 内部创建 2.txt,发现也被同步到宿主机中了。

同理,我们对里面的文件本身做修改,同样会实现共享。

[root@mea ~]# docker exec -it df66650cce72 /bin/sh
sh-4.4# cat /container/1.txt 
sh-4.4# cat /container/2.txt 
sh-4.4# 
sh-4.4# echo "matsuri" > /container/1.txt 
sh-4.4# exit
exit
[root@mea ~]# 
[root@mea ~]# cat host/1.txt 
matsuri
[root@mea ~]# echo "matsuri" > /root/host/2.txt 
[root@mea ~]# 
[root@mea ~]# docker exec -it df66650cce72 /bin/sh
sh-4.4# cat /container/2.txt 
matsuri
sh-4.4# 

我们进入容器,看到里面的文件都是没有内容的,然后向 1.txt 中写入内容,回到宿主机中发现 host 目录下的 1.txt 已经有内容了。然后在 host 目录下的 2.txt 中也写入内容,再进入容器,看到 /container 目录下的 2.txt 中也有内容了。

所以在目录中做任何的修改,都会同步到另一个目录中。

并且我们在操作的时候,是使用exit直接退出容器,并不是使用ctrl+p+q。也就是说,我们在宿主机操作的时候,容器时处于关闭状态的。这种情况下依旧会同步,类似于持久化,当容器启动之后再将数据同步过去就可以了。

当然使用 ctrl+p+q 就更不用说了。

如果我们在同步之后,禁止容器内部修改文件,只能在宿主机中修改,该怎么做呢?

docker run -it --privileged=true -v /宿主机绝对路径:/容器绝对路径:ro 镜像名

只需要在容器的目录后面加上一个 ro 即可,表示read only,只读。

[root@mea ~]# docker run -it -v /root/host:/container:ro centos
[root@900695bdf841 /]# ls /container/
1.txt  2.txt
[root@900695bdf841 /]# touch /container/3.txt
touch: cannot touch '/container/3.txt': Read-only file system
[root@900695bdf841 /]# 
[root@900695bdf841 /]# echo "hello matsuri" >> /container/1.txt 
bash: /container/1.txt: Read-only file system
[root@900695bdf841 /]# 

这个容器是我们新创建的,但是我们看到里面居然有文件。因为宿主机内部有文件,虽然当时我们把容器删掉了,但如果是删除整个容器的话,宿主机内部的目录和目录里面的文件是不受影响的。当然,如果关联容器目录本身就有文件,那么容器内的文件在启动的时候也会同步到宿主机上。

然后我们在容器内的 /container 目录创建文件、修改文件都是不允许的,因为它是只读的,当然在其它目录创建是可以的。

因此可以看到,如果是以只读方式创建容器,那么在宿主机里面是可以修改并创建文件的,但是在容器里面不行,至于数据本身,在宿主机里面进行的操作依旧会进行同步。

我们使用 docker inspect 查看一下容器的内部细节:

DockerFile添加

DockerFile会在下面详细介绍,但是现在可以提前了解一下。DockerFile相当于是对镜像的描述,可以对DockerFile进行build,得到镜像。如果我们想修改或者创建镜像的话,那么就可以修改或者创建DockerFile文件。DockerFile相当于是源码文件,镜像相当于是编译之后的字节码。Python运行的也是字节码文件,如果我们想修改字节码,那么就要修改源码,再重新编译为字节码。DockerFile也是一样的。

新建一个文件,就叫dockerfile,写入如下内容:

FROM centos
VOLUME ["/root/dataVolumeContainer1","/root/dataVolumeContainer2"]
CMD echo "finished,--------success1"
CMD /bin/bash

dockerfile会在下一章介绍,先来简单地看一看。首先是第一行的 FROM centos,相当于继承,extend。之前说过镜像是分层的,这样可以共享。比如tomcat,总共四百多兆,这显然太大了。但是如果看tomcat的DockerFile文件的话,会发现开头写着from open-jdk1.8,说明tomcat是包含了jdk的,所以才会这么大。不然只有tomcat没有jdk是没法运行的,因此在删除tomcat的时候,会发现删除的不止一层。镜像就像花卷或者俄罗斯套娃一样,一层套着一层。

VOLUME则是数据卷,里面可以有多个目录,这些目录会自动和宿主机内的目录进行关联。就像 -v 一样,当然我们使用命令添加数据卷的时候也可以关于多个目录,比如:

-v /host1:/container1 -v /host2/container2

但是我们说 VOLUME 里面目录会自动和宿主机里面的目录进行关联,那宿主机目录在哪里指定呢?答案是不需要指定,因为出于可移植和分享的考虑,用 -v 主机目录:容器目录 这种方法不能够直接在Dockerfile中实现。由于宿主机目录是依赖于特定宿主机的,并不能够保证在所有的宿主机上都存在这样的特定目录。所以我们只需要指定容器目录即可,宿主机目录 docker 会自动创建。

而最后两个 CMD 则不用管,后面说。

然后生成镜像

docker build -f DockerFile文件 -t 生成的镜像名称 生成在哪个目录(一般写.即可)

[root@mea ~]# docker run -it my_centos
[root@52d903a2e286 /]# ls /root/ | grep data
dataVolumeContainer1
dataVolumeContainer2
[root@52d903a2e286 /]# 

那么问题来了,我要找对应的容器目录怎么找?使用 docker inspect 即可,容器的所有细节都能查到。

我们创建个文件试试:

[root@mea ~]# ls /var/lib/docker/volumes/2d8fe1c3c159392830d06aea8ab35a53c50928cce312e47564567323ef71843d/_data/
[root@mea ~]# ls /var/lib/docker/volumes/8362b41fb65b0b5cb725de0ed841855084a7b40f4eddd462657f227ea16b1c57/_data/
[root@mea ~]# 
[root@mea ~]# docker start 52d903a2e286
52d903a2e286
[root@mea ~]# docker exec -it 52d903a2e286 /bin/sh
sh-4.4# echo "hello" >> /root/dataVolumeContainer1/1.txt
sh-4.4# echo "matsuri" >> /root/dataVolumeContainer2/1.txt
sh-4.4# exit
exit
[root@mea ~]# 
[root@mea ~]# cat /var/lib/docker/volumes/2d8fe1c3c159392830d06aea8ab35a53c50928cce312e47564567323ef71843d/_data/1.txt 
hello
[root@mea ~]# cat /var/lib/docker/volumes/8362b41fb65b0b5cb725de0ed841855084a7b40f4eddd462657f227ea16b1c57/_data/1.txt 
matsuri
[root@mea ~]# 

关联文件

但是很多时候,我们不希望关联一个目录,而是只需要关联一个文件即可。

这个时候只能通过命令的方式添加数据卷,以 -v /a:/b 为例:

  • a不存在, 则a、b均为目录
  • a是个目录, 则a、b均为目录
  • a是个文件, 则a、b均为文件

数据卷容器的继承

命名的容器挂载数据卷,其它容器通过挂载这个(父容器)实现数据共享,挂载数据卷的容器,称之为数据卷容器。

宿主机相当于电脑,容器相当于硬盘,电脑的数据放到硬盘里。但是如果我有很多的容器呢?因此数据卷容器,相当于硬盘挂载到硬盘上,这样硬盘之间的数据也可以共享。

我们之前使用 DockerFile build一个镜像,下面根据这个镜像来启动几个容器。

[root@mea ~]# docker run -it --name="c1" my_centos
[root@4a2c486b6455 /]# ls /root/dataVolumeContainer1
[root@4a2c486b6455 /]# ls /root/dataVolumeContainer2
[root@4a2c486b6455 /]# 

返回的容器 id 不好记,所以这里我们给容器起一个名字,因为操作容器除了可以指定容器id、还可以指定容器的名字。

[root@mea ~]# docker run -it --name="c2" --volumes-from c1  my_centos
[root@5c99cbae9eff /]# ls /root/dataVolumeContainer1
[root@5c99cbae9eff /]# ls /root/dataVolumeContainer2
[root@5c99cbae9eff /]# exit
exit
[root@mea ~]# docker run -it --name="c3" --volumes-from c1  my_centos
[root@02e6a6d12b83 /]# ls /root/dataVolumeContainer1
[root@02e6a6d12b83 /]# ls /root/dataVolumeContainer2
[root@02e6a6d12b83 /]# 

然后创建容器 c2,要挂载到 c1 上,因此使用--volumes-from 容器,表示挂载到某个容器上。所以 c2 里面也有相应的目录,注意此时容器 c1 已经退出了,但是不影响。同理容器 c3 也是一样。

接下来进入容器 c1,在里面创建文件并写入内容。

[root@mea ~]# docker start c1
c1
[root@mea ~]# docker exec -it c1 /bin/sh
sh-4.4# echo "hello matsuri" >> /root/dataVolumeContainer1/1.txt
sh-4.4# exit
exit
[root@mea ~]# docker start c2
c2
[root@mea ~]# docker exec -it c2 /bin/sh
sh-4.4# cat /root/dataVolumeContainer1/1.txt 
hello matsuri
sh-4.4# exit
exit
[root@mea ~]# docker start c3
c3
[root@mea ~]# docker exec -it c3 /bin/sh
sh-4.4# cat /root/dataVolumeContainer1/1.txt 
hello matsuri
sh-4.4# exit
exit
[root@mea ~]# 

在c1里面写文件,会同步到c2和c3中,那么问题来了,在c2和c3中写文件会不会同步到c1中呢?由于c2和c3是等价的,我们只需要在c2中写就可以了。

[root@mea ~]# docker start c2
c2
[root@mea ~]# docker exec -it c2 /bin/sh
sh-4.4# echo "hello 夏色祭" >> /root/dataVolumeContainer1/1.txt 
sh-4.4# exit
exit
[root@mea ~]# docker start c1
c1
[root@mea ~]# docker exec -it c1 /bin/bash
[root@4a2c486b6455 /]# cat /root/dataVolumeContainer1/1.txt 
hello matsuri
hello 夏色祭
[root@4a2c486b6455 /]# exit
exit
[root@mea ~]# docker start c3
c3
[root@mea ~]# docker exec -it c3 /bin/bash
[root@02e6a6d12b83 /]# cat /root/dataVolumeContainer1/1.txt 
hello matsuri
hello 夏色祭
[root@02e6a6d12b83 /]# 

所以说容器之间是共享数据的,c2和c3都继承自c1,我们在c1里面创建的文件,会同步到c2和c3里面去,但是我们在c2和c3里面做的修改也会作用到c1里面。因此docker容器不仅仅是父到子,还是子到父。尽管c2和c3都继承自c1,但是三者是共享的。在dc01创建文件会同步到dc02和dc03里面去,同理在dc02创建文件也会同步到dc01和dc03里面去,在dc03创建文件也会同步到dc01和dc02里面去,当然除了创建文件、删除文件、修改文件也是一样的。

那么问题来了,现在c2和c3都是继承自c1,那如果我把c1删掉,然后在c2里面创建文件,那么会不会也作用到c3里面去呢。

想都不用想,肯定是会的。如果我们再创建容器c4,继承自c3,那么数据也会同步到c4里面去。同理再创建c5继承c4、创建c6继承c5、创建c7继承c6,然后在c2里面创建文件,也会同步到c3、c4、c5、c6、c7里面(c1被删掉了)。这些数据之间都是共享的,里面的数据会保持一致。

容器之间配置信息的传递,数据卷的生命周期一直持续到没有容器使用它为止。通俗点的说,只要没死绝,那么都可以进行全量的备份。

DockerFile解析

DockerFile 是用来构建 Docker镜像 的文件,是由一系列命令和参数构成的脚本

那么这个文件长什么样子呢?我们以官方的centos镜像为例:

先来了解一下DockerFile,然后里面的关键字慢慢解释。

DockerFile基础知识

  • 每条保留字指令都必须为大写字母且后面要跟随至少一个参数
  • 指定按照从上到下,顺序执行
  • 井号表示注释, 但是必须写在单独的一行
  • 每条指令都会创建一个新的镜像层,并对镜像进行提交

docker执行DockerFile的大致流程

  • Docker执行基础镜像运行一个容器
  • 执行每一条指令对容器做出修改
  • 执行类似Docker commit的操作提交一个新的镜像层
  • Docker再基于刚提交的镜像运行一个新的容器
  • 再执行DockerFile中的下一条指令,重复相同的操作,直到所有的指令都完成
  • 最终形成一个新的镜像

DockerFile、Docker镜像、Docker容器

  • DockerFile是软件的原材料
  • Docker镜像是软件的交付品
  • Docker容器则可以认为是软件的运行态

DockerFile面向开发,Docker镜像成为交付标准,Docker容器则涉及部署和运维,三者缺一不可,合力充当Docker体系的基石。

  • Dockerfile,需要定义一个Dockerfile,Dockerfile定义了进程需要的一切东西。Dockerfile涉及的内容包括执行代码或者是文件、环境变量、依赖包、运行时环境、动态链接库、操作系统的发行版、服务进程和内核进程(当应用进程需要和系统服务和内核进程打交道,这时需要考虑如何设计namespace的权限控制)等等;
  • Docker镜像,在用Dockerfile定义一个文件之后,docker build时会产生一个Docker镜像,当运行 Docker镜像时,会真正开始提供服务;
  • Docker容器,容器是直接提供服务的

DockerFile体系结构(保留字指令)

下面来讲解一下DockerFile的语法:

FROM

基础镜像,当前要创建的镜像是基于哪一个镜像的。比如centos基于scratch,自己创建的镜像FROM centos 的话,等于也基于scratch。

MAINTAINER

镜像维护者的信息。

RUN

容器构建时需要运行的命令。

EXPOSE

容器运行时对外暴露出来的端口。

WORKDIR

指定在创建容器时,终端登录进来的默认的工作目录,一个落脚点。

ENV

用来在构建镜像过程中设置的环境变量,ENV MY_PATH /usr/mytest,比如:WORKDIR $MY_PATH。

ADD

将宿主机目录下的文件或目录拷贝到镜像,且 ADD 命令会自动处理url和解压tar包。

COPY

类似于 ADD,拷贝文件或目录到镜像中,copy src dst,或者 COPY ["src", "dst"]

VOLUME

容器数据卷,用于数据保存和持久化工作。

CMD

提交一个容器启动时要运行的命令,DockerFile中可以有多个CMD指令,但只有最后一个生效,CMD会被docker run -it centos之后的参数替换。

ENTRYPOINT

作用和 CMD 一样,只不过多个命令不会覆盖,而是会追加。

ONBUILD

当个构建一个被继承的DockerFile时运行命令,当父镜像被子镜像继承后,子镜像运行时,父镜像会触发 ONBUILD,类似于一个触发器。或者理解为是父镜像里面的一个回调函数,当子镜像运行时,会触发父镜像的回调函数。

我们下面来依次展示一下这些指令的用法。

自定义镜像my_centos

默认的centos镜像是什么样子:

[root@mea ~]# docker run -it centos
[root@e7472068d47e /]# pwd
/
[root@e7472068d47e /]# vim
bash: vim: command not found
[root@e7472068d47e /]# 

进入之后在根目录,不支持vim。那么我们的任务就是,自定义一个centos,具备的特征是:进入之后要默认在 /matsuri 目录,支持vim。

# 继承自centos, 另外有一个Base镜像:scratch, 百分之99的镜像都是在此基础之上构建得到的
FROM centos

# 维护者的信息
MAINTAINER shiinamashiro@gmail.com

# 将 /matsuri 设置为环境变量
ENV m /matsuri

# 执行命令, 创建目录
RUN mkdir $m

# 指定工作区, 进入容器之后默认在这个目录
WORKDIR $m

# 执行命令安装相关应用
RUN yum install -y vim

# 暴露端口为80
EXPOSE 80

# 启动容器之后执行/bin/bash
CMD /bin/bash

然后我们build 完镜像之后,创建容器。

此时我们的任务就完成了,我们来查看一下镜像的形成历史。

[root@mea ~]# docker history my_centos
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
2d59e5aa5493        2 minutes ago       /bin/sh -c #(nop)  CMD ["/bin/sh" "-c" "/b...   0 B                 
404a8640bf1d        2 minutes ago       /bin/sh -c #(nop)  EXPOSE 80/tcp                0 B                 
9374ec9b97ae        2 minutes ago       /bin/sh -c yum install -y vim                   57.8 MB             
9f9823697c57        3 minutes ago       /bin/sh -c #(nop) WORKDIR /matsuri              0 B                 
60a8a8d0416e        3 minutes ago       /bin/sh -c mkdir $m                             0 B                 
93d393b42b1a        3 minutes ago       /bin/sh -c #(nop)  ENV m=/matsuri               0 B                 
a01e1f02a0db        3 minutes ago       /bin/sh -c #(nop)  MAINTAINER shiinamashir...   0 B                 
300e315adb2f        7 days ago          /bin/sh -c #(nop)  CMD ["/bin/bash"]            0 B                 
<missing>           7 days ago          /bin/sh -c #(nop)  LABEL org.label-schema....   0 B                 
<missing>           7 days ago          /bin/sh -c #(nop) ADD file:bd7a2aed6ede423...   209 MB              
[root@mea ~]# 

可以看到,类似于栈一样,从底往上。

CMD/ENTRYPOINT 镜像案例

两者都是指定一个容器启动时要运行的命令。但是如果DockerFile中有多个CMD指令,那么只有最后一个生效,并且CMD会被 docker run 镜像 之后的参数替换。

怎么理解呢?我们看看 tomcat 的 dockerfile 的最后两句。

EXPOSE 8080
CMD ["catalina.sh", "run"]

表示暴露端口为8080,然后执行CMD ["catalina.sh", "run"]。docker run -it tomcat之所以可以启动服务,输入localhost:8080还能看到那只猫,是因为最后一条命令。但是如果我们自己指定参数,比如:docker -d run tomcat -ls -l,那么你会发现服务根本不会启动,因为我们的 ls -l 把 tomcat 的 dockerfile 文件中的 CMD 命令给覆盖了,所以只是执行了 ls -l,没有启动 tomcat。

如果有多个 CMD,也只有最后一个生效。

再来看看 ENTRYPOINT,docker run -it 镜像 之后的参数会传递给ENTRYPOINT,之后形成新的命令组合。举个栗子:

vim dockerfile1
输入如下内容
FROM centos
CMD ["ls", "-l"]

vim dockerfile2
输入如下内容
FROM centos
ENTRYPOINT ["ls", "-l"]

然后build
docker build -f ./dockerfile1 -t centos1 .
docker build -f ./dockerfile2 -t centos2 .

然后来运行这两个镜像,运行 centos1:

[root@mea ~]# docker run centos1
total 48
lrwxrwxrwx   1 root root    7 Nov  3 15:22 bin -> usr/bin
drwxr-xr-x   5 root root  340 Dec 15 09:00 dev
drwxr-xr-x   1 root root 4096 Dec 15 09:00 etc
drwxr-xr-x   2 root root 4096 Nov  3 15:22 home
lrwxrwxrwx   1 root root    7 Nov  3 15:22 lib -> usr/lib
lrwxrwxrwx   1 root root    9 Nov  3 15:22 lib64 -> usr/lib64
drwx------   2 root root 4096 Dec  4 17:37 lost+found
drwxr-xr-x   2 root root 4096 Nov  3 15:22 media
drwxr-xr-x   2 root root 4096 Nov  3 15:22 mnt
drwxr-xr-x   2 root root 4096 Nov  3 15:22 opt
dr-xr-xr-x 123 root root    0 Dec 15 09:00 proc
dr-xr-x---   2 root root 4096 Dec  4 17:37 root
drwxr-xr-x   1 root root 4096 Dec 15 09:00 run
lrwxrwxrwx   1 root root    8 Nov  3 15:22 sbin -> usr/sbin
drwxr-xr-x   2 root root 4096 Nov  3 15:22 srv
dr-xr-xr-x  13 root root    0 Dec 15 09:00 sys
drwxrwxrwt   7 root root 4096 Dec  4 17:37 tmp
drwxr-xr-x  12 root root 4096 Dec  4 17:37 usr
drwxr-xr-x  20 root root 4096 Dec  4 17:37 var
[root@mea ~]# 
[root@mea ~]# docker run centos1 usr
container_linux.go:235: starting container process caused "exec: \"usr\": executable file not found in $PATH"
/usr/bin/docker-current: Error response from daemon: oci runtime error: container_linux.go:235: starting container process caused "exec: \"usr\": executable file not found in $PATH".
[root@mea ~]# 

当我们直接启动容器时,会默认执行 ls -l 打印根目录的信息。

但是当我加上 usr 的时候,CMD ["ls", "l"] 就被覆盖了,相当于直接输入一个 usr,而它显然不是一个命令。

再来运行 centos2:

[root@mea ~]# docker run centos2
total 48
lrwxrwxrwx   1 root root    7 Nov  3 15:22 bin -> usr/bin
drwxr-xr-x   5 root root  340 Dec 15 09:04 dev
drwxr-xr-x   1 root root 4096 Dec 15 09:04 etc
drwxr-xr-x   2 root root 4096 Nov  3 15:22 home
lrwxrwxrwx   1 root root    7 Nov  3 15:22 lib -> usr/lib
lrwxrwxrwx   1 root root    9 Nov  3 15:22 lib64 -> usr/lib64
drwx------   2 root root 4096 Dec  4 17:37 lost+found
drwxr-xr-x   2 root root 4096 Nov  3 15:22 media
drwxr-xr-x   2 root root 4096 Nov  3 15:22 mnt
drwxr-xr-x   2 root root 4096 Nov  3 15:22 opt
dr-xr-xr-x 123 root root    0 Dec 15 09:04 proc
dr-xr-x---   2 root root 4096 Dec  4 17:37 root
drwxr-xr-x   1 root root 4096 Dec 15 09:04 run
lrwxrwxrwx   1 root root    8 Nov  3 15:22 sbin -> usr/sbin
drwxr-xr-x   2 root root 4096 Nov  3 15:22 srv
dr-xr-xr-x  13 root root    0 Dec 15 09:00 sys
drwxrwxrwt   7 root root 4096 Dec  4 17:37 tmp
drwxr-xr-x  12 root root 4096 Dec  4 17:37 usr
drwxr-xr-x  20 root root 4096 Dec  4 17:37 var
[root@mea ~]# docker run centos2 usr
total 64
dr-xr-xr-x  2 root root 12288 Dec  4 17:37 bin
drwxr-xr-x  2 root root  4096 Nov  3 15:22 games
drwxr-xr-x  3 root root  4096 Dec  4 17:37 include
dr-xr-xr-x 23 root root  4096 Dec  4 17:37 lib
dr-xr-xr-x 24 root root 20480 Dec  4 17:37 lib64
drwxr-xr-x 11 root root  4096 Dec  4 17:37 libexec
drwxr-xr-x 12 root root  4096 Dec  4 17:37 local
dr-xr-xr-x  2 root root  4096 Dec  4 17:37 sbin
drwxr-xr-x 57 root root  4096 Dec  4 17:37 share
drwxr-xr-x  4 root root  4096 Dec  4 17:37 src
lrwxrwxrwx  1 root root    10 Nov  3 15:22 tmp -> ../var/tmp
[root@mea ~]# 

首先直接运行没有问题,但是当我在结尾加上 usr 的时候,等价于 ls -l usr。

因此两者的区别就在于此。都是运行容器时执行指令。CMD是如果我们自己加了参数选择覆盖,ENTRYPOINT则是如果我们自己加上了参数则选择追加。

onbuild镜像案例

创建镜像father。

FROM centos
CMD ["bin","bash"]
ONBUILD RUN echo "triggerred -------------"

创建子镜像,继承父镜像。

FROM father
CMD ["bin", "bash"]

自定义tomcat

首先在当前目录下创建一个tomcat文件,将tomcat和jdk放进去。

构建dockerfile文件。

FROM centos
MAINTAINER  zgg<shiinamashiro163@gmail.com>
 
#把java与tomcat添加到容器中, ADD命令会自动将压缩包进行解压, 如果是COPY则不会自动解压
ADD ./jdk-8u211-linux-x64.tar.gz /usr/local/
ADD ./apache-tomcat-8.5.29.tar.gz /usr/local/
 
#设置工作访问时候的WORKDIR路径,登录落脚点
ENV MYPATH /usr/local
WORKDIR $MYPATH
 
#配置java与tomcat环境变量
ENV JAVA_HOME /usr/local/jdk1.8.0_211
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-8.5.29
ENV CATALINA_BASE /usr/local/apache-tomcat-8.5.29
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
 
#容器运行时监听的端口
EXPOSE  8080
 
#启动时运行tomcat
CMD /usr/local/apache-tomcat-8.5.29/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.8/bin/logs/catalina.out

构建镜像文件。

创建容器并启动。

docker run -d -p 9080:8080 --name mytomcat9 
# 多个主机目录对应多个容器目录
-v /home/satori/tomcat:/usr/local/apache-tomcat-9.0.8/webapps/test 

-v /home/satori/tomcat/tomcat9logs/:/usr/local/apache-tomcat-9.0.8/logs 

--privileged=true tomcat9

# 格式大概长这样  为了解释 我分开写了 但是输入的时候 一定要连起来

我们来验证一下。

docker安装mysql

说句实话,数据库这种东西一般使用容器还是比较少见的。

还是拉取镜像。

[root@mea ~]# docker pull mysql:5.6
Trying to pull repository docker.io/library/mysql ... 
5.6: Pulling from docker.io/library/mysql
Digest: sha256:5cb02288931d4d9f3ac9a837cb84bf1b1b40542fb714f5bfa9d6f07cc9e976d7
Status: Image is up to date for docker.io/mysql:5.6
[root@mea ~]# 

然后启动 mysql。

# 将12345端口映射到3306,指定name为mysql 
docker run -p 12345:3306 --name mysql

# -e 类似于DockerFile里面的 ENV, 用来设置环境变量的
# 而mysql的镜像是有一个初始化脚本,这个脚本会读取这个变量MYSQL_ROOT_PASSWORD, 并将其值初始化root的密码 
-e MYSQL_ROOT_PASSWORD=123456

# 后台运行mysql
-d mysql:5.6
[root@mea ~]# docker run -p 12345:3306 --name mysql \
> -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.6
ebc4b0cc26e77d41869fbdc82a8901166bdb41c955822177eb740dac26021c3d
[root@mea ~]# 

然后客户端连接mysql,执行几条语句。

[root@mea ~]# docker exec -it ebc4b0cc26e77 /bin/sh
# mysql -uroot -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.6.50 MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database db;
Query OK, 1 row affected (0.00 sec)

mysql> use db;
Database changed
mysql>
mysql>
mysql> create table girls(name varchar(255), age int);
Query OK, 0 rows affected (0.02 sec)

mysql> insert into girls values("matsuri", 17);
Query OK, 1 row affected (0.00 sec)

mysql> insert into girls values("mea", 38);
Query OK, 1 row affected (0.01 sec)

mysql> select * from girls;
+---------+------+
| name    | age  |
+---------+------+
| matsuri |   17 |
| mea     |   38 |
+---------+------+
2 rows in set (0.00 sec)

mysql>

Docker网络

当你开始大规模使用 Docker时,那么网络问题就成为你不得不面对的事情了,虽然Docker本身很优秀,但是在网络方面其实还是不完善的,所以我们有必要了解一下Docker的网络。尽管到目前为止,即使我们不了解网络,依旧可以使用Docker正常开发,但这仅限于单个容器。如果多个容器互相通信,那么网络就是不得不考虑的一个问题了。

首先安装完 Docker 时,它会自动创建三个网络:bridge(创建容器默认连接到此网络)、 none 、host。它们的特点如下:

  • bridge:此模式会为每一个容器分配、设置IP等,并将容器连接到一个docker0虚拟网桥,通过docker0网桥以及iptables nat表配置与宿主机通信
  • host:容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口
  • none:该模式关闭了容器的网络功能
[root@mea ~]# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
a795bcb721fb        bridge              bridge              local
7ec4ca3031b2        host                host                local
57d21d733aa7        none                null                local
[root@mea ~]# 

Docker内置这三个网络,运行容器时,你可以使用该 --network 标志来指定容器应连接到哪些网络。不指定的话,Docker守护进程默认将容器连接到此网络。

[root@mea ~]# docker run -it --network=bridge centos
[root@a72e9a61c6f4 /]# exit
exit
[root@mea ~]# docker run -it --network=host centos
[root@mea /]# exit
exit
[root@mea ~]# docker run -it --network=none centos
[root@77af7930be49 /]# exit
exit

下面分别介绍一下Docker的各个网络模式。

host模式

相当于Vmware中的桥接模式,与宿主机在同一个网络中,但没有独立IP地址。

众所周知,Docker 使用了 Linux 的 Namespaces 技术来进行资源隔离,如PID Namespace隔离进程,Mount Namespace隔离文件系统,Network Namespace隔离网络等。

一个Network Namespace提供了一份独立的网络环境,包括网卡、路由、Iptable规则等都与其他的Network Namespace隔离。一个Docker容器一般会分配一个独立的Network Namespace。但如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。

container模式

在理解了host模式后,这个模式也就好理解了。这个模式指定新创建的容器和已经存在的一个容器共享一个Network Namespace,而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。两个容器的进程可以通过lo网卡设备通信。

none模式

该模式将容器放置在它自己的网络栈中,但是并不进行任何配置。实际上,该模式关闭了容器的网络功能,在以下两种情况下是有用的:容器并不需要网络(例如只需要写磁盘卷的批处理任务)。

在docker1.7代码进行了重构,单独把网络部分独立出来编写,所以在docker1.8新加入的一个overlay网络模式。Docker对于网络访问的控制也是在逐渐完善的。

bridge模式

相当于Vmware中的Nat 模式,容器使用独立Network Namespace,并连接到docker0虚拟网卡(默认模式)。通过docker0网桥以及Iptables nat表配置与宿主机通信;bridge模式是Docker默认的网络设置,此模式会为每一个容器分配Network Namespace、设置IP等,并将一个主机上的Docker容器连接到一个虚拟网桥上。下面着重介绍一下此模式。

当Docker Server启动时,会在主机上创建一个名为docker0的虚拟网桥,此主机上启动的Docker容器会连接到这个虚拟网桥上。虚拟网桥的工作方式和物理交换机类似,这样主机上的所有容器就通过交换机连在了一个二层网络中。接下来就要为容器分配IP了,Docker会从RFC1918所定义的私有IP网段中,选择一个和宿主机不同的IP地址和子网分配给docker0,连接到docker0的容器就从这个子网中选择一个未占用的IP使用。一般Docker会使用172.17.0.0/16这个网段,并将172.17.0.1/16分配给docker0网桥(在主机上使用ifconfig命令是可以看到docker0的,可以认为它是网桥的管理接口,在宿主机上作为一块虚拟网卡使用)。单机环境下的网络拓扑如下,主机地址为172.24.60.6/18。

Docker完成以上网络配置的过程大致是这样的:

1. 在主机上创建一对虚拟网卡veth pair设备。veth设备总是成对出现的,它们组成了一个数据的通道,数据从一个设备进入,就会从另一个设备出来。因此,veth设备常用来连接两个网络设备。

2. Docker将veth pair设备的一端放在新创建的容器中,并命名为eth0。另一端放在主机中,以 veth65f9 这样类似的名字命名,并将这个网络设备加入到docker0网桥中。

3. 从docker0子网中分配一个IP给容器使用,并设置docker0的IP地址为容器的默认网关。

相关命令

下面来看看Docker中和网络相关的命令。

使用 docker network inspect 网络:可以查看一个网络的具体信息,可以使用 docker network create -d bridge 网络 来创建一个网络。

[root@mea ~]# docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "a795bcb721fbe9f1df29f59e259ff94c8ba9d9c5c9c01c71c928c48ed9b89982",
        "Created": "2020-12-15T12:03:36.326542104+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Containers": {
            "ebc4b0cc26e77d41869fbdc82a8901166bdb41c955822177eb740dac26021c3d": {
                "Name": "mysql",
                "EndpointID": "a8a1366a3cd9be7f6e3d0fae4f3de74395b58fa01824092b227f82f644fc3d32",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]
[root@mea ~]# docker network create -d bridge my_bridge
92d65957fa6c2c5dafa13c0339a4bf9a5d3b911ddf9dd2b44b92acac639f02ba
[root@mea ~]# 
[root@mea ~]# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
a795bcb721fb        bridge              bridge              local
7ec4ca3031b2        host                host                local
92d65957fa6c        my_bridge           bridge              local
57d21d733aa7        none                null                local
[root@mea ~]# 

当然我们也可以使用 docker network rm 网络:来删除我们创建的网络,注意:docker自带的不可以删除。

[root@mea ~]# docker network rm my_bridge
my_bridge
[root@mea ~]# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
a795bcb721fb        bridge              bridge              local
7ec4ca3031b2        host                host                local
57d21d733aa7        none                null                local
[root@mea ~]# 
[root@mea ~]# docker network rm bridge
Error response from daemon: bridge is a pre-defined network and cannot be removed
[root@mea ~]# 

还可以使用 docker network prune:删除所有未被使用的网络。

如果我们想将某个容器连接到指定的网络上的话,可以通过 "docker network connect 网络 容器" 的方式。还可以通过 "docker network disconnect 网络 容器" 将容器从连接的网络上取消。

docker run -it --link 容器:容器别名 -d 网络 镜像:创建一个容器,此时的容器就和我们指定的容器进行了连接,我们通过连接的容器名、容器id、我们起的容器别名都可以访问对应的容器。如果不指定 "-d 网络" 的话,那么默认走的bridge,注意可以同时连接到多个网络。

[root@mea ~]# docker run -it --name="c1" centos
[root@862cacf76091 /]# [root@mea ~]# 
[root@mea ~]# 
[root@mea ~]# docker run -it --name="c2" --link c1:c1 centos
[root@ab1e24e495d4 /]# ping c1
PING c1 (172.17.0.2) 56(84) bytes of data.
64 bytes from c1 (172.17.0.2): icmp_seq=1 ttl=64 time=0.137 ms
64 bytes from c1 (172.17.0.2): icmp_seq=2 ttl=64 time=0.074 ms
64 bytes from c1 (172.17.0.2): icmp_seq=3 ttl=64 time=0.068 ms
64 bytes from c1 (172.17.0.2): icmp_seq=4 ttl=64 time=0.067 ms
64 bytes from c1 (172.17.0.2): icmp_seq=5 ttl=64 time=0.093 ms
64 bytes from c1 (172.17.0.2): icmp_seq=6 ttl=64 time=0.087 ms
^C
--- c1 ping statistics ---
6 packets transmitted, 6 received, 0% packet loss, time 5ms
rtt min/avg/max/mdev = 0.067/0.087/0.137/0.026 ms
[root@ab1e24e495d4 /]# 

小结

总的来说,Docker 算是越来越火了,而且事实上 Docker 也确实方便,极大地简化了运维人员的工作量。像很多大厂内部都在大量地使用Docker,既然火,那么热度就一定要蹭。

posted @ 2018-07-03 21:35  古明地盆  阅读(2992)  评论(1编辑  收藏  举报