声明:所有的实验示例大部分来自《learn-docker-in-a-month-of-lunches》的作者Elton Stoneman,但运行结果并不都是照搬,大部分实验结果可能与原书不同
一、前言
到目前为止,你应该已经在容器上发现了这么一个特点:你每次运行一个新的容器,容器的内的任何文件都回归初始了,你乍一听会觉得这不是挺好的嘛,这样会保证每一个新运行的容器不论在何时不论在何地都从同一个状态开始运行。但你有没有想过这么一种情况,你的容器存储了一些重要信息在容器内的文件系统里,但是这时因为软件升级,你的镜像也更新了,你必须以新的镜像去运行新的容器,也就是说你得关停老的容器,那么这些重要数据岂不是丢失了?
不必担心,强大的Docker早就想到了这一点,它们给出了Docker Volumn和Docker Bind两种文件共享方式,以适应你数据持久化的需求。
不过在此之前,我们先来讨论一下Docker容器的文件系统结构。
二、Docker容器的文件系统结构
Docker会将所有的文件系统源合并在一起呈现给Docker容器,而Docker容器能看到只是一个普普通通的块存储设备,Docker称这样的系统为union filesystem,下图是union filesystem的一个基本组成结构:
- 最底下的是镜像层文件系统,文件来源是(或者说是镜像)Dockerfile中生成、复制或者其他操作产生的文件,这些文件是只读的,但是因为使用了copy-on-write技术的原因,这些文件某种意义上其实也是可以变更的
- Docker Volume是Docker提供的用于容器与容器之间共享数据的文件系统。来自Docker生成
- Docker Bind是Docker提供的挂载型文件系统,用于交互主机(甚至是云文件系统)与容器的文件系统,文件来源于挂载源
- 可写层是每个容器都独有的文件系统,这些文件仅在容器存在时存在,文件来源于容器本身
三、Docker 镜像层文件系统与可写层文件系统
- 概念:
镜像层文件系统顾名思义来自于镜像,镜像是用来分享的,且必须保持一致,所以镜像层文件系统是只读的。但是实际应用中,我们发现镜像中原本就存在的文件其实是可以修改的,这是因为在镜像层文件系统上还有一层可写层文件系统。
可写层文件系统由Docker在容器运行时特地为容器创建,在容器移除时移除。可写层不仅仅只应用于新添加的文件,其实也应用于修改镜像层存在的文件,在我们尝试修改镜像层存在的文件时,Docker其实会把该文件复制一份到可写层上,你所有的修改操作实际上全部都操作在可写层的文件上,所以对镜像层文件不会有任何的影响。这种技术称之为copy-on-write。 - 应用领域:
镜像层文件系统应用于镜像分享,因为它的只读属性这保证了镜像的一致性。
可写层文件系统应用于短期存储,比如缓存一些数据之类的。
四、Docker Volume容器共享文件系统
概念:
Docker Volume就相当于一个U盘,只不过这个U盘由Docker创建,且服务于容器,Docker Volume是一个独立的组件,和容器的生命周期互不相关,就像你电脑关机了,但是U盘还在一样。以容器的视角来看Docker Volume,它其实就只是一个普通的目录,像平常一样操作这个目录就可以了。-
使用方法(两种):
- 在Dockerfile中指定
VOLUME /data
在Dockerfile中添加
Volume
指令,告诉Docker在创建这个镜像的容器时,创建一个Docker Volume并挂载在/data
目录下,需要注意的是,这样运行的每个容器其Docker Volume都是不同的$ docker container run --name todo-list --detach --publish 80:80 diamol/ch06-todo-list 5b81d8fe9953224a9c768ebbceead8799474cc204ce5df294098e92a5ac7a4e8 $ docker volume ls DRIVER VOLUME NAME local f07bea1de6730f9aa91e2f631249c89eb8126d5827383fa8bfbb43ebdd4cc3fe
diamol/ch06-todo-list该镜像的Dockerfile中使用过
volume
指令,运行该镜像后查看Docker中存在的Volume确实发现生成了一个名为f07bea1de6730f9aa91e2f631249c89eb8126d5827383fa8bfbb43ebdd4cc3fe的Docker Volume
那另外一个容器想使用这个‘随机’生成的Volume怎么办呢?答案是在运行容器时加上volumes-from <容器名>
这个选项$ docker container run --name todo-list2 --volumes-from todo-list --detach --publish 80:80 diamol/ch06-todo-list
运行另一个容器,并指定todo-list的volume为自己的volume
$ docker volume ls DRIVER VOLUME NAME local f07bea1de6730f9aa91e2f631249c89eb8126d5827383fa8bfbb43ebdd4cc3fe $ docker container inspect --format '{{.Mounts}}' todo-list2 [{volume f07bea1de6730f9aa91e2f631249c89eb8126d5827383fa8bfbb43ebdd4cc3fe /var/lib/docker/volumes/f07bea1de6730f9aa91e2f631249c89eb8126d5827383fa8bfbb43ebdd4cc3fe/_data /data local true }]
检查Docker Volume的个数,只有一个!检查todo-list2的挂载信息,挂载的Volume是f07bea1de6730f9aa91e2f631249c89eb8126d5827383fa8bfbb43ebdd4cc3fe,的确是todo-list的那个volume
- 手动创建Volume并添加给指定容器
$ docker volume create todo-list-volume todo-list-volume $ docker volume ls DRIVER VOLUME NAME local todo-list-volume
创建一个名为todo-list-volume的Docker Volume
注意:执行下一步之前,请确保diamol/ch06-todo-list镜像的所有容器已经移除$ docker container run --name todo-list1 --volume todo-list-volume:'/data' --detach --publish 5000:80 diamol/ch06-todo-list
将名为todo-list-volume的Docker Volume挂载给容器todo-list1,接下来让我打开浏览器去其中添加一些数据吧
$ docker container rm --force todo-list1 $ docker container run --name todo-list2 --volume todo-list-volume:'/data' --detach --publish 5000:80 diamol/ch06-todo-list
移除上一个容器,重新创建一个容器并挂载todo-list-volume,去浏览器查看结果,正确的话,你会发现新的容器会含有上一个容器留存的数据,如果没有,你得检查
:'/data'
有没有加上,它会告诉Docker这个Volume挂载在什么目录上 -
注意事项:
- Docker Volume可以同时挂载给多个容器,但同时你得考虑到这么一个问题,那就是多个容器同时对一个目录下的资源进行操作,你的应用可能工作得并不如你得预期(因为你要的数据可能正好被另一个容器中的应用修改了)
- Docker Volume中Volume的优先级以命令中指定的Volume为最优先,如果同时Dockerfile也指定了Volume,则该指令无效。
- 作为镜像创作者,你应该在Dockerfile中指定Volume,以避免用户不指定Volume而造成的各种问题。作为用户,你应该避免使用镜像制作者为你提供的默认Volume。
应用领域:鉴于上述注意事项第一条的原因,Docker Volume应避免被同时共享给多个容器,所以,其最主要应用于数据迁移这一方面。
五、Docker Bind容器挂载文件系统
- 概念:
Docker Bind允许你将任意文件系统(主机的文件系统,云文件系统)挂载给容器 - 使用方法:
$ mkdir database
$ source="$(pwd)/database"
$ docker container run --mount type=bind,source=$source,target='/data' --detach --publish 5000:80 diamol/ch06-todo-list
$ curl localhost:5000
$ ll database
-rw-r--r--. 1 root root 12288 Jan 21 18:19 todo-list.db
$ docker volume ls
DRIVER VOLUME NAME
在当前目录下创建一个database目录,然后将该目录挂载给新启动的容器,然后访问这个应用,查看database目录下的文件,你会发现多了一个数据库文件。
然后再查看Docker Volume(运行前已经清空)一个都没有,这说明Docker Bind的设置将默认的Docker Volume覆盖了
这时候你会问,那如果同时在命令里面将Volume和其他文件系统挂载给同一个目录呢?
当然会报错啦!docker: Error response from daemon: Duplicate mount point: /data.
那如果Volume挂载给/data
目录,而其他文件系统挂载给/data/config
目录呢?
经测试,是可以的
- 注意事项:
- 使用Docker Bind毕竟会和实实在在的文件系统相关联,所以必须要注意不法分子通过该渠道经由容器控制你的主机。
- 为了解决上述问题,你应该提供最小的权限给(在Dockerfile中使用
USER
指令)容器,但是这样就不能自由的在挂载的文件系统上读写了,因此你必须提升该容器的权限,比如本例中的todo-list
使用的就是root权限 - 使用高权限毕竟很不安全,针对某些必定是只读的目录或者文件,你可以在容器运行时指定该挂载具有只读属性,来限制容器在这个目录或文件上进行修改
$ docker container run --mount type=bind,source=$source,target='/data',readonly --detach --publish 5000:80 diamol/ch06-todo-list
- 文件系统类型有很多种,并不是所有类型你的容器都可以兼容的,所以在挂载时务必确保挂载的文件系统是否兼容,否则将会导致你的应用崩溃
- 应用领域:
- 由于挂载并不仅仅局限于本地机器,所以你可以应用Docker Bind来实现容器和远端存储之间的数据共享。
- 你在本地被挂载的目录中做的任何更改都将立即反映到容器之中。比如你可以将源代码的目录挂载给容器。
六、其他说明
- 假如你将一个目录挂载在容器中已经存在的目录上,则原目录会被隐藏
- 如果你单单只将一个文件挂载给容器中已经存在的目录上,则该文件将加入该目录,但要注意这个特性目前在Windows的容器中并不支持
参考文档:
[1] learn-docker-in-a-month-of-lunches
[2] 官方文档
附:
[1] Elton Stoneman的github项目