5. Docker数据卷

前面介绍了Docker的镜像是由一系列的只读层组合而来的,当启动一个容器时,Docker加载镜像的所有只读层,并在最上层加入一个读写层。 这个设计使得Docker可以提高镜像构建、存储和分发的效率,节省了时间和存储空间,然而也存在如下问题。

  • 容器中的文件在宿主机上存在形式复杂,不能在宿主机上很方便地对容器中的文件进行访问。

  • 多个容器之间的数据无法共享。

  • 当删除容器时,容器产生的数据将丢失。

为了解决这些问题,Docker引入了数据卷(volume)机制。 volume 是存在于一个或多个容器中的特定文件或文件夹,这个目录以独立于联合文件系统的形式在宿主机中存在,并为数据的共享与持久化提供以下便利。

  • volume在容器创建时就会初始化,在容器运行时就可以使用其中的文件。

  • volume能在不同的容器之间共享和重用。

  • 对volume中数据的操作会马上生效。

  • 对volume中数据的操作不会影响到镜像本身。

  • volume的生存周期独立于容器的生存周期,即使删除容器,volume仍然会存在,没有任何容器使用的volume也不会被Docker删除。

Docker提供了volumedriver接口,通过实现该接口,我们可以为Docker容器提供不同的volume存储支持。当前官方默认实现了local这种volumedriver,它使用宿主机的文件系统为Docker容器提供volume。

5.1. 数据卷的使用方式

为容器添加volume,类似于Linux的mount操作,用户将一个文件夹作为volume挂载到容器上,可以很方便地将数据添加到容器中供其中的进程使用。 多个容器可以共享同一个volume,为不同容器之间的数据共享提供了便利。

5.1.1. 创建volume

Docker1.9版本引入了新的子命令,即docker volume。用户可以使用这个命令对volume进行创建、查看和删除,与此同时,传统的-v参数创建volume的方式也得到了保留。

用户可以使用docker volume create创建一个volume,以下命令创建了一个指定名字的volume。

$ [root@demo ~]<20210521 09:41:03># docker volume create --name volume_t1
volume_t1

[root@demo ~]<20210521 09:45:07># docker volume ls
DRIVER    VOLUME NAME
local     volume_t1

用户在使用docker run或docker create创建新容器时,也可以使用-v标签为容器添加volume,以下命令创建了一个随机名字的volume,并挂载到容器中的指定目录下。

$ docker run -d -v /tmp nginx:1.14-alpine  sh

以下命令创建了一个指定名字的volume,并挂载到容器中的/data目录下。

$ docker run -d -v volume_t2:/data nginx:1.14-alpine  sh

Docker在创建volume的时候会在宿主机/var/lib/docker/volume/中创建一个以volume ID为名的目录,并将volume中的内容存储在名为_data的目录下。

使用docker volume inspect命令可以获得该volume包括其在宿主机中该文件夹的位置等信息。

 1 $ docker volume inspect volume_t2
 2 [
 3     {
 4         "CreatedAt": "2021-05-21T09:54:09+08:00",
 5         "Driver": "local",
 6         "Labels": null,
 7         "Mountpoint": "/var/lib/docker/volumes/volume_t2/_data",
 8         "Name": "volume_t2",
 9         "Options": null,
10         "Scope": "local"
11     }
12 ]

5.1.2. 挂载volume

用户在使用docker run或docker create创建新容器时,可以使用-v标签为容器添加volume。用户可以将自行创建或者由Docker创建的volume挂载到容器中,也可以将宿主机上的目录或者文件作为volume挂载到容器中。下面分别介绍这两种挂载方式。

用户可以使用如下命令创建volume,并将其创建的volume挂载到容器中的/data目录下。

$ docker volume create --name volume_t3
volume_t3

$  docker run --rm -v volume_t3:/data nginx:1.14-alpine sh

如果用户不执行第一条命令而直接执行第二条命令的话,Docker会代替用户来创建一个名为vol_t3的volume,并将其挂载到容器中的/data目录下。

用户也可以使用如下命令创建一个随机ID的volume,并将其挂载到/data目录下。

