卷是容器的持久数据存储,由Docker创建和管理。您可以使用docker volume create命令显式创建卷,或者Docker可以在容器或服务创建期间创建卷。

当你创建一个卷时,它会被存储在Docker主机上的一个目录中。当你将卷挂载到容器中时,这个目录就是被挂载到容器中的内容。这与绑定挂载的工作方式类似,不同之处在于卷是由Docker管理的,并且与主机的核心功能隔离。

何时使用卷

卷是持久化由Docker容器生成和使用的数据的首选机制。虽然绑定挂载依赖于主机的目录结构和操作系统,但卷完全由Docker管理。卷适用于以下用例:

  • 卷比绑定挂载更容易备份或迁移。
  • 您可以使用Docker CLI命令或Docker API来管理卷。
  • 卷适用于Linux和Windows容器。
  • 卷可以在多个容器之间更安全地共享。
  • 新卷的内容可以由容器或构建预先填充。
  • 当您的应用程序需要高性能I/O时。

如果您需要从主机访问文件,卷不是一个好的选择,因为卷完全由Docker管理。如果您需要从容器和主机访问文件或目录,请使用绑定挂载

卷通常是比直接将数据写入容器更好的选择,因为卷不会增加使用它的容器的大小。使用卷也更快;写入容器的可写层需要一个存储驱动来管理文件系统。存储驱动使用Linux内核提供联合文件系统。与直接写入主机文件系统的卷相比,这种额外的抽象降低了性能。

如果你的容器生成非持久状态数据,考虑使用 tmpfs挂载来避免永久存储数据,并通过避免写入容器的 可写层来提高容器的性能。

卷使用rprivate绑定传播,并且卷的绑定传播不可配置。

卷的生命周期

卷的内容存在于给定容器的生命周期之外。当容器被销毁时,可写层也会随之销毁。使用卷可以确保即使使用它的容器被移除,数据仍然会被保留。

给定的卷可以同时挂载到多个容器中。当没有正在运行的容器使用卷时,该卷仍然对Docker可用,并且不会自动删除。您可以使用docker volume prune删除未使用的卷。

在现有数据上挂载卷

如果你将一个非空卷挂载到容器中的一个目录中,而该目录中已经存在文件或目录,那么预先存在的文件将被挂载所遮蔽。这类似于你在Linux主机上将文件保存到/mnt,然后将USB驱动器挂载到/mnt/mnt的内容将被USB驱动器的内容遮蔽,直到USB驱动器被卸载。

使用容器时,没有直接的方法可以移除挂载以再次显示被隐藏的文件。您的最佳选择是在没有挂载的情况下重新创建容器。

如果你将一个空卷挂载到容器中已存在文件或目录的目录中,默认情况下这些文件或目录会被传播(复制)到卷中。同样,如果你启动一个容器并指定一个尚不存在的卷,系统会为你创建一个空卷。这是预填充另一个容器所需数据的好方法。

为了防止Docker将容器的预先存在的文件复制到空卷中,请使用volume-nocopy选项,参见 --mount 的选项

命名和匿名卷

卷可以被命名或匿名。匿名卷会被赋予一个随机名称,保证在给定的Docker主机内是唯一的。就像命名卷一样,即使你删除了使用它们的容器,匿名卷也会持续存在,除非你在创建容器时使用了--rm标志,在这种情况下,与容器关联的匿名卷会被销毁。参见删除匿名卷

如果你连续创建多个使用匿名卷的容器,每个容器都会创建自己的卷。匿名卷不会在容器之间自动重用或共享。要在两个或更多容器之间共享匿名卷,你必须使用随机卷ID挂载匿名卷。

语法

要使用docker run命令挂载卷,您可以使用--mount--volume标志。

$ docker run --mount type=volume,src=<volume-name>,dst=<mount-path>
$ docker run --volume <volume-name>:<mount-path>

一般来说,--mount 是首选。主要区别在于 --mount 标志更加明确,并且支持所有可用的选项。

如果你想实现以下目的,必须使用 --mount

--mount 的选项

--mount 标志由多个键值对组成,用逗号分隔,每个键值对由一个 = 元组组成。键的顺序不重要。

$ docker run --mount type=volume[,src=<volume-name>],dst=<mount-path>[,<key>=<value>...]

--mount type=volume 的有效选项包括:

OptionDescription
source, srcThe source of the mount. For named volumes, this is the name of the volume. For anonymous volumes, this field is omitted.
destination, dst, targetThe path where the file or directory is mounted in the container.
volume-subpathA path to a subdirectory within the volume to mount into the container. The subdirectory must exist in the volume before the volume is mounted to a container. See 挂载卷子目录.
readonly, roIf present, causes the volume to be 作为只读挂载到容器中.
volume-nocopyIf present, data at the destination isn't copied into the volume if the volume is empty. By default, content at the target destination gets copied into a mounted volume if empty.
volume-optCan be specified more than once, takes a key-value pair consisting of the option name and its value.
Example
$ docker run --mount type=volume,src=myvolume,dst=/data,ro,volume-subpath=/foo

--volume 的选项

--volume-v 标志由三个字段组成,用冒号字符 (:) 分隔。字段必须按正确的顺序排列。

$ docker run -v [<volume-name>:]<mount-path>[:opts]

在命名卷的情况下,第一个字段是卷的名称,并且在给定主机上是唯一的。对于匿名卷,第一个字段被省略。第二个字段是文件或目录在容器中挂载的路径。

第三个字段是可选的,是一个逗号分隔的选项列表。对于带有数据卷的--volume,有效的选项包括:

OptionDescription
readonly, roIf present, causes the volume to be 作为只读挂载到容器中.
volume-nocopyIf present, data at the destination isn't copied into the volume if the volume is empty. By default, content at the target destination gets copied into a mounted volume if empty.
Example
$ docker run -v myvolume:/data:ro

创建和管理卷

与绑定挂载不同,您可以在任何容器的范围之外创建和管理卷。

创建一个卷:

$ docker volume create my-vol

列出卷:

$ docker volume ls

local               my-vol

检查一个卷:

$ docker volume inspect my-vol
[
    {
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-vol",
        "Options": {},
        "Scope": "local"
    }
]

移除一个卷:

$ docker volume rm my-vol

启动一个带有卷的容器

如果你启动一个容器时使用了一个尚不存在的卷,Docker 会为你创建该卷。以下示例将卷 myvol2 挂载到容器中的 /app/ 目录。

以下 -v--mount 示例产生相同的结果。除非在运行第一个示例后删除 devtest 容器和 myvol2 卷,否则不能同时运行它们。


$ docker run -d \
  --name devtest \
  --mount source=myvol2,target=/app \
  nginx:latest
$ docker run -d \
  --name devtest \
  -v myvol2:/app \
  nginx:latest

使用 docker inspect devtest 来验证 Docker 是否创建了卷并正确挂载。查找 Mounts 部分:

"Mounts": [
    {
        "Type": "volume",
        "Name": "myvol2",
        "Source": "/var/lib/docker/volumes/myvol2/_data",
        "Destination": "/app",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
],

这表明挂载是一个卷,它显示了正确的源和目标,并且挂载是读写的。

停止容器并移除卷。注意,卷的移除是一个单独的步骤。

$ docker container stop devtest

$ docker container rm devtest

$ docker volume rm myvol2

使用 Docker Compose 挂载卷

以下示例展示了一个带有卷的单个 Docker Compose 服务:

services:
  frontend:
    image: node:lts
    volumes:
      - myapp:/home/node/app
volumes:
  myapp:

首次运行docker compose up时会创建一个卷。当你随后运行该命令时,Docker会重用相同的卷。

你可以直接在Compose之外使用docker volume create创建一个卷,然后在compose.yaml中引用它,如下所示:

services:
  frontend:
    image: node:lts
    volumes:
      - myapp:/home/node/app
volumes:
  myapp:
    external: true

有关在Compose中使用卷的更多信息,请参阅Compose规范中的 Volumes 部分。

启动带有卷的服务

当你启动一个服务并定义一个卷时,每个服务容器都使用其自己的本地卷。如果你使用local卷驱动程序,这些容器都无法共享这些数据。然而,一些卷驱动程序确实支持共享存储。

以下示例启动了一个带有四个副本的nginx服务,每个副本都使用一个名为myvol2的本地卷。

$ docker service create -d \
  --replicas=4 \
  --name devtest-service \
  --mount source=myvol2,target=/app \
  nginx:latest

使用 docker service ps devtest-service 来验证服务是否正在运行:

$ docker service ps devtest-service

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
4d7oz1j85wwn        devtest-service.1   nginx:latest        moby                Running             Running 14 seconds ago

您可以移除服务以停止正在运行的任务:

$ docker service rm devtest-service

删除服务不会删除由服务创建的任何卷。 卷删除是一个单独的步骤。

使用容器填充卷

如果你启动一个创建新卷的容器,并且容器在要挂载的目录中有文件或目录,例如/app/,Docker会将目录的内容复制到卷中。然后容器挂载并使用该卷,使用该卷的其他容器也可以访问预先填充的内容。

为了展示这一点,以下示例启动了一个nginx容器,并将容器/usr/share/nginx/html目录的内容填充到新卷nginx-vol中。这是Nginx存储其默认HTML内容的地方。

--mount-v 示例具有相同的最终结果。


$ docker run -d \
  --name=nginxtest \
  --mount source=nginx-vol,destination=/usr/share/nginx/html \
  nginx:latest
$ docker run -d \
  --name=nginxtest \
  -v nginx-vol:/usr/share/nginx/html \
  nginx:latest

在运行这些示例中的任何一个之后,运行以下命令来清理容器和卷。请注意,卷的移除是一个单独的步骤。

$ docker container stop nginxtest

$ docker container rm nginxtest

$ docker volume rm nginx-vol

使用只读卷

对于一些开发应用程序,容器需要写入绑定挂载,以便更改传播回Docker主机。在其他时候,容器只需要对数据的读取权限。多个容器可以挂载相同的卷。您可以同时将单个卷挂载为某些容器的read-write,并为其他容器挂载为read-only

以下示例对前一个示例进行了更改。它通过将ro添加到容器内的挂载点后的(默认情况下为空的)选项列表中,将目录挂载为只读卷。当存在多个选项时,可以使用逗号分隔它们。

--mount-v 示例具有相同的结果。


$ docker run -d \
  --name=nginxtest \
  --mount source=nginx-vol,destination=/usr/share/nginx/html,readonly \
  nginx:latest
$ docker run -d \
  --name=nginxtest \
  -v nginx-vol:/usr/share/nginx/html:ro \
  nginx:latest

使用 docker inspect nginxtest 来验证 Docker 是否正确创建了只读挂载。查找 Mounts 部分:

"Mounts": [
    {
        "Type": "volume",
        "Name": "nginx-vol",
        "Source": "/var/lib/docker/volumes/nginx-vol/_data",
        "Destination": "/usr/share/nginx/html",
        "Driver": "local",
        "Mode": "",
        "RW": false,
        "Propagation": ""
    }
],

停止并移除容器,并移除卷。卷移除是一个单独的步骤。

$ docker container stop nginxtest

$ docker container rm nginxtest

$ docker volume rm nginx-vol

挂载卷子目录

当你将一个卷挂载到一个容器时,你可以使用--mount标志的volume-subpath参数来指定要使用的卷的子目录。你指定的子目录在尝试将其挂载到容器之前必须存在于卷中;如果它不存在,挂载将失败。

指定volume-subpath非常有用,如果你只想与容器共享卷的特定部分。例如,假设你有多个容器在运行,并且你想将每个容器的日志存储在共享卷中。你可以在共享卷中为每个容器创建一个子目录,并将该子目录挂载到容器中。

以下示例创建了一个logs卷,并在该卷中初始化了子目录app1app2。然后启动两个容器,并将logs卷的一个子目录挂载到每个容器中。此示例假设容器中的进程将其日志写入/var/log/app1/var/log/app2

$ docker volume create logs
$ docker run --rm \
  --mount src=logs,dst=/logs \
  alpine mkdir -p /logs/app1 /logs/app2
$ docker run -d \
  --name=app1 \
  --mount src=logs,dst=/var/log/app1/,volume-subpath=app1 \
  app1:latest
$ docker run -d \
  --name=app2 \
  --mount src=logs,dst=/var/log/app2,volume-subpath=app2 \
  app2:latest

通过这种设置,容器将它们的日志写入logs卷的单独子目录中。容器无法访问其他容器的日志。

在机器之间共享数据

在构建容错应用程序时,您可能需要配置同一服务的多个副本以访问相同的文件。

shared storage

在开发应用程序时,有几种方法可以实现这一点。 一种是在应用程序中添加逻辑,将文件存储在像Amazon S3这样的云对象存储系统上。 另一种是使用支持将文件写入外部存储系统(如NFS或Amazon S3)的驱动程序创建卷。

卷驱动程序允许您将底层存储系统与应用程序逻辑分离。例如,如果您的服务使用带有NFS驱动程序的卷,您可以更新服务以使用不同的驱动程序。例如,为了在云中存储数据,而无需更改应用程序逻辑。

使用卷驱动

当你使用docker volume create创建卷时,或者当你启动一个使用尚未创建的卷的容器时,你可以指定一个卷驱动程序。以下示例使用vieux/sshfs卷驱动程序,首先在创建独立卷时使用,然后在启动创建新卷的容器时使用。

注意

如果你的卷驱动程序接受逗号分隔的列表作为选项,你必须从外部的CSV解析器中转义该值。要转义volume-opt,请用双引号(")将其包围,并用单引号(')包围整个挂载参数。

例如,local驱动程序接受挂载选项作为o参数中的逗号分隔列表。此示例显示了正确转义列表的方法。

$ docker service create \
 --mount 'type=volume,src=,dst=,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:,"volume-opt=o=addr=,vers=4,soft,timeo=180,bg,tcp,rw"'
 --name myservice \
 

初始设置

以下示例假设您有两个节点,第一个是Docker主机,并且可以使用SSH连接到第二个节点。

在Docker主机上,安装vieux/sshfs插件:

$ docker plugin install --grant-all-permissions vieux/sshfs

使用卷驱动程序创建卷

此示例指定了一个SSH密码,但如果两台主机配置了共享密钥,则可以排除密码。每个卷驱动程序可能有零个或多个可配置选项,您可以使用-o标志指定每个选项。

$ docker volume create --driver vieux/sshfs \
  -o sshcmd=test@node2:/home/test \
  -o password=testpassword \
  sshvolume

启动一个使用卷驱动程序创建卷的容器

以下示例指定了一个SSH密码。但是,如果两台主机配置了共享密钥,则可以排除密码。 每个卷驱动程序可能有零个或多个可配置选项。

注意

如果卷驱动程序要求您传递任何选项, 您必须使用--mount标志来挂载卷,而不是-v

$ docker run -d \
  --name sshfs-container \
  --mount type=volume,volume-driver=vieux/sshfs,src=sshvolume,target=/app,volume-opt=sshcmd=test@node2:/home/test,volume-opt=password=testpassword \
  nginx:latest

创建一个服务以创建NFS卷

以下示例展示了在创建服务时如何创建NFS卷。 它使用10.0.0.10作为NFS服务器,并使用/var/docker-nfs作为NFS服务器上的导出目录。 请注意,指定的卷驱动程序是local

NFSv3

$ docker service create -d \
  --name nfs-service \
  --mount 'type=volume,source=nfsvolume,target=/app,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/var/docker-nfs,volume-opt=o=addr=10.0.0.10' \
  nginx:latest

NFSv4

$ docker service create -d \
    --name nfs-service \
    --mount 'type=volume,source=nfsvolume,target=/app,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/var/docker-nfs,"volume-opt=o=addr=10.0.0.10,rw,nfsvers=4,async"' \
    nginx:latest

创建CIFS/Samba卷

您可以直接在Docker中挂载Samba共享,而无需在主机上配置挂载点。

$ docker volume create \
	--driver local \
	--opt type=cifs \
	--opt device=//uxxxxx.your-server.de/backup \
	--opt o=addr=uxxxxx.your-server.de,username=uxxxxxxx,password=*****,file_mode=0777,dir_mode=0777 \
	--name cif-volume

如果您指定主机名而不是IP地址,则addr选项是必需的。这使Docker能够执行主机名查找。

块存储设备

你可以将一个块存储设备,如外部驱动器或驱动器分区,挂载到一个容器中。 以下示例展示了如何创建并使用一个文件作为块存储设备, 以及如何将该块设备挂载为容器卷。

重要

以下过程仅为一个示例。 此处展示的解决方案不建议作为常规做法。 除非您确信自己在做什么,否则不要尝试这种方法。

块设备挂载的工作原理

在底层,使用local存储驱动程序的--mount标志会调用Linux的mount系统调用,并将您传递的选项原封不动地转发给它。Docker在Linux内核支持的原生挂载功能之上没有实现任何额外的功能。

如果你熟悉 Linux mount 命令, 你可以将 --mount 选项视为以下方式转发给 mount 命令:

$ mount -t <mount.volume-opt.type> <mount.volume-opt.device> <mount.dst> -o <mount.volume-opts.o>

为了进一步解释这一点,请考虑以下mount命令示例。 此命令将/dev/loop5设备挂载到系统上的/external-drive路径。

$ mount -t ext4 /dev/loop5 /external-drive

以下docker run命令从运行的容器的角度来看,实现了类似的结果。 使用此--mount选项运行容器会以与执行前一个示例中的mount命令相同的方式设置挂载。

$ docker run \
  --mount='type=volume,dst=/external-drive,volume-driver=local,volume-opt=device=/dev/loop5,volume-opt=type=ext4'

你不能直接在容器内运行mount命令, 因为容器无法访问/dev/loop5设备。 这就是为什么docker run命令使用--mount选项的原因。

示例:在容器中挂载块设备

以下步骤创建了一个ext4文件系统并将其挂载到容器中。 您系统的文件系统支持取决于您使用的Linux内核版本。

  1. 创建一个文件并为其分配一些空间:

    $ fallocate -l 1G disk.raw
    
  2. disk.raw文件上构建一个文件系统:

    $ mkfs.ext4 disk.raw
    
  3. 创建一个循环设备:

    $ losetup -f --show disk.raw
    /dev/loop5
    

    注意

    losetup 创建一个临时的循环设备,该设备在系统重启后会被移除,或者可以通过 losetup -d 手动移除。

  4. 运行一个将循环设备挂载为卷的容器:

    $ docker run -it --rm \
      --mount='type=volume,dst=/external-drive,volume-driver=local,volume-opt=device=/dev/loop5,volume-opt=type=ext4' \
      ubuntu bash
    

    当容器启动时,路径 /external-drive 将主机文件系统中的 disk.raw 文件挂载为块设备。

  5. 当你完成后,设备从容器中卸载时, 分离循环设备以从主机系统中移除设备:

    $ losetup -d /dev/loop5
    

备份、恢复或迁移数据卷

卷对于备份、恢复和迁移非常有用。 使用 --volumes-from 标志来创建一个挂载该卷的新容器。

备份卷

例如,创建一个名为 dbstore 的新容器:

$ docker run -v /dbdata --name dbstore ubuntu /bin/bash

在下一个命令中:

  • 启动一个新容器并从dbstore容器挂载卷
  • 将本地主机目录挂载为 /backup
  • 传递一个命令,将dbdata卷的内容打包成一个backup.tar文件,放在/backup目录中。
$ docker run --rm --volumes-from dbstore -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

当命令完成且容器停止时,它会创建dbdata卷的备份。

从备份恢复卷

使用刚刚创建的备份,您可以将其恢复到同一个容器,或者恢复到您在其他地方创建的其他容器。

例如,创建一个名为 dbstore2 的新容器:

$ docker run -v /dbdata --name dbstore2 ubuntu /bin/bash

然后,在新容器的数据卷中解压备份文件:

$ docker run --rm --volumes-from dbstore2 -v $(pwd):/backup ubuntu bash -c "cd /dbdata && tar xvf /backup/backup.tar --strip 1"

您可以使用这些技术来自动化备份、迁移和恢复测试,使用您偏好的工具。

移除卷

Docker 数据卷在删除容器后仍然存在。有两种类型的卷需要考虑:

  • 命名卷具有来自容器外部的特定源,例如,awesome:/bar
  • 匿名卷没有特定的来源。因此,当容器被删除时,您可以指示Docker Engine守护进程删除它们。

移除匿名卷

要自动删除匿名卷,请使用--rm选项。例如, 此命令创建一个匿名的/foo卷。当您删除容器时, Docker引擎会删除/foo卷,但不会删除awesome卷。

$ docker run --rm -v /foo -v awesome:/bar busybox top

注意

如果另一个容器使用 --volumes-from绑定卷,卷定义会被复制,并且 匿名卷在第一个容器被移除后仍然存在。

删除所有卷

要删除所有未使用的卷并释放空间:

$ docker volume prune

下一步