当我同时使用主机卷(绑定装载)和命名卷(一种类型的 docker 托管卷)时发生了什么?

What happened when I use a host volume(bind mounts) and named volume(one type of docker managed volume) at the same time?

在问这个问题之前我已经阅读了一些文档:

我对以下两部分感到困惑:

  1. 绑定坐骑:Mounting into a non-empty directory on the container
  2. 命名卷:Populate a volume using a container

他们的行为完全相反:

对于绑定安装:

If you bind-mount into a non-empty directory on the container, the directory’s existing contents are obscured by the bind mount.

对于命名卷和匿名卷:

If you start a container which creates a new volume, as above, and the container has files or directories in the directory to be mounted (such as /app/ above), the directory’s contents are copied into the volume. The container then mounts and uses the volume, and other containers which use the volume also have access to the pre-populated content.

我的问题是,如果我在 docker 文件中使用 VOLUME 命令创建一个 命名卷 匿名卷,同时,使用绑定挂载将不存在的路径挂载到容器中,幕后发生了什么?

例如:

# in docker file
VOLUME /path/in/container

# when run container
docker run -v /not-exist-dir/in/host:/path/in/container  ... /< image >

我做了一些测试,结果是:

  1. /var/lib/docker/volumes/
  2. 中没有创建匿名卷
  3. 在主机上创建了not-exist-dir,现在不是空的,我想Populate a volume using a container已经生效了。

所以,

  1. 为什么没有按预期创建匿名卷?
  2. 为什么目录的现有内容(在容器端)没有被绑定挂载遮挡?

这种情况背后发生了什么?


让我举一个更具体的例子:

这里是dockerfile of the openfrontier/gerrit image,我们可以看到在docker文件的末尾,有一个VOLUME命令:

VOLUME $GERRIT_SITE

这实际上会在主机上创建一个匿名卷并将其挂载到容器中的 /var/gerrit/review_site(GERRIT_SITE 的值),当任何容器从这个图像创建时,如下所示:

docker run -dit --name gerrit  openfrontier/gerrit:2.15.3

之后,我可以在 /var/lib/docker/volumes/ 下看到匿名卷,我可以使用 docker volume ls 查看它的名称 be4538dbf3a51da463391c6eca9714fb6dd0c11379f1e2918f74c33d56633f00,也可以使用 docker inspect gerrit 命令,我们可以看到:

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

这两个文件夹下还有一些文件,是运行ning之后的容器创建的。

到目前为止,这是一个使用VOLUME命令创建匿名卷的正常示例。

但是,如果我运行这个容器用下面的命令:

docker run -dit --name gerrit -v /home/test:/var/gerrit/review_site openfrontier/gerrit:2.15.3

where /home/test is not existed on the host, then no anonymous volumes are created and it is not empty!!!,挂载信息如下:

"Mounts": [
            {
                "Type": "bind",
                "Source": "/home/test",
                "Destination": "/var/gerrit/review_site",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }
        ],

我认为bind mounts在这里生效,但我只是不明白为什么主机上的路径不为空(因为bind-mount到容器上的非空目录,该目录的现有内容被绑定安装。)

在使用docker run启动容器之前,我还通过在“/home/test”下放置一些文件来测试将一个非空文件夹安装到容器中,结果是原始文件被保留并且也添加了新文件。

我知道上面的例子不是使用 docker 卷的好习惯,但我只是想知道后面发生了什么。

Dockerfile 中的卷声明在图像上设置了一些元数据,告诉 docker run 在任何时候从该图像创建容器时在该位置定义一个匿名卷。这不是命名卷或主机安装,但它与两者有很多共同点(默认情况下它们都是绑定安装,匿名卷与命名卷一起列在 docker volume ls 中)。

但是,当您指定将主机挂载到同一个容器目录时,这会优先(不会将两个卷挂载到容器内的同一目录)。通过容器内的命令 运行 对该目录所做的任何修改都将在主机上可见,但目录本身不会被图像内容初始化。

如果要用映像内容初始化主机目录,可以使用执行绑定安装的命名卷。与主机挂载的行为有一些差异。主要是目录必须提前存在,而且在compose文件里面必须用绝对路径,不能用相对路径。我在此处的演示文稿中提供了语法:

https://sudo-bmitch.github.io/presentations/dc2018eu/tips-and-tricks-of-the-captains.html#48

你的问题有些混乱,因为它并不总是直观的,我会尝试通过示例使它更清楚。

备注:

  • 这是我为我的同事写的关于卷的小教程。解释肯定有瑕疵,但是例子都经过测试了。
  • 重要的是你 运行 按以下顺序排列事物,而不是重新 运行 命令(因为 "new" 事物可能会变成 "existing" 并且与他们的不匹配目的)
  • 我将挂载到某个系统路径...这仅用于示例目的:确保它们存在以及避免增加初步创建命令。

Dockerfile

FROM ubuntu

VOLUME /tmp/dockerfilevolumefromnowhere

RUN mkdir -p /dir/created/from/container
RUN touch /dir/created/from/container/emptyFile.txt
VOLUME /dir/created/from/container

CMD "sh"

让我们删除所有以前的测试数据

请注意,它会删除所有不再使用的卷,包括您之前创建的卷。

如果您有任何 运行ning 容器(特别是如果它们使用卷),请停止它们以进行这些测试。

root@host:~# docker container prune 
root@host:~# docker volume prune 
root@host:~# rm -r /does/not/exit/within/host
root@host:~# rm -r /does/not/exit/within/host2
root@host:~# rm -r /tmp/dockerfilevolumefromnowhere

一些图像和体积创建和填充

root@host:~# docker build -t tmpcontainer .
root@host:~# docker volume create existingVolume
root@host:~# touch /var/lib/docker/volumes/existingVolume/_data/someExistingFile.txt
root@host:~# docker volume create existingVolume2
root@host:~# touch /var/lib/docker/volumes/existingVolume2/_data/anotherExistingFile.txt
root@host:~# docker volume create existingEmptyVolume

此时你有3"existing volumes":

root@host:~# docker volume ls
DRIVER              VOLUME NAME
local               existingVolume
local               existingVolume2
local               existingEmptyVolume

现在让我们运行我们的容器

docker run -it \
-v /does/not/exit/within/host:/does/not/exist/within/container \
-v /does/not/exit/within/host2:/sbin \
-v /tmp:/again/another/does/not/exist/within/container \
-v /tmp:/tmp/ \
-v newVolume:/another/does/not/exist/within/container \
-v newVolume2:/bin \
-v existingVolume:/new/path/on/container \
-v existingVolume2:/usr \
-v existingEmptyVolume:/var \
bash: groups: command not found # that's normal. you overrided the /usr... see beyond

现在,我们已连接到新创建的容器。

让我们看看我们在容器和主机上都有什么 :

# -v /does/not/exit/within/host:/does/not/exist/within/container 
#no one exists on both sides : both directories are created, and are now bound to each other
root@host:~# ll /does/not/exit/within/host
. 
root@ffb82b56d64b:/# ll /does/not/exist/within/container
.

# -v /does/not/exit/within/host:/sbin 
# the directory on host is created, and the one on container is erased with this new one. Both are now bound to each other
root@host:~# ll /does/not/exit/within/host2
. 
root@ffb82b56d64b:/# ll /sbin
.


# -v /tmp:/again/another/does/not/exist/within/container
# the path on host exists, and the path on container will be created and will hold the content on container (they are bound to each other)
root@host:~# ll /tmp
    -rw-------  1 root root 65536 Feb 19 11:11 one.txt
    -rw-------  1 root root 65536 Feb 19 11:11 two.yml
root@ffb82b56d64b:/# ll /again/another/does/not/exist/within/container
    -rw-------  1 root root 65536 Feb 19 11:11 one.txt
    -rw-------  1 root root 65536 Feb 19 11:11 two.yml

# -v /tmp:/tmp
# the path on host exists, so all its content will replace the previously path on container
# there were some data on container's /tmp, but they are replaced with hosts ones
root@host:~# ll /tmp
    -rw-------  1 root root 65536 Feb 19 11:11 one.txt
    -rw-------  1 root root 65536 Feb 19 11:11 two.yml