$ docker run --rm -v /data nginx:1.14-alpine sh

以上命令都是将自行创建或者由Docker创建的volume挂载到容器中。Docker同时也允许我们将宿主机上的目录挂载到容器中。

$ docker run --rm -v /host/dir:/container/dir ubuntu /bin/bash

使用以上命令将宿主机中的/host/dir文件夹作为一个volume挂载到容器中的/container/dir。注意以下几点:

  • 文件夹必须使用绝对路径。

  • 如果宿主机中不存在/host/dir,将创建一个空文件夹。

  • 在/host/dir文件夹中的所有文件或文件夹可以在容器的/container/dir文件夹下被访问。

  • 如果镜像中原本存在/container/dir文件夹,该文件夹下原有的内容将被隐藏,以保持与宿主机中的文件夹一致。

用户还可以将单个的文件作为volume挂载到容器中:

$ docker run --rm -v host_file:container_file ubuntu /bin/bash

使用上条命令将主机中的/host/file文件作为一个volume挂载到容器中的/container/file。文件必须使用绝对路径,如果文件中不存在/host/file,则Docker会创建一个同名空目录。挂载后文件内容与宿主机中的文件一致,也就是说如果容器中原本存在/container/file,该文件将被隐藏。

小技巧

将主机上的文件或文件夹作为volume挂载时,可以使用:ro指定该volume为只读。

$  echo 'My Logs' > /tmp/test.log
$  docker run -it --rm -v /tmp/test.log:/tmp/container.log:ro nginx:1.14-alpine sh
/ # cat /tmp/container.log
My Logs
/ # echo 'container' >> /tmp/container.log
sh: can't create /tmp/container.log: Read-only file system
/ # exit

类似于SELinux这类的标签系统,可以在volume挂载时使用z和Z来指定该volume是否可以共享。Docker中默认的是z,即共享该volume。 用户也可以在挂载时使用Z来标注该volume为私有数据卷。

容器卷允许多挂载测试
 1$ echo 'start test' > /tmp/test.log  # 创建测试数据
 2$ docker run -it --rm --name container-1 -v /tmp/test.log:/tmp/container.log:z,rw  nginx:1.14-alpine sh  # 启动第一个容器
 3/ # cat /tmp/container.log
 4start test
 5/ # echo ' container-1' >> /tmp/container.log  # 写入内容到共享卷
 6
 7
 8// 新开启一个tty终端
 9$ docker run -it --rm --name container-2 -v /tmp/test.log:/tmp/container.log:z,rw  nginx:1.14-alpine sh
10/ # cat /tmp/container.log
11start test
12container-1

在使用docker run或docker create创建新容器时,可以使用多个-v标签为容器添加多个volume。

$ docker run -it -v /host/dir1:/contaier/dir1 -v /host/dir2:/container/dir2 ubuntu /bin/bash

5.1.3. Dockerfile引用挂载卷

使用VOLUME指令向容器添加volume。

$ VOLUME /data

在使用docker build命令生成镜像并且以该镜像启动容器时会挂载一个volume到/data。与前面的volume_t2例子类似,如果镜像中存在/data文件夹,这个文件夹中的内容将全部被复制到宿主机中对应的文件夹中,并且根据容器中的文件设置合适的权限和所有者。

注解

类似的,可以使用VOLUME指令添加多个volume

VOLUME ["/data1", "data2"]

与使用docker run -v不同的是,VOLUME指令不能挂载主机中指定的文件夹。这是为了保证Dockerfile的可移植性,因为不能保证所有的宿主机都有对应的文件夹。

需要注意的是,在Dockerfile中使用VOLUME指令之后的代码,如果尝试对这个volume进行修改,这些修改都不会生效。在下面的例子中,在创建volume后,尝试在其中添加一些初始化的文件并改变文件所有权。

Dockerfile
1FROM ubuntu
2RUN useradd foo
3VOLUME /foo
4RUN touch /foo/bar.sh
5RUN chown -R foo:foo /foo
$ docker build -t my_ubuntu .
Sending build context to Docker daemon  2.048kB
Step 1/5 : FROM ubuntu
 ---> 7e0aa2d69a15
