简介
默认情况下,容器内创建的所有文件都存储在可写的容器层上。这意味着:
- 如果容器不再存在,数据将会丢失。
- 容器的可写层与主机紧密耦合。
- 要管理文件系统,你需要一个使用 Linux 内核提供联合文件系统的存储驱动程序。与直接写入文件系统的「数据卷」相比,这种额外的抽象会降低性能。
Docker 提供了两种将文件存储在主机中的选项:「卷」(volumes)和「绑定挂载」(bind mounts)。如果你在 Linux 上运行 Docker,还可以使用「临时文件系统挂载」(tmpfs mount);在 Windows 上运行 Docker 时,也可以使用「命名管道」(named pipe)。

- 「卷」存储在由 Docker 管理的主机文件系统中。
- 「绑定挂载」可以存储在主机系统的任何位置。
- 「临时文件系统挂载」仅存储在主机内存中。
最初,--mount标志用于 Docker Swarm 服务,--volume标志用于独立容器。从 Docker 17.06 及更高版本开始,你也可以将--mount用于独立容器,并且它通常比--volume更明确、更详细。
卷
「数据卷」(data volume)或「卷」(volume)是绕过 Docker 的「联合文件系统」的目录。
卷有三种类型:
- 匿名卷
- 命名卷
- 主机卷
匿名卷
让我们创建一个流行的开源 NoSQL 数据库 CouchDB 的实例,并使用「匿名卷」来存储数据库的数据文件。
要运行 CouchDB 实例,请使用 Docker Hub 上的 CouchDB 镜像,网址为https://hub.docker.com/_/couchdb。文档中提到,CouchDB 的默认设置是「使用其自己的内部卷管理将数据库文件写入主机系统的磁盘」。
运行以下命令:
docker run -d -p 5984:5984 --name my-couchdb -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=passw0rd1 couchdb:3.1
CouchDB 将创建一个匿名卷并生成一个哈希名称。检查主机系统上的卷:
labex:~/ $ docker volume ls
DRIVER VOLUME NAME
local 1d292aca855adb9de9be7acea88f6d3f8e6a08eef5bfd986a81f073f1906b82f
设置一个环境变量VOLUME,其值为生成的名称:
export VOLUME=<VOLUME NAME>
然后检查创建的卷,使用为该卷生成的哈希名称:
$ docker volume inspect $VOLUME
[
{
"CreatedAt": "2020-09-24T14:10:07Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/f543c5319ebd96b7701dc1f2d915f21b095dfb35adbb8dc851630e098d526a50/_data",
"Name": "f543c5319ebd96b7701dc1f2d915f21b095dfb35adbb8dc851630e098d526a50",
"Options": null,
"Scope": "local"
}
]
你会看到 Docker 已经在 Docker 主机文件系统的/var/lib/docker/volumes/$VOLUME_NAME/_data下创建并管理了一个卷。请注意,这不是主机上的路径,而是 Docker 管理的文件系统的一部分。
创建一个新数据库mydb,并插入一条带有「hello world」消息的新文档。
curl -X PUT -u admin:passw0rd1 http://127.0.0.1:5984/mydb
curl -X PUT -u admin:passw0rd1 http://127.0.0.1:5984/mydb/1 -d '{"msg": "hello world"}'
停止容器并再次启动:
docker stop my-couchdb
docker start my-couchdb
检索数据库中的文档,以测试数据是否已持久保存。
curl -X GET -u admin:passw0rd1 http://127.0.0.1:5984/mydb/_all_docs
curl -X GET -u admin:passw0rd1 http://127.0.0.1:5984/mydb/1
输出:
## $ curl -X GET -u admin:passw0rd1 http://127.0.0.1:5984/mydb/_all_docs
{"total_rows":1,"offset":0,"rows":[
{"id":"1","key":"1","value":{"rev":"1-c09289617e06b96bc747fb1201fea7f1"}}
]}
## $ curl -X GET -u admin:passw0rd1 http://127.0.0.1:5984/mydb/1
{"_id":"1","_rev":"1-c09289617e06b96bc747fb1201fea7f1","msg":"hello world"}
共享卷
你可以使用--volumes-from选项与另一个容器共享匿名卷。
创建一个busybox容器,将一个匿名卷挂载到容器中的/data目录,并使用 shell 命令将一条消息写入日志文件。
$ docker run -it --name busybox1 -v /data busybox sh
/ ## echo "hello from busybox1" > /data/hi.log
/ ## ls /data
hi.log
/ ## exit
确保容器busybox1已停止但未删除。
labex:~/ $ docker ps -a | grep busybox1
f4dbf9ee7513 busybox "sh" 2 minutes ago Exited (0) About a minute ago busybox1
然后使用--volumes-from选项创建第二个名为busybox2的busybox容器,以共享busybox1创建的卷:
$ docker run --rm -it --name busybox2 --volumes-from busybox1 busybox sh
/ ## ls -al /data
total 12
drwxr-xr-x 2 root root 4096 Jan 23 07:20.
drwxr-xr-x 1 root root 4096 Jan 23 07:24..
-rw-r--r-- 1 root root 20 Jan 23 07:20 hi.log
/ ## cat /data/hi.log
hello from busybox1
/ ## exit
Docker 创建了你能够使用--volumes-from选项共享的匿名卷,并创建了一个新的匿名卷。
labex:~/ $ docker volume ls
DRIVER VOLUME NAME
local 0f971b2477d5fc0d0c2b31fc908ee59d6b577b4887e381964650ce6853890dc9
local 1d292aca855adb9de9be7acea88f6d3f8e6a08eef5bfd986a81f073f1906b82f
清理现有的卷和容器。
docker stop my-couchdb
docker rm my-couchdb
docker rm busybox1
docker volume rm $(docker volume ls -q)
docker system prune -a
clear
命名卷
「命名卷」和「匿名卷」的相似之处在于,Docker 管理它们的位置。然而,「命名卷」在挂载到容器目录时可以通过名称引用。如果你想在多个容器之间共享一个卷,这会很有帮助。
首先,创建一个「命名卷」:
docker volume create my-couchdb-data-volume
验证卷是否已创建:
$ docker volume ls
DRIVER VOLUME NAME
local my-couchdb-data-volume
现在使用「命名卷」创建名为my-couchdb-name-vol的 CouchDB 容器:
docker run -d -p 59840:5984 --name my-couchdb-name-vol -v my-couchdb-data-volume:/opt/couchdb/data -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=passw0rd1 couchdb:3.1
等待 CouchDB 容器运行并实例可用。
创建一个新数据库mydb,并插入一条带有「hello world」消息的新文档。
curl -X PUT -u admin:passw0rd1 http://127.0.0.1:59840/mydb
curl -X PUT -u admin:passw0rd1 http://127.0.0.1:59840/mydb/1 -d '{"msg": "hello world"}'
现在很容易与另一个容器共享该卷。例如,使用busybox镜像读取卷的内容,并通过将卷挂载到busybox容器中的一个目录来共享my-couchdb-data-volume卷。
labex:~/ $ docker run --rm -it --name busybox -v my-couchdb-data-volume:/myvolume busybox sh
/ #
/ ## ls -al /myvolume
total 40
drwxr-xr-x 4 5984 5984 4096 Jan 23 07:30.
drwxr-xr-x 1 root root 4096 Jan 23 07:31..
drwxr-xr-x 2 5984 5984 4096 Jan 23 07:29.delete
-rw-r--r-- 1 5984 5984 8388 Jan 23 07:30 _dbs.couch
-rw-r--r-- 1 5984 5984 8385 Jan 23 07:29 _nodes.couch
drwxr-xr-x 4 5984 5984 4096 Jan 23 07:30 shards
/ ## exit
你可以通过以特权权限运行busybox容器并将进程 ID 设置为host来检查 Docker 管理的文件系统中的卷,以检查主机系统,并浏览到 Docker 管理的目录。
docker run -it --privileged --pid=host busybox nsenter -t 1 -m -u -n -i sh
/ ## ls -l /var/lib/docker/volumes
total 28
-rw------- 1 root root 32768 Nov 10 15:54 metadata.db
drwxr-xr-x 3 root root 4096 Nov 10 15:54 my-couchdb-data-volume
/ ## exit
清理:
docker stop my-couchdb
docker rm my-couchdb
docker volume rm my-couchdb-data-volume
docker system prune -a
docker volume prune
clear
主机卷
当你希望直接从主机轻松访问卷目录,而不是使用 Docker 管理的目录时,可以创建一个「主机卷」。
让我们使用当前工作目录(通过命令pwd指示)中的一个名为data的目录,或者在主机上选择你自己的数据目录,例如/home/couchdb/data。如果$(pwd)/data目录尚不存在,我们让 Docker 创建它。我们将 CouchDB 容器内的「主机卷」挂载到容器目录/opt/couchdb/data,这是 CouchDB 的默认数据目录。
运行以下命令:
cd /home/labex/project
docker run -d -p 5984:5984 --name my-couchdb -v $(pwd)/data:/opt/couchdb/data -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=passw0rd1 couchdb:3.1
验证是否创建了一个data目录:
$ ls -al
total 20
drwxrwxr-x 3 labex labex 4096 Aug 29 14:14.
drwxr-x--- 25 labex labex 4096 Aug 29 14:14..
drwxr-xr-x 3 5984 5984 4096 Aug 29 14:14 data
并且 CouchDB 已在此处创建数据文件:
$ ls -al data
total 32
drwxr-xr-x 3 5984 5984 4096 Aug 29 14:14.
drwxrwxr-x 3 labex labex 4096 Aug 29 14:14..
-rw-r--r-- 1 5984 5984 4257 Aug 29 14:14 _dbs.couch
drwxr-xr-x 2 5984 5984 4096 Aug 29 14:14.delete
-rw-r--r-- 1 5984 5984 8385 Aug 29 14:14 _nodes.couch
还要检查现在 Docker 没有创建管理卷,因为我们现在使用的是「主机卷」。
docker volume ls
以及:
docker run -it --privileged --pid=host busybox nsenter -t 1 -m -u -n -i sh
sh-5.1## ls -l /var/lib/docker/volumes
total 28
brw------- 1 root root 252, 3 Jan 23 15:15 backingFsBlockDev
-rw------- 1 root root 32768 Jan 23 15:33 metadata.db
drwx-----x 3 root root 4096 Jan 23 15:26 my-couchdb-data-volume
sh-5.1## exit
创建一个新数据库mydb,并插入一条带有「hello world」消息的新文档。
curl -X PUT -u admin:passw0rd1 http://127.0.0.1:5984/mydb
curl -X PUT -u admin:passw0rd1 http://127.0.0.1:5984/mydb/1 -d '{"msg": "hello world"}'
请注意,CouchDB 创建了一个shards文件夹:
$ ls -al data
total 40
drwxr-xr-x 4 5984 5984 4096 Aug 29 14:15.
drwxrwxr-x 3 labex labex 4096 Aug 29 14:14..
-rw-r--r-- 1 5984 5984 8388 Aug 29 14:15 _dbs.couch
drwxr-xr-x 2 5984 5984 4096 Aug 29 14:14.delete
-rw-r--r-- 1 5984 5984 8385 Aug 29 14:14 _nodes.couch
drwxr-xr-x 4 5984 5984 4096 Aug 29 14:15 shards
列出shards目录的内容:
$ ls -al data/shards
total 16
drwxr-xr-x 4 5984 5984 4096 Aug 29 14:15.
drwxr-xr-x 4 5984 5984 4096 Aug 29 14:15..
drwxr-xr-x 2 5984 5984 4096 Aug 29 14:15 00000000-7fffffff
drwxr-xr-x 2 5984 5984 4096 Aug 29 14:15 80000000-ffffffff
以及第一个分片:
$ ls -al data/shards/00000000-7fffffff/
total 20
drwxr-xr-x 2 5984 5984 4096 Aug 29 14:15.
drwxr-xr-x 4 5984 5984 4096 Aug 29 14:15..
-rw-r--r-- 1 5984 5984 8346 Aug 29 14:15 mydb.1693289721.couch
分片是数据库中数据的水平分区。将数据划分为分片并将每个分片的副本分布到集群中的不同节点,可以使数据在节点丢失时具有更高的耐久性。CouchDB 会自动对数据库进行分片,并在节点之间分布文档子集。
清理:
docker stop my-couchdb
docker rm my-couchdb
sudo rm -rf $(pwd)/data
docker system prune -a
绑定挂载
与「卷」语法相比,Docker 推荐使用「挂载」(mount)语法。与卷相比,绑定挂载的功能有限。当一个文件或目录挂载到容器中时,它是通过主机上的完整路径来引用的。绑定挂载依赖于主机文件系统具有特定的目录结构,并且你不能使用 Docker CLI 来管理绑定挂载。请注意,绑定挂载可以通过容器中运行的进程来更改主机文件系统。
与使用由冒号分隔符(:)分隔的三个字段的-v语法不同,「挂载」语法更详细,使用多个「键值」对:
- type:bind、volume 或 tmpfs,
- source:主机上文件或目录的路径,
- destination:容器中的路径,
- readonly,
- bind-propagation:rprivate、private、rshared、shared、rslave、slave,
- consistency:consistent、delegated、cached,
- mount。
cd /home/labex/project
mkdir data
docker run -it --name busybox --mount type=bind,source="$(pwd)"/data,target=/data busybox sh
在容器中输入命令:
echo "hello busybox" > /data/hi.txt
exit
验证文件是否在主机上创建。
cat data/hi.txt
[可选] OverlayFS
OverlayFS 是 Linux 的一种「联合挂载文件系统」实现。要理解什么是 Docker 卷,有助于了解层和文件系统在 Docker 中是如何工作的。
要启动一个容器,Docker 会获取只读镜像并在其之上创建一个新的读写层。为了将这些层视为一个整体,Docker 使用联合文件系统或 OverlayFS(覆盖文件系统),具体来说是overlay2存储驱动程序。
要查看由 Docker 主机管理的文件,你需要访问 Docker 进程文件系统。使用--privileged和--pid=host标志,你可以从像busybox这样的容器内部访问主机的进程 ID 命名空间。然后,你可以浏览到 Docker 的/var/lib/docker/overlay2目录,查看由 Docker 管理的已下载层。
要查看 Docker 中当前的层列表:
$ docker run -it --privileged --pid=host busybox nsenter -t 1 -m -u -n -i sh
/ ## ls -l /var/lib/docker/overlay2
total 16
drwx------ 3 root root 4096 Sep 25 19:44 0e55ecaa4d17c353191e68022d9a17fde64fb5e9217b07b5c56eb4c74dad5b32
drwx------ 5 root root 4096 Sep 25 19:44 187854d05ccd18980642e820b0d2be6a127ba85d8ed96315bb5ae37eb1add36d
drwx------ 4 root root 4096 Sep 25 19:44 187854d05ccd18980642e820b0d2be6a127ba85d8ed96315bb5ae37eb1add36d-init
drwx------ 2 root root 4096 Sep 25 19:44 l
/ ## exit
拉取ubuntu镜像并再次检查:
docker pull ubuntu
docker run -it --privileged --pid=host busybox nsenter -t 1 -m -u -n -i sh
再次输入命令查看层列表:
ls -l /var/lib/docker/overlay2/ & exit
你会看到拉取ubuntu镜像时,隐式地拉取了 4 个新层:
- a611792b4cac502995fa88a888261dfba0b5d852e72f9db9e075050991423779
- d181f1a41fc35a45c16e8bfcb8eee6f768f3b98f82210a43ea65f284a45fcd65
- dac2f37f6280a076836d39b87b0ae5ebf5c0d386b6d8b991b103aadbcebaa7c6
- f3e921b440c37c86d06cd9c9fb70df50edad553c36cc87f84d5eeba734aae709
overlay2存储驱动程序本质上是将主机上的不同目录分层,并将它们呈现为一个单一目录。
- 基础层或 lowerdir
diff层或 upperdir- 覆盖层(用户视图)
work目录
OverlayFS 将较低的目录称为lowerdir,其中包含基础镜像和拉取下来的只读(R/O)层。
上层目录称为upperdir,是读写(R/W)容器层。
统一视图或overlay层称为merged。
最后,workdir是必需的,它是 overlay 用于内部使用的空目录。
overlay2驱动程序最多支持 128 个较低的 OverlayFS 层。l目录包含作为符号链接的缩短层标识符。

清理:
docker system prune -a
clear
总结
在本实验中,你学习了如何使用「卷」(volumes)和「绑定挂载」(bind mounts)来管理容器中的数据。你还了解了overlay2存储驱动程序以及它如何使用「联合文件系统」来管理层。