简介
Docker 是一个开源项目,诞生于 2013 年初,最初是 dotCloud 公司内部的一个业余项目。它基于 Google 公司推出的 Go 语言实现。 项目后来加入了 Linux 基金会,遵从了 Apache 2.0 协议,项目代码在 GitHub (改为https://github.com/moby/moby)上进行维护。
Docker 其他教程:https://hik.lanzouy.com/b01240cte
密码:ayb6
下文为精简笔记,来源:稀土掘金 @有明
虚拟化优点
提高计算机资源使用率 ,而非 减少程序资源的占用率,减少程序干扰,解决程序运行冲突。
虚拟化的分类
硬件虚拟化
物理硬件本身就提供虚拟化的支持。举个例子来说,某个平台的 CPU,能够将另外一个平台的指令集转换为自身的指令集执行,并给程序完全运行在那个平台上的感觉。又或者说,CPU 能够自身模拟裂变,让程序或者操作系统认为存在多个 CPU,进而能够同时运行多个程序或者操作系统。这些都是硬件虚拟化的体现。
软件虚拟化
通过软件的方式来实现虚拟化中关键的指令转换部分。依然用 CPU 的例子来说话,在软件虚拟化实现中,通过一层夹杂在应用程序和硬件平台上的虚拟化实现软件来进行指令的转换。也就是说,虽然应用程序向操作系统或者物理硬件发出的指令不是当前硬件平台所支持的指令,这个实现虚拟化的软件也会将之转换为当前硬件平台所能识别的。
虚拟机
通常来说就是通过一个 虚拟机监视器 (Virtual Machine Monitor) 的设施来隔离操作系统与硬件或者应用程序和操作系统,以此达到虚拟化的目的。这个夹在其中的虚拟机监视器,常常被称为 Hypervisor。常用软件有:VMware Workstation、Xen、Java 虚拟机 JVM,PHP 虚拟机 HHVM 等。通过隔离程序和操作系统,将程序的指令转换为当前所在操作系统平台所能执行的指令,达到了不用对程序进行任何修改即可执行的目的。
虽然虚拟机技术得益于 Hypervisor 的加持,使得应用程序或者操作系统可以在无任何修改的情况下运行在另一平台上,但大家很容易发现,其有一个致命的缺陷,就是所有的指令都必须经过虚拟机监视器的处理。这也就意味着,虚拟机的性能是低下的,例如运行在 ZendVM 或者 HHVM 中的 PHP 程序,所有代码虽然编译成了 Opcode 码,但其依然是通过虚拟机才最终转换为机器所能识别的机器码去执行。
这种效率的低下有时候是无法容忍的,为了解决这个问题,真实的虚拟机程序常常不完全遵循 Hypervisor 的设计结构,而是引入一些其他技术来解决效率问题。
例如,在 VMware Workstation、Xen 中我们能够看到硬件辅助虚拟化的使用,通过让指令直达支持虚拟化的硬件,以此避开了效率低下的 Hypervisor。而如 JRE、HPHP 中,除了基于 Hypervisor 实现的 解释执行 机制外,还有 即时编译 (Just In Time) 运行机制,让程序代码在运行前编译成符合当前硬件平台的机器码,这种方式就已经不属于虚拟化的范畴了。
容器技术
容器技术是一种全新意义上的虚拟化技术,按分类或者实现方式来说,其应该属于操作系统虚拟化的范畴,也就是在由操作系统提供虚拟化的支持。
所谓容器技术,指的是操作系统自身支持一些接口,能够让应用程序间可以互不干扰的独立运行,并且能够对其在运行中所使用的资源进行干预。当然,目前来说容器技术还没有一个严格的定义,其实现方式也各有不同,所以这里只能算是我的一点小小总结归纳。
由于应用程序的运行被隔离在了一个独立的运行环境之中,这个独立的运行环境就好似一个容器,包裹住了应用程序,这就是容器技术名字的由来。
容器化运行性能上要远超虚拟机等其他虚拟化实现。更甚一步说,运行在容器虚拟化中的应用程序,在运行效率上与真实运行在物理平台上的应用程序不相上下。其实没通过指令转换,可能不属于虚拟化范畴。
总结:docker 与宿主机是共享内核的
Docker 容器本质上是宿主机上的进程,Docker 通过 namespace 实现了资源隔离,docker 和宿主机共享内核本质上是通过内核的 namespace 和 cgroup 来实现的进程隔离。
Docker 开源地址(改名)
docker 改名风波:Docker 越来越火,便于 docker 商业化,dotCloud 把公司名字改成 Docker Inc. 专门从事 Docker 周边的生意。将 Docker 开源项目的名称修改为了 Moby,因此在 GitHub 上正确地址是:
https://github.com/moby/moby
Docker 的技术实现
Docker 的实现,主要归结于三大技术:命名空间 (Namespaces) 、控制组 (Control Groups) 和联合文件系统 (Union File System) 。
(Union File System)在 Docker 中,提供了一种对 UnionFS 的改进实现,也就是 AUFS (Advanced Union File System)。
AUFS 将文件的更新挂载到老的文件之上,而不去修改那些不更新的内容,这就意味着即使虚拟的文件系统被反复修改,也能保证对真实文件系统的空间占用保持一个较低水平。
也许这个表述还不够形象,那么我们来用 Git 进行比较,会让大家会更容易理解。大家知道,我们在 Git 中每进行一次提交,Git 并不是将我们所有的内容打包成一个版本,而只是将修改的部分进行记录,这样即使我们提交很多次后,代码库的空间占用也不会倍数增加。
同样的,通过 AUFS,Docker 大幅减少了虚拟文件系统对物理存储空间的占用。由此,Docker 也开创出了虚拟化领域很多新的轻量级解决方案,这在之后的小节里我们会提到。
Docker 的理念
Docker 推崇一种轻量级容器的结构,即一个应用一个容器。例如我们要搭建一套 LAMP 结构的服务,我们通常会建立一个虚拟机,在虚拟机中安装上 Linux 系统,之后分别安装 Apache、MySQL 和 PHP。而在 Docker 里,最佳的实践是分别基于 Apache、MySQL 和 PHP 的镜像建立三个容器,分别运行 Apache、MySQL 和 PHP ,而它们所在的虚拟操作系统也直接共享于宿主机的操作系统。
属性 | Docker | 虚拟机 |
---|---|---|
启动速度 | 秒级 | 分钟级 |
硬盘使用 | MB 级 | GB 级 |
性能 | 接近原生 | 较低 |
普通机器支撑量 | 数百个 | 几个 |
首先,只有在容器技术的支撑下,应用即容器的方案才能有效的实施。因为容器技术既剔除了 Hypervisor 层,又干掉了虚拟操作系统的概念,让容器中应用运行的消耗与真实操作系统中运行的消耗几乎完全一致。只有这样,我们才能像在真实操作系统中开启应用一样开启新的容器,而不用过分担心虚拟化带来的性能消耗。
其次,基于联合文件系统的底层文件系统支持,让容器能够很容易在真实操作系统中共享存储资源,并由此带来了对存储空间的低消耗。与动辄就要独立开辟十几 GB 甚至几十 GB 的虚拟化实现相比,要存在巨大的优势。
当然,Docker 也支持你在容器中同时运行很多种程序,但其容器设计本身并不针对这种方案,所以如果你以这种方案在 Docker 中搭建环境,你会花费不少时间做出一些本来并不需要做的事情。虽然这看上去动手性很强,但我并不推荐在工作中这么去做,因为我们使用 Docker 本身就是为了效率,浪费时间在这些不必要的事情上,已经违背了我们使用 Docker 的初衷。
Docker 核心
镜像 (Image)、容器 (Container)、网络 (Network)、数据卷 (Volume)
镜像
镜像 (Image) 这个概念相信大家不会陌生,因为它是其他虚拟化技术 (特别是虚拟机) 中常常被使用的一个概念。所谓镜像,可以理解为一个只读的文件包,其中包含了 虚拟环境运行最原始文件系统的内容。
当然,Docker 的镜像与虚拟机中的镜像还是有一定区别的。首先,之前我们谈到了 Docker 中的一个创新是利用了 AUFS 作为底层文件系统实现,通过这种方式,Docker 实现了一种增量式的镜像结构。
每次对镜像内容的修改,Docker 都会将这些修改铸造成一个镜像层,而一个镜像其实就是由其下层所有的镜像层所组成的。当然,每一个镜像层单独拿出来,与它之下的镜像层都可以组成一个镜像。
另外,由于这种结构,Docker 的镜像实质上是无法被修改的,因为所有对镜像的修改只会产生容器
容器 (Container) 就更好理解了,在容器技术中,容器就是用来隔离虚拟环境的基础设施,而在 Docker 里,它也被引申为隔离出来的虚拟环境。
如果把镜像理解为编程中的类,那么容器就可以理解为类的实例。镜像内存放的是不可变化的东西,当以它们为基础的容器启动后,容器内也就成为了一个“活”的空间。
用更官方的定义,Docker 的容器应该有三项内容组成:
- 一个 Docker 镜像
- 一个程序运行环境
- 一个指令集合
,而不是更新原有的镜像。
容器
容器 (Container) 就更好理解了,在容器技术中,容器就是用来隔离虚拟环境的基础设施,而在 Docker 里,它也被引申为隔离出来的虚拟环境。
如果把镜像理解为编程中的类,那么容器就可以理解为类的实例。镜像内存放的是不可变化的东西,当以它们为基础的容器启动后,容器内也就成为了一个“活”的空间。
用更官方的定义,Docker 的容器应该有三项内容组成:
- 一个 Docker 镜像
- 一个程序运行环境
- 一个指令集合
网络
对于大部分程序来说,它们的运行都不会是孤立的,而是要与外界或者更准确的说是与其他程序进行交互的,这里的交互绝大多数情况下指的就是数据信息的交换。网络通讯是目前最常用的一种程序间的数据交换方式了。
由于计算机网络领域拥有相对统一且独立的协议等约定,其跨平台性非常优秀,所有的应用都可以通过网络在不同的硬件平台或操作系统平台上进行数据交互。特别是在分布式云计算的时代,应用或服务间的通讯更是充分依赖于网络传输,所以自然拥有一套完善的网络体系支撑,是承载应用运行所必须的基础设施。
在 Docker 中,实现了强大的网络功能,我们不但能够十分轻松的对每个容器的网络进行配置,还能在容器间建立虚拟网络,将数个容器包裹其中,同时与其他网络环境隔离。
另外,利用一些技术,Docker 能够在容器中营造独立的域名解析环境,这使得我们可以在不修改代码和配置的前提下直接迁移容器,Docker 会为我们完成新环境的网络适配。对于这个功能,我们甚至能够在不同的物理服务器间实现,让处在两台物理机上的两个 Docker 所提供的容器,加入到同一个虚拟网络中,形成完全屏蔽硬件的效果。
数据卷
得益于 Docker 底层的 Union File System 技术。在 UnionFS 的加持下,除了能够从宿主操作系统中挂载目录外,还能够建立独立的目录持久存放数据,或者在容器间共享。在 Docker 中,通过这几种方式进行数据共享或持久化的文件或目录,我们都称为数据卷 (Volume)。
Docker 软件
Docker Engine
时至今日,Docker 生态已经远比它诞生之初要庞大许多,虽然我们仍然习惯使用 Docker 这个名字去指代实现容器技术支持的软件,但显然更加容易与其他的概念产生混淆。这里我们很有必要对这个 Docker 中最核心的软件进行介绍,不仅因为它在 Docker 生态中扮演着中心的地位,也因为它是我们在开发中实实在在接触最多的东西。
目前这款实现容器化的工具是由 Docker 官方进行维护的,Docker 官方将其命名为 Docker Engine,同时定义其为工业级的容器引擎 (Industry-standard Container Engine)。在 Docker Engine 中,实现了 Docker 技术中最核心的部分,也就是容器引擎这一部分。
docker daemon 和 docker CLI
虽然我们说 Docker Engine 是一款软件,但实实在在去深究的话,它其实算是由多个独立软件所组成的软件包。在这些程序中,最核心的就是 docker daemon 和 docker CLI 这俩了。
所有我们通常认为的 Docker 所能提供的容器管理、应用编排、镜像分发等功能,都集中在了 docker daemon 中,而我们之前所提到的镜像模块、容器模块、数据卷模块和网络模块也都实现在其中。在操作系统里,docker daemon 通常以服务的形式运行以便静默的提供这些功能,所以我们也通常称之为 Docker 服务。
在 docker daemon 管理容器等相关资源的同时,它也向外暴露了一套 RESTful API,我们能够通过这套接口对 docker daemon 进行操作。或者更确切的说,是通过这套 RESTful API 对 docker daemon 中运行的容器和其他资源进行管理。
通常来说,我们是采用在控制台或者命令行输入命令来控制 docker daemon 的,因为这样很酷也更容易适应那些有或者没有图形界面的操作系统。
那么问题来了,如果我们在控制台中编写一个 HTTP 请求以借助 docker daemon 提供的 RESTful API 来操控它,那显然是个费脑、费手又费时间的活儿。所以在 Docker Engine 里还直接附带了 docker CLI 这个控制台程序。
docker daemon 和 docker CLI 所组成的,正是一个标准 C/S (Client-Server) 结构的应用程序。衔接这两者的,正是 docker daemon 所提供的这套 RESTful API。
安装
CentOS
sudo yum install yum-utils device-mapper-persistent-data lvm2
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce
sudo systemctl enable docker
sudo systemctl start docker
Fedora
sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian (lsb_release -cs) stable"
sudo apt-get update
sudo apt-get install docker-ce
sudo systemctl enable docker
sudo systemctl start docker
Ubuntu
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu (lsb_release -cs) stable"
sudo apt-get update
sudo apt-get install docker-ce
sudo systemctl enable docker
sudo systemctl start docker
常用命令
Docker
阿里云公有镜像仓库:dev.aliyun.com
可理解为小型 Linux 系统,只是没网卡之类,启动快维护便捷等。docker 依赖于宿主机。注册中心 Registy 有:
https://hub.docker.com
https://lug.ustc.edu.cn/wiki/mirrors/help/docker
https://i8tth4vo.mirror.aliyuncs.com
ustc docker mirror 的优势之一就是不需要注册,是真正的公共服务。
centOs 中常用命令:
sudo yum update 更新最新源
docker search 搜索 docker
//yum-util 提供 yum-config-manager 功能,另外两个是 devicemapper 驱动依赖的
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
// 设置 yum 源为阿里云
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sudo yum install docker-ce 安装 docker 服务
docker -v 查看版本
修改 docker 注册中心
vi /etc/docker/daemon.json
编辑内容(阿里云需注册登录获取镜像加速地址):
{"registry-mirrors": ["https://i8tth4vo.mirror.aliyuncs.com"]
}
启动 docker:
systemctl start docker
停止 docker:
systemctl stop docker
重启 docker:
systemctl restart docker
查看 docker 状态:
systemctl status docker
开机启动:
systemctl enable docker
查看 docker 概要信息
docker info
查看 docker 帮助文档
docker --help
操作命令
交互式运行后进入 docker,守护式则不用;文件可以 cp 拷贝本地文件到 docker 或者挂载目录达到文件共享;IP 需要关联映射。
Dockerfile 脚本可以用来创建镜像。
查看镜像
docker images
命令的结果中,我们可以看到 镜像的 ID (IMAGE ID)、构建时间 (CREATED)、占用空间 (SIZE) 等数据。
username: 主要用于识别上传镜像的不同用户,与 GitHub 中的用户空间类似。
repository:主要用于识别进行的内容,形成对镜像的表意描述。
tag:主要用户表示镜像的版本,方便区分进行内容的不同细节
对于 username 来说,在上面我们展示的
docker images
结果中,有的镜像有 username 这个部分,而有的镜像是没有的。没有 username 这个部分的镜像,表示镜像是由 Docker 官方所维护和提供的,所以就不单独标记用户了。如果大家再多接触一些镜像,会发现 Docker 中镜像的 repository 部分通常采用的是软件名。这时候大家一定要注意了,镜像还是镜像,镜像名还是镜像名,其与软件命名其实是独立的。
当我们在操作中没有具体给出镜像的 tag 时,Docker 会采用 latest 作为缺省 tag,方便获取使用最新镜像。
当我们在操作中没有具体给出镜像的 tag 时,Docker 会采用 latest 作为缺省 tag
拉镜像
$ sudo docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
124c757242f8: Downloading [===============================================>] 30.19MB/31.76MB
9d866f8bde2a: Download complete
fa3f2f277e67: Download complete
398d32b153e8: Download complete
afde35469481: Download complete
下载进度会分为几行,其实每一行代表的就是一个镜像层。Docker 首先会拉取镜像所基于的所有镜像层,之后再单独拉取每一个镜像层并组合成这个镜像。当然,如果在本地已经存在相同的镜像层 (共享于其他的镜像),那么 Docker 就直接略过这个镜像层的拉取而直接采用本地的内容。标签缺省默认拉取最新,亦可以指定:
sudo docker pull openresty/openresty:1.13.6.2-alpine
镜像在被拉取之后,就存放到了本地,接受当前这个 Docker 实例管理了,我们可以通过 docker images
命令看到它们。
Docker Hub
Docker Hub 是 Docker 官方建立的中央镜像仓库,除了普通镜像仓库的功能外,它内部还有更加细致的权限管理,支持构建钩子和自动构建,并且有一套精致的 Web 操作页面。
Docker Hub 的地址是:hub.docker.com/
由于定位是 Docker 的中央镜像仓库系统,同时也是 Docker Engine 的默认镜像仓库,所以 Docker Hub 是开发者共享镜像的首选,那么也就意味着其中的镜像足够丰富。
搜索镜像
除了直接通过 Docker Hub 网站搜索镜像这种方式外,我们还可以用 docker CLI 中的 docker search
这个命令搜索 Docker Hub 中的镜像。
管理镜像信息
如果我们要获得 镜像 更详细的信息,我们可以通过 docker inspect
这个命令。
$ sudo docker inspect redis:3.2
[
{
"Id": "sha256:2fef532eadb328740479f93b4a1b7595d412b9105ca8face42d3245485c39ddc",
"RepoTags": ["redis:3.2"],
"RepoDigests": ["redis@sha256:745bdd82bad441a666ee4c23adb7a4c8fac4b564a1c7ac4454aa81e91057d977"],
## ......
}
]
docker inspect
还能查看 容器 等之前我们所提到的 Docker 对象的信息,而传参的方式除了传递镜像或容器的名称外,还可以传入镜像 ID 或容器 ID。
$ sudo docker inspect redis:4.0
$ sudo docker inspect 2fef532e
其实 inspect 后 id 类似于模糊搜索,因此即使 id、名称未写全也可以进行匹配。
可以通过如下过滤获取容器 IP 信息。
docker inspect --format='{{.NetworkSettings.IPAddress}}' 容器名称(容器 ID)
删除镜像
删除镜像的命令是 docker rmi
,参数是镜像的名称或 ID,它还可以后面接空格删除多个镜像。
sudo docker rmi ubuntu:latest
sudo docker rmi redis:3.2 redis:4.0
删除所有
docker rmi `docker images -q`
删除报错:删除镜像的时候出现错误 Error response from daemon: conflict: unable to delete 4037a5562b03 (must be forced) - image is being used by stopped container 432e0264d475, 解决先通过 docker rm 删除依赖的容器之后在删除镜像。
创建容器
当我们选择好镜像以后,就可以通过 docker create
这个命令来创建容器了。
$ sudo docker create nginx:1.12
34f277e22be252b51d204acbb32ce21181df86520de0c337a835de6932ca06c3
执行 docker create
后,Docker 会根据我们所给出的镜像创建容器,在控制台中会打印出 Docker 为容器所分配的容器 ID,此时容器是处于 Created 状态的。这时 ID 是随机给的,我们可以进行自定义:
$ sudo docker create --name nginx nginx:1.12
启动容器
通过 docker create
创建的容器,是处于 Created 状态的,其内部的应用程序还没有启动,所以我们需要通过 docker start
命令来启动它。
$ sudo docker start nginx
当容器启动后,其中的应用就会运行起来,容器的几个生命周期也会绑定到了这个应用上,这个之前我们已经提及,这里就不在赘述。只要应用程序还在运行,那么容器的状态就会是 Running,除非进行一些修改容器的操作。
创建与启动
在 Docker 里,还允许我们通过 docker run
这个命令将 docker create
和 docker start
这两步操作合成为一步,进一步提高工作效率。
$ sudo docker run --name nginx -d nginx:1.12
89f2b769498a50f5c35a314ab82300ce9945cbb69da9cda4b022646125db8ca7
通过 docker run
创建的容器,在创建完成之后会直接启动起来,不需要我们再使用 docker start
去启动了。
docker 默认前台运行,关闭控制台程序随之关闭,通过 -d
或 --detach
这个选项告诉 Docker 在启动后将程序与控制台分离,使其进入“后台”运行。
启动参数说明
-i:表示运行容器
-t:表示容器启动后会进入其命令行。加入这两个参数后,容器创建就能登录进去。即分配一个伪终端。
–name : 为创建的容器命名。
-v:表示目录映射关系(前者是宿主机目录,后者是映射到宿主机上的目录),可以使用多个-v 做多个目录
或文件映射。注意:最好做目录映射,在宿主机上做修改,然后共享到容器上。
-d:在 run 后面加上 - d 参数, 则会创建一个守护式容器在后台运行(这样创建容器后不会自动登录容器,如果只
加 -i - t 两个参数,创建后就会自动进去容器)。
-p:表示端口映射,前者是宿主机端口,后者是容器内的映射端口。可以使用多个 - p 做多个端口映射
交互式
docker run -it --name= 容器名称 镜像名称: 标签 /bin/
守护式
docker run -di --name= 容器名称 镜像名称: 标签
登录守护式容器控制台
docker exec -it 容器名称 (或者容器 ID) /bin/bash
目录挂载
创建容器的时候,将宿主机的目录与容器内的目录进行映射, 创建容器 添加 - v 参数 后边为 宿主机目录: 容器目录。
docker run -di -v /usr/local/myhtml:/usr/local/myhtml --name=mycentos3 centos:7
映射端口
docker run -di --name=myproject_mysql -p 33306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql
docker run -di --name=mytomcat -p 9000:8080 -v /usr/local/webapps:/usr/local/tomcat/webapps tomcat:7-jre7
-p 代表端口映射,格式为 宿主机映射端口: 容器运行端口
-e 代表添加环境变量 上面例子就是 MYSQL_ROOT_PASSWORD 是 root 用户的登陆密码
管理容器
docker ps 查看正在运行的容器
docker ps –a 或 docker ps –all 查看所有容器
docker ps –l 查看最后一次运行的容器
docker ps -f status=exited 查看状态停止的容器
显示
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
425a0d3cd18b redis:3.2 "docker-entrypoint.s…" 2 minutes ago Created redis
89f2b769498a nginx:1.12 "nginx -g 'daemon of…" About an hour ago Up About an hour 80/tcp nginx
在 docker ps
的结果中,我们可以看到几项关于容器的信息。其中 CONTAINER ID、IMAGE、CREATED、NAMES 大家都比较容易理解,分别表示容器 ID,容器所基于的镜像,容器的创建时间和容器的名称。
结果中的 COMMAND 表示的是容器中主程序 (也就是与容器生命周期所绑定进程所关联的程序) 的启动命令,这条命令是在镜像内定义的,而容器的启动其实质就是启动这条命令。关于 COMMAND 的更多知识,我们在之后的 Docker 镜像制作中会更详细的解读。
结果中的 STATUS 表示容器所处的状态,其值和我们之前所谈到的状态有所区别,主要是因为这里还记录了其他的一些信息。在这里,常见的状态表示有三种:
- Created 此时容器已创建,但还没有被启动过。
- Up [Time] 这时候容器处于正在运行状态,而这里的 Time 表示容器从开始运行到查看时的时间。
- Exited ([Code]) [Time] 容器已经结束运行,这里的 Code 表示容器结束运行时,主程序返回的程序退出码,而 Time 则表示容器结束到查看时的时间。
有些读者有疑问,既然是列出容器,应该为命令取一些带有 ls
字眼的名字,为啥会用类似 Linux 中查看进程的 ps
呢?这其实有一部分历史原因,由于容器并非真的包裹住了进程,而只是隔离了进程,进程还是允许在宿主机操作系统之上的,所以列出镜像的过程到更新是查看正在运行的进程,故而有了这样的名字。
当然,在 Docker 逐渐成熟后,命令的命名也没有原来那么随意了,已经逐渐转换为使用大家广泛认可的形式。只是 docker ps
这条命令,还保留着复古的风格。
停止和删除容器
要将正在运行的容器停止,我们可以使用 docker stop
命令。
$ sudo docker stop nginx
容器停止后,其维持的文件系统沙盒环境还是存在的,内部被修改的内容也都会保留,我们可以通过 docker start
命令将这个容器再次启动。
当我们需要完全删除容器时,可以通过 docker rm
命令将容器进行删除。
$ sudo docker rm nginx
正在运行中的容器默认情况下是不能被删除的,我们可以通过增加 -f
或 --force
选项来让 docker rm
强制停止并删除容器,不过这种做法并不妥当。
容器内操作
之前登录守护式操作,/bin/bash 可以省略为 bash,当然了你也可以用 /bin/sh 启动,但是省略了目录显示观感不直观。
docker exec -it 容器名称 (或者容器 ID) /bin/bash
docker exec -it nginx bash
其中 -i
(--interactive
) 表示保持我们的输入流,只有使用它才能保证控制台程序能够正确识别我们的命令。而 -t
(--tty
) 表示启用一个伪终端,形成我们与 bash 的交互,如果没有它,我们无法看到 bash 内部的执行结果。
用容器中的 more
命令查看容器的主机名定义
$ sudo docker exec nginx more /etc/hostname
::::::::::::::
/etc/hostname
::::::::::::::
83821ea220ed
Docker 为我们提供了一个 docker attach
命令,用于将当前的输入输出流连接到指定的容器上。
$ sudo docker attach nginx
attach 是可以带上 --sig-proxy=false 来确保 CTRL- D 或 CTRL- C 不会关闭容器。
sudo docker attach --sig-proxy=false nginx
这个命令最直观的效果可以理解为我们将容器中的主程序转为了“前台”运行 (与 docker run
中的 -d
选项有相反的意思 )。用途不大,不赘述。
创建启动 Nginx 容器,之后进入到容器中,利用相关命令停止 Nginx 程序的运行,杀掉容器中的主进程,相当于杀掉了容器,因此容器内进程变化影响到容器运行状态,容器并非单独的虚拟系统。
配置网络
在 Docker 网络中,有三个比较核心的概念,也就是:沙盒 (Sandbox)、网络 (Network)、端点 (Endpoint)。
- 沙盒 提供了容器的虚拟网络栈,也就是之前所提到的端口套接字、IP 路由表、防火墙等的内容。其实现隔离了容器网络与宿主机网络,形成了完全独立的容器网络环境。
- 网络 可以理解为 Docker 内部的虚拟子网,网络内的参与者相互可见并能够进行通讯。Docker 的这种虚拟网络也是于宿主机网络存在隔离关系的,其目的主要是形成容器间的安全通讯环境。
- 端点 是位于容器或网络隔离墙之上的洞,其主要目的是形成一个可以控制的突破封闭的网络环境的出入口。当容器的端点与网络的端点形成配对后,就如同在这两者之间搭建了桥梁,便能够进行数据传输了。
这三者形成了 Docker 网络的核心模型,也就是容器网络模型 (Container Network Model)。
容器互联
让一个容器连接到另外一个容器,我们可以在容器通过 docker create
或 docker run
创建时通过 --link
选项进行配置。
例如,这里我们创建一个 MySQL 容器,将运行我们 Web 应用的容器连接到这个 MySQL 容器上,打通两个容器间的网络,实现它们之间的网络互通。
$ sudo docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes mysql
$ sudo docker run -d --name webapp --link mysql webapp:latest
容器间的网络已经打通,那么我们要如何在 Web 应用中连接到 MySQL 数据库呢?Docker 为容器间连接提供了一种非常友好的方式,我们只需要将容器的网络命名填入到连接地址中,就可以访问需要连接的容器了。
假设我们在 Web 应用中使用的是 JDBC 进行数据库连接的,我们可以这么填写连接。
String url = "jdbc:mysql://mysql:3306/webapp";
在这里,连接地址中的 mysql 就好似我们常见的域名解析,Docker 会将其指向 MySQL 容器的 IP 地址。
别名连接
$ sudo docker run -d --name webapp --link mysql:database webapp:latest
在这里,我们使用 --link <name>:<alias>
的形式,连接到 MySQL 容器,并设置它的别名为 database。当我们要在 Web 应用中使用 MySQL 连接时,我们就可以使用 database 来代替连接地址了。
String url = "jdbc:mysql://database:3306/webapp";
优点:在以往的开发中,我们每切换一个环境 (例如将程序从开发环境提交到测试环境),都需要重新配置程序中的各项连接地址等参数,而在 Docker 里,我们并不需要关心这个,只需要程序中配置被连接容器的别名,映射 IP 的工作就交给 Docker 完成了。
暴露端口
需要注意的是,虽然容器间的网络打通了,但并不意味着我们可以任意访问被连接容器中的任何服务。Docker 为容器网络增加了一套安全机制,只有容器自身允许的端口,才能被其他容器所访问。
这个容器自我标记端口可被访问的过程,我们通常称为暴露端口。我们在 docker ps
的结果中可以看到容器暴露给其他容器访问的端口。
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
95507bc88082 mysql:5.7 "docker-entrypoint.s…" 17 seconds ago Up 16 seconds 3306/tcp, 33060/tcp mysql
这里我们看到,MySQL 这个容器暴露的端口是 3306 和 33060。所以我们连接到 MySQL 容器后,只能对这两个端口进行访问。
端口的暴露可以通过 Docker 镜像进行定义,也可以在容器创建时进行定义。在容器创建时进行定义的方法是借助 --expose
这个选项。
$ sudo docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --expose 13306 --expose 23306 mysql:5.7
这里我们为 MySQL 暴露了 13306 和 23306 这两个端口,暴露后我们可以在 docker ps
中看到这两个端口已经成功的打开。
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3c4e645f21d7 mysql:5.7 "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 3306/tcp, 13306/tcp, 23306/tcp, 33060/tcp mysql
容器暴露了端口只是类似我们打开了容器的防火墙,具体能不能通过这个端口访问容器中的服务,还需要容器中的应用监听并处理来自这个端口的请求。
管理网络
容器能够互相连接的前提是两者同处于一个网络中 (这里的网络是指容器网络模型中的网络)。这个限制很好理解,刚才我们说了,网络这个概念我们可以理解为 Docker 所虚拟的子网,而容器网络沙盒可以看做是虚拟的主机,只有当多个主机在同一子网里时,才能互相看到并进行网络数据交换。
当我们启动 Docker 服务时,它会为我们创建一个默认的 bridge 网络,而我们创建的容器在不专门指定网络的情况下都会连接到这个网络上。所以我们刚才之所以能够把 webapp 容器连接到 mysql 容器上,其原因是两者都处于 bridge 这个网络上。
我们通过 docker inspect
命令查看容器,可以在 Network 部分看到容器网络相关的信息。
$ sudo docker inspect mysql
[
{
## ......
"NetworkSettings": {
## ......
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "bc14eb1da66b67c7d155d6c78cb5389d4ffa6c719c8be3280628b7b54617441b",
"EndpointID": "1e201db6858341d326be4510971b2f81f0f85ebd09b9b168e1df61bab18a6f22",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:02",
"DriverOpts": null
}
}
## ......
}
## ......
}
]
这里我们能够看到 mysql 容器在 bridge 网络中所分配的 IP 地址,其自身的端点、Mac 地址,bridge 网络的网关地址等信息。
Docker 默认创建的这个 bridge 网络是非常重要的,理由自然是在没有明确指定容器网络时,容器都会连接到这个网络中。在之前讲解 Docker for Win 和 Docker for Mac 安装的时候,我们提到过这两个软件的配置中都有一块配置 Docker 中默认网络的内容,这块所指的默认网络就是这个 bridge 网络。
创建网络
在 Docker 里,我们也能够创建网络,形成自己定义虚拟子网的目的。
docker CLI 里与网络相关的命令都以 docker network
开头,其中创建网络的命令是 docker network create
。
$ sudo docker network create -d bridge individual
通过 -d
选项我们可以为新的网络指定驱动的类型,其值可以是刚才我们所提及的 bridge、host、overlay、maclan、none,也可以是其他网络驱动插件所定义的类型。这里我们使用的是 Bridge Driver (当我们不指定网络驱动时,Docker 也会默认采用 Bridge Driver 作为网络驱动)。
通过 docker network ls
或是 docker network list
可以查看 Docker 中已经存在的网络。
$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
bc14eb1da66b bridge bridge local
35c3ef1cc27d individual bridge local
之后在我们创建容器时,可以通过 --network
来指定容器所加入的网络,一旦这个参数被指定,容器便不会默认加入到 bridge 这个网络中了 (但是仍然可以通过 --network bridge
让其加入 )。
$ sudo docker run -d --name mysql -e MYSQL_RANDOM_ROOT_PASSWORD=yes --network individual mysql:5.7
我们通过 docker inspect
观察一下此时的容器网络。
$ sudo docker inspect mysql
[
{
## ......
"NetworkSettings": {
## ......
"Networks": {
"individual": {
"IPAMConfig": null,
"Links": null,
"Aliases": ["2ad678e6d110"],
"NetworkID": "35c3ef1cc27d24e15a2b22bdd606dc28e58f0593ead6a57da34a8ed989b1b15d",
"EndpointID": "41a2345b913a45c3c5aae258776fcd1be03b812403e249f96b161e50d66595ab",
"Gateway": "172.18.0.1",
"IPAddress": "172.18.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:12:00:02",
"DriverOpts": null
}
}
## ......
}
## ......
}
]
可以看到,容器所加入网络已经变成了 individual 这个网络了。
这时候我们通过 --link
让处于另外一个网络的容器连接到这个容器上,看看会发生什么样的效果。
$ sudo docker run -d --name webapp --link mysql --network bridge webapp:latest
docker: Error response from daemon: Cannot link to /mysql, as it does not belong to the default network.
ERRO[0000] error waiting for container: context canceled
可以看到容器并不能正常的启动,而 Docker 提醒我们两个容器处于不同的网络,之间是不能相互连接引用的。
我们来改变一下,让运行 Web 应用的容器加入到 individual 这个网络,就可以成功建立容器间的网络连接了。
$ sudo docker run -d --name webapp --link mysql --network individual webapp:latest
端口映射
刚才我们提及的都是容器直接通过 Docker 网络进行的互相访问,在实际使用中,还有一个非常常见的需求,就是我们需要在容器外通过网络访问容器中的应用。最简单的一个例子,我们提供了 Web 服务,那么我们就需要提供一种方式访问运行在容器中的 Web 应用。
在 Docker 中,提供了一个端口映射的功能实现这样的需求。
通过 Docker 端口映射功能,我们可以把容器的端口映射到宿主操作系统的端口上,当我们从外部访问宿主操作系统的端口时,数据请求就会自动发送给与之关联的容器端口。
要映射端口,我们可以在创建容器时使用 -p
或者是 --publish
选项。
$ sudo docker run -d --name nginx -p 80:80 -p 443:443 nginx:1.12
使用端口映射选项的格式是 -p <ip>:<host-port>:<container-port>
,其中 ip 是宿主操作系统的监听 ip,可以用来控制监听的网卡,默认为 0.0.0.0,也就是监听所有网卡。host-port 和 container-port 分别表示映射到宿主操作系统的端口和容器的端口,这两者是可以不一样的,我们可以将容器的 80 端口映射到宿主操作系统的 8080 端口,传入 -p 8080:80
即可。
我们可以在容器列表里看到端口映射的配置。
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bc79fc5d42a6 nginx:1.12 "nginx -g 'daemon of…" 4 seconds ago Up 2 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp nginx
打印的结果里用 ->
标记了端口的映射关系。
在 Windows 和 macOS 中使用映射
Docker 的端口映射功能是将容器端口映射到宿主操作系统的端口上,实际来说就是映射到了 Linux 系统的端口上。而我们知道,在 Windows 和 macOS 中运行的 Docker,其 Linux 环境是被虚拟出来的,如果我们仅仅是将端口映射到 Linux 上,由于虚拟环境还有一层隔离,我们依然不能通过 Windows 或 macOS 的端口来访问容器。
解决这种问题的方法很简单,只需要再加一次映射,将虚拟 Linux 系统中的端口映射到 Windows 或 macOS 的端口即可。
如果我们使用 Docker for Windows 或 Docker for Mac,这个端口映射的操作程序会自动帮助我们完成,所以我们不需要做任何额外的事情,就能够直接使用 Windows 或 macOS 的端口访问容器端口了。
而当我们使用 Docker Toolbox 时,由于其自动化能力比较差,所以需要我们在 VirtualBox 里单独配置这个操作系统端口到 Linux 端口的映射关系。
在 VirtualBox 配置中的端口转发一栏里,进行相关的配置即可。
备份与恢复
备份
将容器 mynginx 创建备份镜像 mynginx_i
docker commit mynginx mynginx_i
在使用 docker commit
提交镜像更新后,我们可以得到 Docker 创建的新镜像的 ID,之后我们也能够从本地镜像列表中找到它。
我们还能在提交容器更改的时候给出一个提交信息,方便以后查询。
docker commit -m "Configured" webapp
使用 docker tag
能够为未命名的镜像指定镜像名,也能够对已有的镜像创建一个新的命名。
$ sudo docker tag 0bc42f7ff218 webapp:1.0
$ sudo docker tag webapp:1.0 webapp:latest
除了使用 docker tag
在容器提交为新的镜像后为镜像命名这种方式外,我们还可以直接在 docker commit
命令里指定新的镜像名
$ sudo docker commit -m "Upgrade" webapp webapp:2.0
打包为 tar 保存在用户目录,docker save
命令可以将镜像输出,提供了一种让我们保存镜像到 Docker 外部的方式。
docker save -o ./mynginx.tar mynginx_i // 推荐方式
docker save webapp:1.0 > webapp-1.0.tar // 使用管道方式不需要 - o 参数
恢复
首先我们先删除掉 mynginx_img 镜像 然后执行此命令进行恢复
docker load -i mynginx.tar
docker load < webapp-1.0.tar
-i 输入的文件
执行后再次查看镜像,可以看到镜像已经恢复
批量迁移
通过 docker save
和 docker load
命令我们还能够批量迁移镜像,只要我们在 docker save
中传入多个镜像名作为参数,它就能够将这些镜像都打成一个包,便于我们一次性迁移多个镜像。
$ sudo docker save -o ./images.tar webapp:1.0 nginx:1.12 mysql:5.7
装有多个镜像的包可以直接被 docker load
识别和读取,我们将这个包导入后,所有其中装载的镜像都会被导入到 Docker 之中。
直接导入导出
使用 docker export
命令我们可以直接导出容器,我们可以把它简单的理解为 docker commit
与 docker save
的结合体。
$ sudo docker export -o ./webapp.tar webapp
相对的,使用 docker export
导出的容器包,我们可以使用 docker import
导入。这里需要注意的是,使用 docker import
并非直接将容器导入,而是将容器运行时的内容以镜像的形式导入。所以导入的结果其实是一个镜像,而不是容器。在 docker import
的参数里,我们可以给这个镜像命名。
$ sudo docker import ./webapp.tar webapp:1.0
在开发的过程中,使用 docker save
和 docker load
,或者是使用 docker export
和 docker import
都可以达到迁移容器或者镜像的目的。
Q:容器直接导入执行报错 docker: Error response from daemon: No command specified。
A:使用 import 导入后的镜像,启动容器时,需要传入 command,而这个 command 可以参照原镜像容器或者开发人员提供的参数,ps -a –no-trunc 可查看到完整的 command,追加在容器启动命令后面就可以了
数据存储
Docker 容器显著的两点弊端就是:
- 沙盒文件系统是跟随容器生命周期所创建和移除的,数据无法直接被持久化存储。
- 由于容器隔离,我们很难从容器外部获得或操作容器内部文件中的数据。
得益于 Docker 容器文件系统是基于 UnionFS。由于 UnionFS 支持挂载不同类型的文件系统到统一的目录结构中,所以我们只需要将宿主操作系统中,文件系统里的文件或目录挂载到容器中,便能够让容器内外共享这个文件。
挂载方式
- Bind Mount 能够直接将宿主操作系统中的目录和文件挂载到容器内的文件系统中,通过指定容器外的路径和容器内的路径,就可以形成挂载映射关系,在容器内外对文件的读写,都是相互可见的。
- Volume 也是从宿主操作系统中挂载目录到容器内,只不过这个挂载的目录由 Docker 进行管理,我们只需要指定容器内的目录,不需要关心具体挂载到了宿主操作系统中的哪里。
- Tmpfs Mount 支持挂载系统内存中的一部分到容器的文件系统里,不过由于内存和容器的特征,它的存储并不是持久的,其中的内容会随着容器的停止而消失。
挂载文件到容器
要将宿主操作系统中的目录挂载到容器之后,我们可以在容器创建的时候通过传递 -v
或 --volume
选项来指定内外挂载的对应目录或文件。记住:左宿主,右容器。
$ sudo docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html nginx:1.12
使用 -v
或 --volume
来挂载宿主操作系统目录的形式是 -v <host-path>:<container-path>
或 --volume <host-path>:<container-path>
,其中 host-path 和 container-path 分别代表宿主操作系统中的目录和容器中的目录。这里需要注意的是,为了避免混淆,Docker 这里强制定义目录时必须使用绝对路径,不能使用相对路径。
然后我们在宿主机新建 index.html 文件就可以在容器中看到了
$ sudo docker exec nginx ls /usr/share/nginx/html
index.html
在 docker inspect
的结果里,我们可以看到有关容器数据挂载相关的信息。
$ sudo docker inspect nginx
[
{
## ......
"Mounts": [
{
"Type": "bind",
"Source": "/webapp/html",
"Destination": "/usr/share/nginx/html",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
## ......
}
]
在关于挂载的信息中我们可以看到一个 RW 字段,这表示挂载目录或文件的读写性 (Read and Write)。实际操作中,Docker 还支持以只读的方式挂载,通过只读方式挂载的目录和文件,只能被容器中的程序读取,但不接受容器中程序修改它们的请求。在挂载选项 -v
后再接上 :ro
就可以只读挂载了。
$ sudo docker run -d --name nginx -v /webapp/html:/usr/share/nginx/html:ro nginx:1.12
由于宿主操作系统文件挂载在权限允许的情况下能够挂载任何目录或文件,这给系统的安全性造成了一定的隐患,所以我们在使用 Bind Mount 的时候,一定要特别注意挂载的外部目录选择。适合场景:
- 当我们需要从宿主操作系统共享配置的时候。对于一些配置项,我们可以直接从容器外部挂载到容器中,这利于保证容器中的配置为我们所确认的值,也方便我们对配置进行监控。例如,遇到容器中时区不正确的时候,我们可以直接将操作系统的时区配置,也就是 /etc/timezone 这个文件挂载并覆盖容器中的时区配置。
- 当我们需要借助 Docker 进行开发的时候。虽然在 Docker 中,推崇直接将代码和配置打包进镜像,以便快速部署和快速重建。但这在开发过程中显然非常不方便,因为每次构建镜像需要耗费一定的时间,这些时间积少成多,就是对开发工作效率的严重浪费了。如果我们直接把代码挂载进入容器,那么我们每次对代码的修改都可以直接在容器外部进行。
挂载临时文件目录
Tmpfs Mount 是一种特殊的挂载方式,它主要利用内存来存储数据。由于内存不是持久性存储设备,所以其带给 Tmpfs Mount 的特征就是临时性挂载。
与挂载宿主操作系统目录或文件不同,挂载临时文件目录要通过 --tmpfs
这个选项来完成。由于内存的具体位置不需要我们来指定,这个选项里我们只需要传递挂载到容器内的目录即可。
$ sudo docker run -d --name webapp --tmpfs /webapp/cache webapp:latest
容器已挂载的临时文件目录我们也可以通过 docker inspect
命令查看。
$ sudo docker inspect webapp
[
{
## ......
"Tmpfs": {"/webapp/cache": ""},
## ......
}
]
挂载临时文件首先要注意它不是持久存储这一特性,在此基础上,它有几种常见的适应场景。
- 应用中使用到,但不需要进行持久保存的敏感数据,可以借助内存的非持久性和程序隔离性进行一定的安全保障。
- 读写速度要求较高,数据变化量大,但不需要持久保存的数据,可以借助内存的高读写速度减少操作的时间。
挂载数据卷
跟第一种方式的操作差不多,不同是目录存放在 Docker 内部,接受 Docker 的管理。你虽然可以通过 inspect 找到它的实际路径,这与第一种方式差不多,但作为数据卷比文件直接挂载也多点使用场景的优势,数据卷的命名在 Docker 中是唯一的,因此你可以公用它。
使用 -v
或 --volume
选项来定义数据卷的挂载。
$ sudo docker run -d --name webapp -v /webapp/storage webapp:latest
数据卷挂载到容器后,我们可以通过 docker inspect
看到容器中数据卷挂载的信息。
$ sudo docker inspect webapp
[
{
## ......
"Mounts": [
{
"Type": "volume",
"Name": "2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336",
"Source": "/var/lib/docker/volumes/2bbd2719b81fbe030e6f446243386d763ef25879ec82bb60c9be7ef7f3a25336/_data",
"Destination": "/webapp/storage",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
## ......
}
]
这里我们所得到的信息与绑定挂载有所区别,除了 Type 中的类型不一样之外,在数据卷挂载中,我们还要关注一下 Name 和 Source 这两个信息。
其中 Source 是 Docker 为我们分配用于挂载的宿主机目录,其位于 Docker 的资源区域 (这里是默认的 /var/lib/docker) 内。当然,我们并不需要关心这个目录,一切对它的管理都已经在 Docker 内实现了。
为了方便识别数据卷,我们可以像命名容器一样为数据卷命名,这里的 Name 就是数据卷的命名。在我们未给出数据卷命名的时候,Docker 会采用数据卷的 ID 命名数据卷。我们也可以通过 -v <name>:<container-path>
这种形式来命名数据卷。
$ sudo docker run -d --name webapp -v appdata:/webapp/storage webapp:latest
由于 -v
选项既承载了 Bind Mount 的定义,又参与了 Volume 的定义,所以其传参方式需要特别留意。前面提到了,-v
在定义绑定挂载时必须使用绝对路径,其目的主要是为了避免与数据卷挂载中命名这种形式的冲突。
虽然与绑定挂载的原理差别不大,但数据卷在许多实际场景下你会发现它很有用。
- 当希望将数据在多个容器间共享时,利用数据卷可以在保证数据持久性和完整性的前提下,完成更多自动化操作。
- 当我们希望对容器中挂载的内容进行管理时,可以直接利用数据卷自身的管理方法实现。
- 当使用远程服务器或云服务作为存储介质的时候,数据卷能够隐藏更多的细节,让整个过程变得更加简单。
共用数据卷
数据卷的另一大作用是实现容器间的目录共享,也就是通过挂载相同的数据卷,让容器之间能够同时看到并操作数据卷中的内容。这个功能虽然也可以通过绑定挂载来实现,但通过数据卷来操作会更加的舒适、简单。
由于数据卷的命名在 Docker 中是唯一的,所以我们很容易通过数据卷的名称确定数据卷,这就让我们很方便的让多个容器挂载同一个数据卷了。
$ sudo docker run -d --name webapp -v html:/webapp/html webapp:latest
$ sudo docker run -d --name nginx -v html:/usr/share/nginx/html:ro nginx:1.12
我们使用 -v
选项挂载数据卷时,如果数据卷不存在,Docker 会为我们自动创建和分配宿主操作系统的目录,而如果同名数据卷已经存在,则会直接引用。
如果有朋友觉得这样对数据卷的操作方式还不够直接和准确,我们还可以通过 docker volume
下的几个命令专门操作数据卷。
通过 docker volume create
我们可以不依赖于容器独立创建数据卷。
$ sudo docker volume create appdata
通过 docker volume ls
可以列出当前已创建的数据卷。
$ sudo docker volume ls
DRIVER VOLUME NAME
local html
local appdata
删除数据卷
虽然数据卷的目的是用来持久化存储数据的,但有时候我们也难免有删除它们以释放空间的需求。直接去 Docker 的目录下删除显然不是好的选择,我们应该通过 Docker 对数据卷的管理命令来删除它们。
我们可以直接通过 docker volume rm
来删除指定的数据卷。
$ sudo docker volume rm appdata
在删除数据卷之前,我们必须保证数据卷没有被任何容器所使用 (也就是之前引用过这个数据卷的容器都已经删除),否则 Docker 不会允许我们删除这个数据卷。
对于我们没有直接命名的数据卷,因为要反复核对数据卷 ID,这样的方式并不算特别友好。这种没有命名的数据卷,通常我们可以看成它们与对应的容器产生了绑定,因为其他容器很难使用到它们。而这种绑定关系的产生,也让我们可以在容器删除时将它们一并删除。
在 docker rm
删除容器的命令中,我们可以通过增加 -v
选项来删除容器关联的数据卷。
$ sudo docker rm -v webapp
如果我们没有随容器删除这些数据卷,Docker 在创建新的容器时也不会启用它们,即使它们与新创建容器所定义的数据卷有完全一致的特征。也就是说,此时它们已经变成了孤魂野鬼,纯粹的占用着硬盘空间而又不受管理。
此时我们可以通过 docker volume rm
来删除它们,但前提时你能在一堆乱码般的数据卷 ID 中找出哪个是没有被容器引用的数据卷。
为此,Docker 向我们提供了 docker volume prune
这个命令,它可以删除那些没有被容器引用的数据卷。
$ sudo docker volume prune -f
Deleted Volumes:
af6459286b5ce42bb5f205d0d323ac11ce8b8d9df4c65909ddc2feea7c3d1d53
0783665df434533f6b53afe3d9decfa791929570913c7aff10f302c17ed1a389
65b822e27d0be93d149304afb1515f8111344da9ea18adc3b3a34bddd2b243c7
## ......
数据卷容器
在数据卷的基础上,我们有一种相对新颖的用法,也就是数据卷容器。所谓数据卷容器,就是一个没有具体指定的应用,甚至不需要运行的容器,我们使用它的目的,是为了定义一个或多个数据卷并持有它们的引用。
创建数据卷容器的方式很简单,由于不需要容器本身运行,因而我们找个简单的系统镜像都可以完成创建。
$ sudo docker create --name appdata -v /webapp/storage ubuntu
在使用数据卷容器时,我们不建议再定义数据卷的名称,因为我们可以通过对数据卷容器的引用来完成数据卷的引用。而不设置数据卷的名称,也避免了在同一 Docker 中数据卷重名的尴尬。
之前我们提到,Docker 的 Network 是容器间的网络桥梁,如果做类比,数据卷容器就可以算是容器间的文件系统桥梁。我们可以像加入网络一样引用数据卷容器,只需要在创建新容器时使用专门的 --volumes-from
选项即可。
$ sudo docker run -d --name webapp --volumes-from appdata webapp:latest
引用数据卷容器时,不需要再定义数据卷挂载到容器中的位置,Docker 会以数据卷容器中的挂载定义将数据卷挂载到引用的容器中。
虽然看上去数据卷容器与数据卷的使用方法变化不大,但最关键的就在于其真正隐藏了数据卷的配置和定义,我们只需要通过数据卷容器的名称来使用它。这些细节的隐藏,意味着我们能够更轻松的实现容器的迁移。
备份和迁移数据卷
由于数据卷本身就是宿主操作系统中的一个目录,我们只需要在 Docker 资源目录里找到它就可以很轻松的打包、迁移、恢复了。虽然这么做相对其他虚拟化方案来说已经很简单了,但在 Docker 里还不是最优雅的解决方式。
利用数据卷容器,我们还能够更方便的对数据卷中的数据进行迁移。
数据备份、迁移、恢复的过程可以理解为对数据进行打包,移动到其他位置,在需要的地方解压的过程。在数据打包之前,我们先建立一个用来存放打包文件的目录,这里我们使用 /backup
作为例子。
要备份数据,我们先建立一个临时的容器,将用于备份的目录和要备份的数据卷都挂载到这个容器上。
$ sudo docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar cvf /backup/backup.tar /webapp/storage
在这条命令中,除了挂载的配置外,我们再注意几个选项。通过 --rm
选项,我们可以让容器在停止后自动删除,而不需要我们再使用容器删除命令来删除它,这对于我们使用一些临时容器很有帮助。在容器所基于的镜像之后,我们还看到了一串命令,也就是 tar cvf /backup/backup.tar /webapp/storage
,其实如果我们在镜像定义之后接上命令,可以直接替换掉镜像所定义的主程序启动命令,而去执行这一条命令。在很多场合下,我们还能通过这个方法干很多不同的事情。
在备份后,我们就可以在 /backup 下找到数据卷的备份文件,也就是 backup.tar 了。
如果要恢复数据卷中的数据,我们也可以借助临时容器完成。
$ docker run --rm --volumes-from appdata -v /backup:/backup ubuntu tar xvf /backup/backup.tar -C /webapp/storage --strip
恢复的过程与备份的过程类似,只不过把打包的命令转换为解包的命令而已。
另一个挂载选项
上面我们讲到了使用 -v
选项来挂载存在容易混淆的问题,其主要原因是挂载的方式和配置随着 Docker 的不断发展日渐丰富,而 -v
选项的传参方式限制了它能使用的场景。
其实在 Docker 里为我们提供了一个相对支持丰富的挂载方式,也就是通过 --mount
这个选项配置挂载。
$ sudo docker run -d --name webapp webapp:latest --mount 'type=volume,src=appdata,dst=/webapp/storage,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>' webapp:latest
在 --mount
中,我们可以通过逗号分隔这种 CSV 格式来定义多个参数。其中,通过 type 我们可以定义挂载类型,其值可以是:bind,volume 或 tmpfs。另外,--mount
选项能够帮助我们实现集群挂载的定义,例如在这个例子中,我们挂载的来源是一个 NFS 目录。
- 虚拟化优点
- 虚拟化的分类
- 虚拟机
- 容器技术
- Docker 开源地址(改名)
- Docker 的技术实现
- Docker 的理念
- Docker 核心
- 镜像
- 容器
- 网络
- 数据卷
- Docker 软件
- Docker Engine
- docker daemon 和 docker CLI
- CentOS
- Fedora
- Ubuntu
- Docker
- 操作命令
- 查看镜像
- 拉镜像
- Docker Hub
- 搜索镜像
- 管理镜像信息
- 删除镜像
- 创建容器
- 启动容器
- 创建与启动
- 启动参数说明
- 交互式
- 守护式
- 目录挂载
- 映射端口
- 管理容器
- 停止和删除容器
- 容器互联
- 暴露端口
- 管理网络
- 创建网络
- 在 Windows 和 macOS 中使用映射
- 备份
- 恢复
- 批量迁移
- 直接导入导出
- 挂载方式
- 挂载文件到容器
- 挂载临时文件目录
- 挂载数据卷
- 共用数据卷
- 删除数据卷
- 数据卷容器
- 备份和迁移数据卷
- 另一个挂载选项