Step 2/5 : RUN useradd foo
 ---> Using cache
 ---> b1415a88d214
Step 3/5 : VOLUME /foo
 ---> Running in 4fc0d264a00a
Removing intermediate container 4fc0d264a00a
 ---> c6af3e144134
Step 4/5 : RUN touch /foo/bar.sh
 ---> Running in d964a67d8f23
Removing intermediate container d964a67d8f23
 ---> 16b2876cf52a
Step 5/5 : RUN chown -R foo:foo /foo
 ---> Running in 61727a4c5057
Removing intermediate container 61727a4c5057
 ---> 68f6dc9e0354
Successfully built 68f6dc9e0354
Successfully tagged my_ubuntu:latest

查看挂载内容

1$  docker run  --rm  -it  my_ubuntu:latest ls /foo
2$

通过这个Dockerfile创建镜像并启动容器后,该容器中存在用户foo,并且能看到在/data挂载的volume,但是/data文件夹内并没有文件file, 更别说file的所有者并没有被改变为foo。这是由于Dockerfile中除了FROM指令的每一行都是基于上一行生成的临时镜像运行一个容器, 执行一条指令并执行类似docker commit的命令得到一个新的镜像,这条类似docker commit的命令不会对挂载的volume进行保存。 所以上面的Dockerfile最后两行执行时,都会在一个临时的容器上挂载/data,并对这个临时的volume进行操作,但是这一行指令执行并提交后, 这个临时的volume没有被保存,我们通过最后生成的镜像创建的容器所挂载的volume是没有操作过的。

如果想要对volume进行初始化或者改变所有者,可以使用以下方式。

Dockerfile
1FROM ubuntu
2RUN useradd foo
3RUN mkdir /foo && touch /foo/bar.sh
4RUN chown -R foo:foo /foo
5VOLUME /foo

docker build 构建该镜像

$ docker build -t ubuntu:v1 .
  Sending build context to Docker daemon  2.048kB
  Step 1/5 : FROM ubuntu
   ---> 7e0aa2d69a15
  Step 2/5 : RUN useradd foo
   ---> Using cache
   ---> b1415a88d214
  Step 3/5 : RUN mkdir /foo && touch /foo/bar.sh
   ---> Using cache
   ---> 3e35b90c9e3c
  Step 4/5 : RUN chown -R foo:foo /foo
   ---> Using cache
   ---> 51db0e326a11
  Step 5/5 : VOLUME /foo
   ---> Using cache
   ---> 7b99e96e8203
  Successfully built 7b99e96e8203
  Successfully tagged ubuntu:v1

查看挂载内容

1$ docker run  --rm  -it ubuntu:v1 ls /foo
2bar.sh

5.1.3.1. Dockerfile中Volume的应用场景

通过上面的案例我们了解了怎么在Dockerfile中声明

首先创建两个独立的Docker容器

$ docker run --name u1 -d ubuntu:v1 sleep 500
0e089f9fdc1a0063e872b2a24c8525a180686e23e9a9d6341942e61c02075fd5
$ docker run --name u2 -d ubuntu:v1 sleep 500
68a9313106132fb504eb434f4d4a03b48883fa231b6e9b7e243b2f9ad7ded249

查看生成的随机id的volume

$ docker volume ls
DRIVER    VOLUME NAME
local     1fc430323d92e5ef446464d00b96e1f5326773359592f91294d12a466c4cd108
local     50a8727c18b968158827cbd286afff703cc81e5c5dac4dc9e7f2f18cf02350b9

接下来创建一个新的容器,引用u1的卷

$ docker run --name u3 -d --volumes-from u1 ubuntu:v1 sleep 500
98d1ac153a67abf89ac5de770d43f364a68469053a5181f91dac31dbf007aec9

$ docker volume ls  # 发现没有新的volume创建出来
DRIVER    VOLUME NAME
local     1fc430323d92e5ef446464d00b96e1f5326773359592f91294d12a466c4cd108
local     50a8727c18b968158827cbd286afff703cc81e5c5dac4dc9e7f2f18cf02350b9

接下来在u1容器内的卷空间下写入数据,再从u3内查看内容是否自动更新