root@ffb82b56d64b:/# ll /tmp
    -rw-------  1 root root 65536 Feb 19 11:11 one.txt
    -rw-------  1 root root 65536 Feb 19 11:11 two.yml

# -v newVolume:/another/does/not/exist/within/container \
# the newVolume does not exist, so it will be created and bound to path on container.  
# since the path on container is new, it will be created empty
root@host:~#  ll /var/lib/docker/volumes/newVolume/_data/
..
root@ffb82b56d64b:/# ll /another/does/not/exist/within/container
..

# -v newVolume2:/bin \
# once again, a volume will be created, but since it matches an existing path on container, it will hold all the content of it (no erasal!)
root@host:~#  ll /var/lib/docker/volumes/newVolume2/_data/
<all the content of Ubuntu's /bin from within container>
root@ffb82b56d64b:/# ll /bin
<whole expected content of /bin on Ubuntu>

# -v existingVolume:/new/path/on/container \
# the volume exists, and it -and all files within- will be bound to a newly created path on container
root@host:~#  ll /var/lib/docker/volumes/existingVolume/_data/
-rw-------  1 root root 65536 Feb 19 11:11 someExistingFile.txt
 root@ffb82b56d64b:/# ll /new/path/on/container
-rw-------  1 root root 65536 Feb 19 11:11 someExistingFile.txt


# -v existingVolume2:/usr \ 
# the volume exists, so does the path on container. It will replace the existing path (and thus erase the former files there) and will be bound to this replaced path.
root@host:~#  ll /var/lib/docker/volumes/existingVolume2/_data/
-rw-------  1 root root 65536 Feb 19 11:11 anotherExistingFile.txt
root@ffb82b56d64b:/# ll /usr    
-rw-------  1 root root 65536 Feb 19 11:11 anotherExistingFile.txt

# -v existingEmptyVolume:/var \ 
# the volume exists, but is empty. the path on container exists in the container and is not empty. In this case, the path on container will not be erased and this will act as a new volume.
root@host:~#  ll /var/lib/docker/volumes/existingEmptyVolume/_data/
drwxr-xr-x  2 root root  4096 Apr 24  2018 backups/
drwxr-xr-x  5 root root  4096 Feb 19 14:36 cache/
drwxr-xr-x  7 root root  4096 Feb 19 14:36 lib/
drwxrwsr-x  2 root staff 4096 Apr 24  2018 local/
lrwxrwxrwx  1 root root     9 Nov 12 21:54 lock -> /run/lock/
drwxr-xr-x  3 root root  4096 Feb 19 14:36 log/
drwxrwsr-x  2 root mail  4096 Nov 12 21:54 mail/
drwxr-xr-x  2 root root  4096 Nov 12 21:54 opt/
lrwxrwxrwx  1 root root     4 Nov 12 21:54 run -> /run/
drwxr-xr-x  2 root root  4096 Feb 19 14:36 spool/
drwxrwxrwt  2 root root  4096 Nov 12 21:56 tmp/
root@ffb82b56d64b:/# ll /var  
drwxr-xr-x  2 root root  4096 Apr 24  2018 backups/
drwxr-xr-x  5 root root  4096 Feb 19 14:36 cache/
drwxr-xr-x  7 root root  4096 Feb 19 14:36 lib/
drwxrwsr-x  2 root staff 4096 Apr 24  2018 local/
lrwxrwxrwx  1 root root     9 Nov 12 21:54 lock -> /run/lock/
drwxr-xr-x  3 root root  4096 Feb 19 14:36 log/
drwxrwsr-x  2 root mail  4096 Nov 12 21:54 mail/
drwxr-xr-x  2 root root  4096 Nov 12 21:54 opt/
lrwxrwxrwx  1 root root     4 Nov 12 21:54 run -> /run/
drwxr-xr-x  2 root root  4096 Feb 19 14:36 spool/
drwxrwxrwt  2 root root  4096 Nov 12 21:56 tmp/

请注意,现在,以前存在的和新创建的卷可以用于其他容器(与您刚刚在上面使用以前存在的容器的方式相同)。

