Docker 数据持久化

概览

默认情况下,在 docker 容器(container)内创建的文件或产生的数据都只是保存在容器的可写层,这意味着当容器不存在时,容器内产生的数据也没有保存下来。

Docker 提供两种容器数据持久化的方法,使用这两种方法即使容器不存在时,数据也能持久化下来:

  • Bind mount:bind mount 可以是宿主机(host)文件系统的任意目录或文件,除了 docker 容器可以访问,宿主机上也可以对这些目录或文件读写操作
  • Volume:volume 数据持久化在宿主机文件系统 /var/lib/docker/volumes/ 目录下,volume 数据由 docker 进行管理,外部不要对 volume 数据进程修改。 Volume 方式是 docker 官方推荐的数据持久化方式。

另外,如果 docker 运行在 Linux 宿主机,还可以使用 tmpfs mounttmpfs mount 的数据只是保存在宿主机的内存中,并没有持久化到硬盘。

Docker 容器与宿主机数据共享的方式如下图所示:

无论使用哪种方式,对于 docker 容器来说,这些数据都被看作成一个目录或文件,数据的使用方式是一样的。

Bind mount

Bind mount 方式是 docker 早期使用的容器与宿主机数据共享的方式,可以实现将宿主机上的文件或目录挂载(mount)到 docker 容器中使用。

相对于 volume 方式,bind mount 方式存在不少的局限。例如,bind mount 在 Linux 和 Windows 操作系统下不可移植。因此 docker 官方推荐使用 volume 方式。

为了便于说明 docker 数据共享的特点,我们以 busybox 镜像为例进行操作演示。

例如,使用 docker run -v 命令来将将宿主机的 /Users/lihao/code/docker/busybox 目录挂载到容器的 /databind 目录:

1
docker run -it --name container1 -v /Users/lihao/code/docker/busybox:/databind busybox

这时,在宿主机上创建两个文件 file1.txtfile2.txt,再在容器内执行命令:

1
2
cd /databind
ls

可以同步看到刚创建的两个文件:

file1.txt file2.txt

对于 bind mount,有几点需要注意。

  • -v 宿主机目录路径必须以 /~/ 开头,否则 docker 会将其当成是 volume 而不是 bind mount
  • 如果宿主机上的目录不存在,docker 会自动创建该目录
  • 如果容器中的目录不存在,docker 会自动创建该目录
  • 如果容器中的目录已有内容,那么 docker 会使用宿主机上目录的内容覆盖容器目录的内容

Volume

与 bind mount 不同,volume 由 docker 创建和管理,docker 所有的 volume 都保存在宿主机文件系统的 /var/lib/docker/volumes 目录下(注意,mac 操作系统以虚拟机运行 docker,因此并不存在该目录,可以参考 stackoverflow)。

Docker 引入 volume 的原因有:

  • 删除容器时,volume 不会被删除
  • 在不同的容器之间共享 volume (存储 / 数据)
  • 容器与存储分离
  • 将 volume 存储在远程主机或云上

可以使用 docker run -v 参数为启动容器加载一个 volume,例如:

1
docker run -it -v /data --name container1 busybox

这样,我们就启动了一个名为 container1 的容器,由于使用了 -it 参数,我们以交互方式进行了容器内部。
这时,执行命令:

1
ls

可以看到容器中 /data 目录:

bin data dev etc home proc root sys tmp usr var

接着,我们在容器 /data 目录创建 file1.txt 文件:

1
2
3
cd data
touch file1.txt
exit

退出容器后,执行 docker ps -a 可以看到 container1 容器的退出状态。

现在,我们 inpect 一下容器,看下 docker 在启动容器时做了什么:

1
docker inspect container1

可以看到一大串 JSON 格式的输出,我们重点关注 Mounts 字段的输出。

1
2
3
4
5
6
7
8
9
10
11
12
"Mounts": [
{
"Type": "volume",
"Name": "9ee296c604fcb8e89c726b3ce07b55efe700763058c992821a81ca1c30f11cc5",
"Source": "/var/lib/docker/volumes/9ee296c604fcb8e89c726b3ce07b55efe700763058c992821a81ca1c30f11cc5/_data",
"Destination": "/data",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],

可以看到,当容器 container1 加载一个 volume (/data)时,docker 在目录 /var/lib/docker/volumes/ 创建一个新的目录,用来存储容器中产生的文件。同时,我们注意到 /dataRW 属性为 true,即可读可写。

我们重新启动 container1 容器,并进入容器:

1
2
docker restart container1
docker attach container1

在容器执行命令:

1
2
3
ls
cd /data
ls

可以看到 /data 目录仍在:

bin data dev etc home proc root sys tmp usr var

之前创建的 file1.txt 文件也在:

file1.txt

这说明,即使容器关闭,之前在 volume 存储的文件仍然会保留下来。

然后,我们退出容器,并将容器 container1 删除。

1
docker rm container1

这时,使用命令 docker volume ls 查看当前的 volume,可以看到 9ee296c604fcb8e89c726b3ce07b55efe700763058c992821a81ca1c30f11cc5 volume 仍然存在。也就是说,即使容器不存在了,volume 仍可在宿主机上保存下来。

local 9ee296c604fcb8e89c726b3ce07b55efe700763058c992821a81ca1c30f11cc5

如果需要在其他容器也使用这个 volume,可以使用以下命令加载指定的 volume:

1
docker run -it -v 9ee296c604fcb8e89c726b3ce07b55efe700763058c992821a81ca1c30f11cc5:/data busybox

可以使用 docker volume rm 删除指定的 volume。

挂载指定的 volume

上面的 docker run -v 命令中,我们并没有指定 volume 的名称,这样 docker 会默认给我们创建一个匿名的 volume。
我们也可以挂载指定名称的 volume:

1
docker run -it -v my-volume:/data --name container1 busybox

这样,我们在启动容器 container1 时,将挂载一个名为 my-volume 的 volume,并挂载到容器的 /data 目录。对于 docker 来说,如果 my-volume 不存在,那么 docker 就会自动创建该 volume,并挂载到 /data 目录。

退出容器后,执行

1
docker volume inspect my-volume

可以看到 my-volume 的 JSON 输出信息:

1
2
3
4
5
6
7
8
9
10
11
[
{
"CreatedAt": "2020-02-06T09:28:54Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/my-volume/_data",
"Name": "my-volume",
"Options": null,
"Scope": "local"
}
]

可以看到,名为 my-volume 的 volume 在宿主机的目录为 /var/lib/docker/volumes/my-volume/_data

除了让 docker 帮我们自动创建 volume,我们也可以自行创建 volume:

1
docker volume create my-volume2

然后将这个手工创建的 volume 挂载到容器:

1
docker run -it -v my-volume2:/data --name container1 busybox

参考资料