转载:原文地址docker-朱双印博客 (zsythink.net)
一个容器运行了一段时间,肯定会产生一些数据,比如日志、数据库数据、新改的配置文件、等等,如果这些数据文件存放在容器中,当我们删除容器时,这些数据也会被随之删除(因为这些数据存放在容器可读写层中,所以会随容器被删除),这可能并不是我们想要的结果,通常情况下,我们想要持久的保存这些数据,以便可以随时使用它们,那么,怎样做才能在删除容器时,仍然保留这些数据呢?在docker中,有一种存储数据的方法,叫做“数据卷”(后文简称卷,卷的英文为volume),借助“卷”,即可达到我们的目的。
那么什么是卷呢?
我们可以把“卷”理解成“宿主机中的目录”,当我们把某个卷和容器中的某个目录建立映射关系后,就相当于把宿主机中的某个目录和容器中的某个目录建立了映射关系。
举个例子:
假设现在有一个名为testA的卷,我们把testA卷和容器A中的/data目录进行了映射,映射后,运行容器A,容器A中的程序将日志写入到了容器的/data/A.log文件中,由于testA卷与容器A的/data目录是映射过的,所以我们可以在testA卷中,看到一个名为A.log的日志文件,testA卷中的A.log其实就是容器A中的/data/A.log,当我们删除容器A以后,testA卷中的A.log仍然存在于宿主机中,并不会随容器A被删除,也就是说,卷中的数据是独立于容器的生命周期之外的,卷能让我们将容器中的数据保存到宿主机上,从而确保我们即使删除了容器,对应的数据也能保存下来。
了解了什么是卷以后,就可以来实际动手操作一下了。
我们先来创建一个卷,使用如下命令,创建一个名为testA的卷
[root@cos7-1 ~]# docker volume create testA
testA
如你所见,docker volume命令就是专门用来管理卷的命令,上例中的create就是docker volume的子命令,docker volume create表示创建一个卷,执行上述命令后,使用ls子命令,可以查看当前主机中的卷,如下:
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
local testA
可以看到,在本地已经有一个名为testA的卷了,我们可以使用inspect子命令,查看对应卷的详细信息。
[root@cos7-1 ~]# docker volume inspect testA
[
{
"CreatedAt": "2022-05-07T00:05:37+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/testA/_data",
"Name": "testA",
"Options": {},
"Scope": "local"
}
上文说过,卷其实就是宿主机中的某个目录,从上述详细信息中可以看出,testA卷其实就是宿主机中的/var/lib/docker/volumes/testA/_data目录,在linux的docker主机中创建一个卷时,其对应的目录路径就是/var/lib/docker/volumes/卷名/_data
testA卷已经创建完成,但是没有任何容器使用这个卷,现在,我们来创建一个测试容器,让这个测试容器使用testA卷,创建容器的命令如下:
docker run -td --name testAcon --volume testA:/data alpine
上述命令表示创建一个名为testAcon的容器,将testA卷映射到testAcon容器的/data目录中,--volume选项就是用来映射卷和容器目录的,--volume testA:/data表示将testA卷映射到容器的/data目录,上例是基于alpine镜像创建的容器,alipine镜像中默认是没有/data目录的,当卷映射的目录在容器中不存在时,会自动在容器中创建对应的目录。
容器创建后,进入容器的/data目录,创建一个名为A.log的文件,如下
[root@cos7-1 ~]# docker exec -it testAcon sh
/ # ls /data
/ # cd /data
/data # echo "test volume A" > /data/A.log
/data # cat /data/A.log
test volume A
/data # exit
[root@cos7-1 ~]#
回到宿主机,查看/var/lib/docker/volumes/testA/_data目录,发现多了一个A.log文件,这个文件正是我们在容器中创建的A.log
[root@cos7-1 ~]# ls /var/lib/docker/volumes/testA/_data/
A.log
[root@cos7-1 ~]# cat /var/lib/docker/volumes/testA/_data/A.log
test volume A
如果此时我们在宿主机中修改A.log文件的内容,会发现容器中的A.log的内容也随之变化了,因为它们就是同一个文件。
现在,我们停止并删除testAcon容器,可以发现,testA卷中的数据仍然保留在宿主机中。
[root@cos7-1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
75c09d483a20 alpine "/bin/sh" 8 minutes ago Up 8 minutes testAcon
[root@cos7-1 ~]# docker stop testAcon
testAcon
[root@cos7-1 ~]# docker rm testAcon
testAcon
[root@cos7-1 ~]# ls /var/lib/docker/volumes/testA/_data/
A.log
这就是我们要的效果,容器中产生的数据不随容器被删除,仍然保留下来,以便随时可以继续使用,比如,我再创建一个新的容器,仍然使用这个卷中的数据
[root@cos7-1 ~]# docker run -td --name testBcon --volume testA:/data alpine
9b6d44174554066de29c5bc1399d757bf678d8bc4bf615cad48d70ce0b843fe9
[root@cos7-1 ~]#
[root@cos7-1 ~]# docker exec -it testBcon sh
/ #
/ # ls /data
A.log
/ # cat /data/A.log
test volume A
/ # exit
[root@cos7-1 ~]#
可以看到,testA卷中的数据可以直接在testBcon中访问到。
当把卷映射到容器目录时,可以理解为把卷对应的宿主机目录挂载到了容器的目录中,卷虽然可以映射到容器的目录中,但其实是独立于容器之外的,当我们使用docker commit命令基于容器A创建镜像时,如果容器A使用了某个卷,卷中的数据并不会随容器A被提交,即新创建的镜像中并不会包含容器A对应卷中的数据。
--volume选项是长选项,它有一个对应的短选项-v,它们两个是等效的,即如下两个命令等效
docker run -td --name testCon --volume testA:/data alpine
docker run -td --name testCon -v testA:/data alpine
一个容器可以使用多个卷,只需要多次使用-v选项指定即可
[root@cos7-1 ~]# docker run -td --name testCcon -v testA:/data -v testB:/var/log alpine
d8d7470c5d86cd42fb8474d94fc39cc8a8ed2defba327835fcf2fa7c2cd5f690
执行上述命令后会发现,我并没有提前使用docker volume create命令创建testB卷,而是直接在docker run命令中指定了名为testB的卷,docker run命令可以正常执行,并没有报错,此时,我们查看一下卷的信息,会发现testB卷被自动创建了。
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
local testA
local testB
可见,当指定的卷不存在时,docker会自动创建对应的卷。
一个卷可以同时被多个容器使用,从而达到数据共享的目的,就像上文的testBcon和testCcon两个容器同时使用了testA卷一样。
在使用--volume选项或者-v选项时,可以省略卷名,只指定一个容器中的目录,当省略卷名时,docker会自动创建一个新的卷,并为这个卷生成一个ID,如下
[root@cos7-1 ~]# docker run -td --name testDcon -v /data1 alpine
60afd20098ed4cb7631ee6eaff5bf4cc9fb5fe8e623c044d95075b637f5d6c9d
[root@cos7-1 ~]#
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
local 86bb1cfa654bbeeefc766131de443ebafbadbccb1e789c4e4cd160698c3bead3
local testA
local testB
上例中,创建testDcon容器时,并没有指定卷名,而是只指定了一个/data1目录,这种情况下,docker自动创建了一个卷,这个卷的名字是一串很长的生成的ID,上例生成的卷ID为86bb1cfa654bbeeefc766131de443ebafbadbccb1e789c4e4cd160698c3bead3,这种用docker生成的ID作为卷名的卷被称之为“匿名卷”,匿名卷的英文原文为Anonymous volume,而上文中的testA卷和testB卷,是我们自定义名称的卷,这种卷被称之为“命名卷”,命名卷的英文原文为Named volumes,上例中的匿名卷对应的宿主机目录也是在/var/lib/docker/volumes/卷ID/_data下,无论是命名卷还是匿名卷,都是被docker管理的,即使是匿名卷,我们也可以使用它的ID作为卷名,从而映射到其他容器的某个目录中,例如
-v 86bb1cfa654bbeeefc766131de443ebafbadbccb1e789c4e4cd160698c3bead3:/data2
上例表示将匿名卷86bb1c映射到容器的/data2目录中。
如果确定卷中的数据不需要再使用了,则可以删除对应的卷。
使用docker volume rm命令即可删除对应的卷,但是删除卷之前,需要先确保卷没有被任何容器引用,如果有容器使用了对应的卷,会提示无法删除,需要先删除对应的容器,示例如下
#查看当前卷,有三个,两个命名卷,一个匿名卷,此处以删除匿名卷为例,命名卷操作也是相同的
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
local 86bb1cfa654bbeeefc766131de443ebafbadbccb1e789c4e4cd160698c3bead3
local testA
local testB
#尝试删除匿名卷,出现提示,卷正在被容器60afd2...使用,这个ID就是testDcon容器的ID
[root@cos7-1 ~]# docker volume rm 86bb1cfa654bbeeefc766131de443ebafbadbccb1e789c4e4cd160698c3bead3
Error response from daemon: remove 86bb1cfa654bbeeefc766131de443ebafbadbccb1e789c4e4cd160698c3bead3: volume is in use - [60afd20098ed4cb7631ee6eaff5bf4cc9fb5fe8e623c044d95075b637f5d6c9d]
#先停止容器并删除容器
[root@cos7-1 ~]# docker stop testDcon
testDcon
[root@cos7-1 ~]# docker rm testDcon
testDcon
#再次删除卷,已经正常删除了
[root@cos7-1 ~]# docker volume rm 86bb1cfa654bbeeefc766131de443ebafbadbccb1e789c4e4cd160698c3bead3
86bb1cfa654bbeeefc766131de443ebafbadbccb1e789c4e4cd160698c3bead3
[root@cos7-1 ~]#
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
local testA
local testB
可以看到,删除卷的前提是,没有引用对应卷的容器存在。
所以,在删除卷或者删除容器时,你可能会有下面两种需求
一,查看某个容器都使用了哪些卷
二,查看某个卷都被哪些容器使用了
通过如下两条命令,可以满足我们的需求
#通过下例查看testCcon容器都使用了哪些卷
#如下命令其实就是过滤出容器的详细信息中的Mounts段
#如下示例的testCcon容器使用了两个卷,testA和testB
[root@cos7-1 ~]# docker inspect testCcon -f '{{.Mounts}}'
[{volume testB /var/lib/docker/volumes/testB/_data /var/log local z true } {volume testA /var/lib/docker/volumes/testA/_data /data local z true }]
#通过下例查看testA卷被哪些容器使用了
#如下示例中testA卷被testBcon和testCcon两个容器使用了
[root@cos7-1 ~]# docker ps -a -f "volume=testA"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9b6d44174554 alpine "/bin/sh" 21 hours ago Exited (137) 13 hours ago testBcon
d8d7470c5d86 alpine "/bin/sh" 24 hours ago Exited (137) 13 hours ago testCcon
除了单独删除某个卷,我们还可以批量删除卷,使用docker volume prune命令,可以批量删除所有的没有被任何容器使用的卷,由于我的测试环境的testA卷和testB卷都有容器在使用,所以使用docker volume prune命令并不能删除它们,为了展示效果,我先提前删除对应的容器
#先删除对应的容器
[root@cos7-1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9b6d44174554 alpine "/bin/sh" 24 hours ago Exited (137) 16 hours ago testBcon
d8d7470c5d86 alpine "/bin/sh" 27 hours ago Exited (137) 16 hours ago testCcon
[root@cos7-1 ~]# docker rm testBcon
testBcon
[root@cos7-1 ~]# docker rm testCcon
testCcon
#查看现有卷
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
local testA
local testB
#执行批量删除命令,会出现确认提示,输入y确认删除所有的没有被任何容器引用的卷
[root@cos7-1 ~]# docker volume prune
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
testA
testB
Total reclaimed space: 14B
#删除完成后,再次查看卷,已经删除
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
匿名卷和命名卷相比,多了一个自动删除的特性,当匿名卷与docker run命令的--rm选项配合使用时,如果容器被停止删除,匿名卷也会随之被自动删除,如果想要搞明白匿名卷自动删除的特性,最好先搞明白docker run命令的--rm选项的作用,--rm选项表示当容器停止时,自动删除对应的容器,示例如下:
#创建一个testAcon容器,使用--rm选项,表示当testAcon容器停止时,会被自动删除
[root@cos7-1 ~]# docker run --rm -td --name testAcon alpine
21f6be3ab1fcf96c72586872cc2faccfbdb5fc1e459d7a3128b04ca743f01449
#查看容器已经正常运行
[root@cos7-1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
21f6be3ab1fc alpine "/bin/sh" 6 seconds ago Up 5 seconds testAcon
#停止testAcon容器
[root@cos7-1 ~]# docker stop testAcon
testAcon
#再次查看容器,testAcon容器已经被自动删除
[root@cos7-1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
如上所示,如果创建容器的时候使用了--rm选项,当容器停止时,会被自动删除,当我们频繁的进行测试实验时,--rm选项非常好用,我们可以多次重复进行实验,避免实验过程中不断的生成多余的容器。
如果创建容器时使用了--rm选项,并且同时使用了匿名卷和命名卷,会发现,当容器停止时,匿名卷会随容器被自动删除,而命名卷不会,实验过程如下
#查看容器,查看卷,目前没有任何容器和卷
[root@cos7-1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
#创建容器时,使用--rm选项,同时使用匿名卷和命名卷
[root@cos7-1 ~]# docker run --rm -td --name testAcon -v /data1 -v testA:/data2 alpine
dc7b0414fc411d7e9ad6770767196927c7d117cad66d2c2718e911de832bd1f3
#再次查看卷和容器
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
local e598d33c3a568d4d5e7de8b932c01d750c92c1ceb071d40d183a555742acd16f
local testA
[root@cos7-1 ~]#
[root@cos7-1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dc7b0414fc41 alpine "/bin/sh" 16 seconds ago Up 16 seconds testAcon
#停止容器
[root@cos7-1 ~]# docker stop testAcon
testAcon
#发现容器和匿名卷都自动删除了,但是命名卷仍然保留了下来
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
local testA
[root@cos7-1 ~]#
[root@cos7-1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
无论是命名卷还是匿名卷,它们都存放在/var/lib/docker/volumes目录中,这个目录是固定的,它们都能被docker volume命令管理,除了命名卷和匿名卷,还有一种映射宿主机目录的方法,这种方法被称之为”绑定挂载”,绑定挂载的英文原文为bind mounts。
绑定挂载能够将指定的宿主机目录挂载到容器中,绑定挂载的使用方法也很简单,仍然是使用--volume选项或者-v选项,我们只需要将卷名替换成宿主机上的目录路径即可,示例如下:
#宿主机上的/root/test1目录如下
[root@cos7-1 ~]# ls /root/test1
a b
#创建一个容器,将宿主机的/root/test1目录映射到容器的/data1目录中,在进行实验时,可以将/root/test1换成任意其他目录。
[root@cos7-1 ~]# docker run --rm -td --name testAcon -v /root/test1:/data1 alpine
9ccbb91679dd92909817d0de851fe9074345fb668787838ec2304fce8047f735
#进入容器,查看/data1目录,与宿主机中的内容相同,退出容器
[root@cos7-1 ~]# docker exec -it testAcon sh
/ #
/ # ls /data1
a b
/ # exit
绑定挂载不会生成任何卷,它直接将指定的宿主机目录映射到容器中,所以,docker volume命令无法查看或管理到绑定挂载的路径。官方建议使用卷,而不是绑定挂载,但是,绑定挂载有一个优势,就是绑定挂载可以直接将宿主机中的文件(非目录)直接挂载到容器中,比如,将宿主机中的/etc/localtime文件映射到容器中的/etc/localtime文件
docker run --rm -td --name testAcon -v /etc/localtime:/etc/localtime alpine
通常,使用绑定挂载就是为了将宿主机中的配置文件挂载到容器中,如果是整个目录的数据,建议使用卷,卷只能映射目录,不能映射文件。
即使通过绑定挂载将宿主机的单个文件挂载到容器中,基于容器通过docker commit创建出新的镜像后,新镜像中也不会包含对应文件的数据的。
我们可以做一个实验,让A容器使用绑定挂载,把宿主机的a.log挂载到容器的/opt/a.log(注意:实验时请使用有内容的文件,即确保a.log非空),然后基于A容器创建b镜像,最后基于b镜像创建B容器,进入B容器后,会发现/opt/a.log是存在的,但是,B容器中的/opt/a.log是一个空文件,没有任何内容,也就是说,源A容器中绑定挂载的a.log的内容并没有通过docker commit命令提交过来,这是为什么呢?之前说过,卷中的数据是独立于容器的,绑定挂载也一样,它们都是宿主机中的数据,当我们把宿主机的a.log挂载到容器的/opt/a.log时,容器会创建'/opt/a.log路径'作为宿主机文件的挂载点,而'/opt/a.log挂载点路径'是在容器A的可读写层中创建的,所以,当基于容器A创建新镜像时,挂载点路径(空文件)也被提交到了新镜像中,但是真正的宿主机文件(数据)并没有随之提交,这就是出现上述现象的原因,具体过程这里就不演示了,快动手试一下吧。
在使用卷或者绑定挂载进行实验时,你可能会纠结一个问题,就是数据覆盖问题,在映射之前,卷和宿主机目录可能为空或者非空,容器目录可能为空或者非空,那么在映射后,到底以哪里的数据为准呢?我总结了一张表,表中写明了映射前后的数据关系(有些容器目录比较特殊,比如/etc,因为/etc目录中有挂载的hosts文件等)。
上文中,无论是使用命名卷、匿名卷还是绑定挂载,都在使用--volume选项或者-v选项,其实,除了这两个选项,还有另一个选项,也能实现同样的效果,这个选项就是--mount,--mount选项的使用示例如下
#创建容器test1,使用命名卷testA,映射到容器的/data目录
docker run -td --name test1 --mount type=volume,source=testA,target=/data alpine
#创建容器test2,使用匿名卷,映射到容器的/data目录
docker run -td --name test2 --mount type=volume,target=/data alpine
#创建容器test3,使用绑定挂载,将宿主机的/root/test1目录挂载到/data目录
docker run -td --name test3 --mount type=bind,source=/root/test1/,target=/data alpine
可见,--mount选项的写法似乎更加清晰,在--mount选项中,type用于指定挂载类型,上例中的type=volume表示使用卷,type=bind表示使用绑定挂载,无论是命名卷还是匿名卷,都使用type=volume,当使用卷时,如果使用的是命名卷,就使用source参数指定对应的卷名,如果使用匿名卷,则省略source参数,target参数用于指定将卷或者宿主机目录挂载到容器的哪个目录上,如果使用绑定挂载,source参数对应宿主机上的目录。
上例中,source参数可以写成src,target参数可以写成dst或者destination,即如下两条命令是等效的。
docker run --rm -td --name test4 --mount type=volume,src=testB,dst=/data alpine
docker run --rm -td --name test4 --mount type=volume,source=testB,target=/data alpine
除了上文聊到的-v选项、--mount选项,还有一个选项我们可能也会用到,它就是--volumes-from选项,--volumes-from选项可以让一个容器去使用另一个容器的卷,比如:
容器A使用了两个卷,当我们创建容器B时,明确告诉容器B:“容器A用了哪些卷,你就用哪些卷”,就好像让B去抄A的作业一样,示例如下
#首先,查看一下当前卷列表,没有任何卷
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
#创建一个test1容器,如下,test1容器使用了一个匿名卷,一个testA卷,一个绑定挂载的宿主机目录
[root@cos7-1 ~]# docker run -td --name test1 -v /data -v testA:/data1 -v /root/test1:/data2 alpine
c1fd78c3ef6ac1e0544646deaf65aed24d8aef47d849e5199c954b6a9308dc52
#再次查看卷列表,匿名卷和testA卷已经自动创建了
[root@cos7-1 ~]# docker volume ls
DRIVER VOLUME NAME
local 7f58548a996cb64f17c0a1e4679e9cf88c6fc87751623f090280f9651b85db81
local testA
#此时,创建一个test2容器,下例命令中的--volumes-from test1表示让test2照抄test1的卷(以及绑定挂载)
[root@cos7-1 ~]# docker run -td --name test2 --volumes-from test1 alpine
d7ac879750abb71480cfe630c379466db5d2c713b80b39a2451d0493ec814c49
#查看test1容器和test2容器的挂载列表,发现它们挂载的卷(以及宿主机目录)都是相同的,对应的容器中的目录也是相同的。
[root@cos7-1 ~]# docker inspect test1 -f '{{.Mounts}}'
[{volume testA /var/lib/docker/volumes/testA/_data /data1 local z true } {bind /root/test1 /data2 true rprivate} {volume 7f58548a996cb64f17c0a1e4679e9cf88c6fc87751623f090280f9651b85db81 /var/lib/docker/volumes/7f58548a996cb64f17c0a1e4679e9cf88c6fc87751623f090280f9651b85db81/_data /data local true }]
[root@cos7-1 ~]#
[root@cos7-1 ~]# docker inspect test2 -f '{{.Mounts}}'
[{volume 7f58548a996cb64f17c0a1e4679e9cf88c6fc87751623f090280f9651b85db81 /var/lib/docker/volumes/7f58548a996cb64f17c0a1e4679e9cf88c6fc87751623f090280f9651b85db81/_data /data local true } {volume testA /var/lib/docker/volumes/testA/_data /data1 local true } {bind /root/test1 /data2 true rprivate}]
[root@cos7-1 ~]#
--volumes-from选项也可以多次使用,从而指定多个容器,比如
--volumes-from test3 --volumes-from test5
这篇文章中总结了数据卷相关的常用选项,卷还有一些其他内容,文章中没有总结,这些内容我们可以参考官网文档的volumes章节,地址如下
https://docs.docker.com/storage/volumes
转载:原文地址docker-朱双印博客 (zsythink.net)