最终,我们剩下的所有用例都是从 Dockerfile 创建的 卷。 我们来看/var/lib/docker/volumes的内容。请记住,我们在执行测试之前已经对其进行了清理,所以这里的所有内容都符合我们的测试。

root@host:~# ll /var/lib/docker/volumes
drwx------  6 root root  4096 Feb 19 11:11 ./
drwx--x--x 14 root root  4096 Feb 14 09:50 ../
-rw-------  1 root root 65536 Feb 19 14:36 metadata.db #indeed 
drwxr-xr-x  3 root root  4096 Feb 19 14:36 635af95ec06f8a44c22915005189bfb12d5bcf2e5ac97c25112d3e65a72546f4/ # anonymous container 1
drwxr-xr-x  3 root root  4096 Feb 19 14:36 897b28ec17275a3c3be184bb20b6314b38c1404e638080c8fe4fc36cae2f9f65/ # anonymous container 2
drwxr-xr-x  3 root root  4096 Feb 19 14:36 existingEmptyVolume/ #we created it (empty) before the run command
drwxr-xr-x  3 root root  4096 Feb 19 14:35 existingVolume/ #we created it before the run command
drwxr-xr-x  3 root root  4096 Feb 19 14:36 existingVolume2/ #we created it before the run command
drwxr-xr-x  3 root root  4096 Feb 19 14:36 newVolume/ #we created it during run command
drwxr-xr-x  3 root root  4096 Feb 19 14:36 newVolume2/ #we created it during run command

那么那些匿名容器是什么:它们实际上是从您的 docker 文件中创建的。 让我们检查一下它们的内容。

root@host:~#  ll /var/lib/docker/volumes/63eeedcb1aa2e4d8785cca409698371381558348ce19bc614d87da372901d224/_data/
-rw-r--r-- 1 root root    0 Feb 19 10:14 emptyFile.txt
root@host:~#  ll /var/lib/docker/volumes/ea0ed5ff271cba03a8b7d35144b58e8da1b2e50b4e05c4cccda7f19b401d7f0b/_data/
..

看,它们保存着您从 docker 文件中放入其中的任何内容。 它们是匿名的,所以你不能(或者实际上不能)从其他容器中使用它们,因为每次你从这个图像中 运行 一个容器时都会创建新的。

注意 : 这时候你可能已经注意到了一些奇怪的事情:

root@host:~# ll /tmp/dockerfilevolumefromnowhere #YES : from HOST!
.. #it exists... but is not linked to the volume or the path in container in any way.

这里...别问了:为什么要在主机上创建它仍然让我感到困惑。我想这是一个错误,但这个问题应该在 docker ML 上讨论,而不是在这里讨论。 无论如何,你不应该那样做:在创建卷之前 mkdir!

那么我们可以从所有这些例子中看到什么:

  • 从主机 path 绑定/挂载到容器 (/some/path/on/host:/a/path/on/container) 将在容器上创建路径(无论它以前是否存在)并在此处替换所有以前的数据。两个目录(及其内容)现在相互绑定。
  • volume (someVolumeName:/a/path/on/container) 挂载到 container ,会将卷绑定到容器上的路径。
  • 现有的非空卷挂载到容器的路径将用现有卷中的内容替换容器中的(可能的)内容(如果为空:它将执行作为新卷)
  • 新卷挂载到容器的路径会将容器中路径的内容与卷绑定。该卷现在将保存此内容(例如供以后使用):此处不会删除/替换任何内容。
  • 如果卷或路径(无论是在容器还是主机上)都将创建(如果不存在)。
  • 来自 Dockerfile 的卷将是匿名的,并且每次您从该映像 运行 另一个容器时重新创建(以前的容器将保留)。它们也将在 /var/lib/docker/volumes 下创建。
  • 在 运行 命令中声明的卷将被创建(即,将是 "new" 卷)如果它们以前不存在。否则,将使用现有的。
  • 通过 运行 命令创建的卷 将保存在 /var/lib/docker/volumes 与其名称匹配的目录下。

注意:我在这里不是在谈论所有权和权利。故意的:那是我们以后可能会讨论的另一件事。