1$ docker exec -it u1 sh
2# echo 'hello u1'>>/foo/bar.sh
3# exit
4
5$ docker exec -it u3 cat /foo/bar.sh
6hello u1

小技巧

  • 如果被共享的容器有多个volume,新容器也将有多个volume,并且其挂载的目的目录也与被共享的容器中的相同。

  • 一个容器挂载了一个volume,即使这个容器停止运行,该volume仍然存在,其他容器也可以使用 --volumes-from 与这个容器共享volume。如果有一些数据,比如配置文件、数据文件等,要在多个容器之间共享,一种常见的做法是创建一个数据容器,其他的容器与之共享volume。

  • 可以使用多个 --volumes-from 标签,使得容器与多个已有容器共享volume。

5.1.4. 删除volume

如果创建容器时从容器中挂载了volume,在 /var/lib/docker/volumes 下会生成与volume对应的目录,使用 docker rm 删除容器并不会删除与volume对应的目录,这些目录会占据不必要的存储空间,即使可以手动删除,因为有些随机生成的目录名称是无意义的随机字符串,要知道它们是否与未被删除的容器对应也十分麻烦。所以在删除容器时需要对容器的volume妥善处理。在删除容器时一并删除volume有以下3种方法。

  • 使用 docker volume rm <volume_name> 删除volume

  • 使用 docker rm -v <container_name> 删除容器

  • 在运行容器时使用 ``docker run –rm, –rm `` 标签会在容器停止运行时删除容器以及容器所挂载的volume

警告

在使用docker volume rm删除volume时,只有当没有任何容器使用该volume的时候,该volume才能成功删除.另外两种方法只会对挂载在该容器上的未命名的volume进行删除,而会对用户指定名字的volume进行保留

如果volume是在创建容器时从宿主机中挂载的,无论对容器进行任何操作都不会导致其在宿主机中被删除,如果不需要这些文件,只能手动删除它们。

5.1.5. 备份、恢复或迁移volume

volume作为数据的载体,在很多情况下需要对其中的数据进行备份、迁移,或是从已有数据恢复。

如果需要将Volume里面的数据备份,一个很容易想到的方法是使用docker inspect命令查找到volume在宿主机上对应的文件夹位置,然后复制其中的内容或是使用tar进行打包;同样地,如果需要恢复某个volume中的数据,可以查找到volume对应的文件夹,将数据复制进这个文件夹或是使用tar从存档文件中恢复。 这些做法可行但并不值得推荐,下面推荐一个用 --volumes-from 实现的volume的备份与恢复方法。

备份volume可以使用以下方法:

backup container
1$ docker run --rm --volumes-from <被备份的容器> -v $(pwd):/backup ubuntu tar cvf /backup/data.tar /<被备份的数据目录>
2
3# 例如备份刚才的u1容器中/foo目录的数据
4$ docker run --rm --volumes-from u1 -v $(pwd):/backup ubuntu tar cvf /backup/data.tar /foo

u1容器包含了我们希望备份的一个volume,上面这行命令启动了另外一个临时的容器,这个容器挂载了两个volume,第一个volume来自于u1容器的共享, 也就是需要备份的volume,第二个volume将宿主机的当前目录挂载到容器的/backup下。容器运行后将要备份的内容(/foo文件夹)备份到/backup/data.tar, 然后删除容器,备份后的data.tar就留在了当前目录。

恢复volume可以使用以下方法:

恢复容器数据
1$ docker run -it --name u_back ubuntu:v1 /bin/bash
2$ docker run --rm --volumes-from u_back -v $(pwd):/backup ubuntu tar xvf data.tar -C /foo

首先运行了一个新容器作为数据恢复的目标。第二行指令启动了一个临时容器,这个容器挂载了两个volume,第一个volume与要恢复的volume共享, 第二个volume将宿主机的当前目录挂载到容器的/backup下。由于之前备份的data.tar在当前目录下,那么它在容器中的/backup也能访问到, 容器启动后将这个存档文件中的/data恢复到/foo目录下,然后删除容器,恢复后的数据就在vol_bck的volume中了。