理解DockerFile中的“VOLUME”指令

312

以下是我的"Dockerfile"的内容

FROM node:boron

# Create app directory
RUN mkdir -p /usr/src/app

# Change working dir to /usr/src/app
WORKDIR /usr/src/app

VOLUME . /usr/src/app

RUN npm install

EXPOSE 8080

CMD ["node" , "server" ]

在这个文件中,我期望使用VOLUME . /usr/src/app指令将主机当前工作目录的内容挂载到容器的/usr/src/app文件夹上。

请告诉我这是否正确?


1
太长不看版(tldr):不需要在Dockerfile中使用VOLUME命令,只需运行docker run -it -v /host/path:/container/path containername bash命令即可。 - Roland Puntaier
7个回答

538
简而言之,不,你的VOLUME指令是不正确的。Dockerfile的VOLUME指定一个或多个容器端路径的卷。但它不允许镜像作者指定主机路径。在主机端,卷是在Docker根目录内创建的具有类似ID的非常长的名称。在我的机器上这是/var/lib/docker/volumes。请注意:由于自动生成的名称非常长,从人类的角度来看毫无意义,因此这些卷通常被称为“未命名”或“匿名”。你使用'.'字符的示例甚至在我的机器上都无法运行,无论我将点作为第一个参数还是第二个参数。我收到以下错误消息:docker: Error response from daemon: oci runtime error: container_linux.go:265: starting container process caused "process_linux.go:368: container init caused "open /dev/ptmx: no such file or directory""。我知道到目前为止所说的可能对试图理解VOLUME-v的人没有太大价值,当然也没有提供解决方案。因此,希望以下示例能更好地阐明这些问题。

迷你教程:指定卷

给出此Dockerfile:
FROM openjdk:8u131-jdk-alpine
VOLUME vol1 vol2

(对于这个迷你教程的结果,如果我们指定vol1 vol2 或 / vol1 / vol2 是没有区别的-因为Dockerfile中默认工作目录是/)

构建它:

docker build -t my-openjdk

运行:

docker run --rm -it my-openjdk

在容器中,通过命令行运行 ls ,你会注意到有两个目录存在: /vol1/vol2
运行该容器还会在主机端创建两个目录或“卷”。
当容器正在运行时,在 主机机器 上执行 docker volume ls ,你会看到类似于以下内容(为了简洁起见,我用三个点代替了名称的中间部分):
DRIVER    VOLUME NAME
local     c984...e4fc
local     f670...49f0

回到容器中,执行touch /vol1/weird-ass-file(在该位置创建一个空文件)。
现在,在未命名的卷中,此文件可在主机上使用。因为我第一次尝试使用列出的第一个卷,所以我尝试了两次才找到我的文件,最终我使用以下命令在主机上找到了我的文件:
sudo ls /var/lib/docker/volumes/f670...49f0/_data

同样地,你可以尝试在主机上删除此文件,并且容器中的文件也会被删除。
注意:`_data` 文件夹也称为“挂载点”。
退出容器并列出主机上的卷。它们已经消失了。我们在运行容器时使用了 `--rm` 标志,该选项有效地在退出时清除了容器和卷。
运行一个新的容器,但使用 `-v` 指定一个卷:
docker run --rm -it -v /vol3 my-openjdk

这会增加一个第三个卷,整个系统最终拥有三个未命名的卷。如果我们只指定-v vol3,则该命令将崩溃。参数必须是容器内的绝对路径。在主机端,新的第三个卷是匿名的,并与其他两个卷一起驻留在/var/lib/docker/volumes/中。

早些时候曾经提到过,在运行时无法将Dockerfile映射到主机路径,这给我们从主机向容器中传输文件带来了问题。不同的-v语法解决了这个问题。

假设我在我的项目目录中有一个子文件夹./src,我希望将其与容器内的/src同步。这条命令可以解决:

docker run -it -v $(pwd)/src:/src my-openjdk
:字符的两侧都需要绝对路径。左侧是主机上的绝对路径,右侧是容器内的绝对路径。pwd是一个命令,可以“打印当前/工作目录”。将该命令放在$()中,将命令置于括号内,在子shell中运行它,并返回至我们的项目目录的绝对路径。
将所有内容结合起来,假设我们在主机上的项目文件夹中有./src/Hello.java,其内容如下:
public class Hello {
    public static void main(String... ignored) {
        System.out.println("Hello, World!");
    }
}

我们构建了这个Dockerfile:
FROM openjdk:8u131-jdk-alpine
WORKDIR /src
ENTRYPOINT javac Hello.java && java Hello

我们运行这个命令:
docker run -v $(pwd)/src:/src my-openjdk

这将打印出 "Hello, World!"。

最好的部分是,我们完全可以自由地修改 .java 文件以获得第二次运行的另一个输出的新消息 - 而无需重新构建镜像 =)

最后的话

我对 Docker 还很陌生,上述的“教程”反映了我从一次为期三天的命令行黑客马拉松中收集到的信息。我几乎感到惭愧,因为我没有能够提供支持我的陈述的清晰英文文档的链接,但我真诚地认为这是由于缺乏文档而不是个人努力造成的。我知道这些示例在使用我当前的设置“Windows 10 -> Vagrant 2.0.0 -> Docker 17.09.0-ce”时可以按照广告所说的那样工作。

该教程未解决“我们如何在 Dockerfile 中指定容器路径并让运行命令仅指定主机路径”的问题。可能有一种方法,只是我还没有找到。

最后,我有一种直觉,即在 Dockerfile 中指定 VOLUME 不仅不常见,而且可能是永远不要使用 VOLUME 的最佳实践。有两个原因。我们已经确定了第一个原因:我们无法指定主机路径 - 这是一件好事,因为 Dockerfiles 应该对主机机器的具体细节非常不感兴趣。但第二个原因是人们可能会忘记在运行容器时使用 --rm 选项。有些人可能会记得删除容器,但忘记删除卷。此外,即使是最好的人类记忆,也可能很难弄清楚哪些匿名卷是安全可删除的。


7
我们什么情况下应该使用未命名/匿名卷? - Searene
33
@Martin非常感谢您。非常感谢您的黑客马拉松和其产生的教程在这里。 - Beezer
16
“我一直没有能够提供清晰易懂的英文文档链接……我想这是因为缺乏文档。” 我可以确认。 这是我找到的最全面和最新的文档,我已经找了几个小时了。 - user697576
9
docker volume prune 可用于清理未附加到运行容器的残留卷。不过,仅通过 ID 区分潜在重要的卷并不容易... - Jeremy
9
对于这个小教程的结果,无论我们指定vol1 vol2还是/vol1 /vol2都没有影响 - 别问我为什么。@MartinAndersson 这是因为当前工作目录是/,所以vol1是相对于/的,解析为/vol1。如果您使用WORKDIR指定除/之外的工作目录,则vol1/vol1不再指向同一目录。 - sebastian
显示剩余8条评论

177
官方 Docker 教程说:
数据卷是一个或多个容器内部的特定指定目录,可以绕过联合文件系统。数据卷为持久性或共享数据提供了几个实用功能:
- 创建容器时会初始化数据卷。如果容器的基础镜像在指定挂载点包含数据,则在初始化数据卷时将该现有数据复制到新数据卷中。(注意当挂载主机目录时不适用。) - 数据卷可在多个容器之间共享和重用。 - 直接对数据卷进行更改。 - 升级镜像时不会包括数据卷中的更改。 - 即使删除容器,数据卷也会被保留。
在 Dockerfile 中,您只能指定容器内部的数据卷目标位置,例如 /usr/src/app。
运行容器时,例如 docker run --volume=/opt:/usr/src/app my_image,您可以但不必指定其在主机上的挂载点(/opt)。如果未指定 --volume 参数,则通常会自动选择挂载点,通常位于 /var/lib/docker/volumes/ 下。

4
这个视频介绍了Docker中存储,解释得非常棒。 - user7075574
50
在Dockerfile中,您只能指定容器内卷的目标位置,例如 /usr/src/app。这是使我停止在此解决方案上投入更多时间所需阅读的确切内容。 - Dalmiro Granas
抱歉挖掘这个旧帖子。在 Docker 的 Volumes 文档中,它写道:"-v 或 --volume: [...] 第二个字段是文件或目录在 容器 中挂载的路径。" ... 根据您的解释,这不应该改为 "挂载在 主机 中" 吗? - AxD
这是关于Dockerfile的,你在提到docker CLI中的-v。CLI语法是-v source_path_on_localohst:destination_path_in_container,而Dockerfile指令VOLUME则是VOLUME destination_path_in_container(然后你可以用docker run指定source_path_on_localohst)。 - nnsense

107
在Dockerfile中指定一个VOLUME行会配置一些元数据到你的镜像上,但这些元数据的使用方式非常重要。
首先,这两行代码做了什么?
WORKDIR /usr/src/app
VOLUME . /usr/src/app

WORKDIR这一行会在目录不存在时创建该目录,并更新一些镜像元数据以指定所有相对路径,同时为像RUN这样的命令指定当前目录位置。 VOLUME这一行指定了两个卷,一个是相对路径.,另一个是/usr/src/app,它们恰好是同一个目录。通常VOLUME只包含单个目录,但也可以像你所做的那样包含多个目录,或者是JSON格式化的数组。

无法在Dockerfile中指定卷源:在Dockerfile中指定卷时经常出现的一个混淆点是,试图在构建镜像时匹配源和目标的运行时语法,这是不起作用的。 Dockerfile只能指定卷的目标。如果有人可以定义卷的源,那么这将是一个微不足道的安全漏洞,因为他们可以更新Docker Hub上的公共镜像,将根目录挂载到容器中,然后作为入口点的一部分启动容器中的后台进程,添加登录到/ etc / passwd,配置systemd在下次重启时启动比特币矿工,或搜索文件系统以将信用卡、社会保险号码和私钥发送到远程站点。

{{VOLUME行的作用是什么?}}正如所述,它设置了一些图像元数据,以表明镜像内的目录是一个卷。这些元数据如何使用?每次从该镜像创建容器时,Docker都会强制该目录成为一个卷。如果您在运行命令或组合文件中未提供卷,则Docker的唯一选择是创建匿名卷。这是一个本地命名卷,其名称具有长唯一ID,并且没有其他指示说明它被创建的原因或包含的数据(匿名卷是数据消失的地方)。如果覆盖卷,指向命名或主机卷,您的数据将进入那里。
{{VOLUME破坏了一些东西:}}在Dockerfile中定义卷后,无法禁用卷。更重要的是,Docker中的RUN命令是使用经典构建器的临时容器实现的。这些临时容器将获得一个临时的匿名卷。该匿名卷将用您镜像的内容初始化。来自RUN命令的容器内写操作将针对该卷进行。当RUN命令完成时,将保存对镜像的更改,但是会丢弃对匿名卷的更改。因此,我强烈建议不要在Dockerfile中定义VOLUME。这会导致下游用户的镜像出现意外行为,他们希望通过初始数据扩展镜像以达到卷位置。
{{应该如何指定卷?}}要指定要与镜像一起使用的卷,请提供一个docker-compose.yml文件。用户可以修改该文件以调整卷位置以适应其本地环境,并捕获其他运行时设置,例如发布端口和网络设置。
有人应该记录这个!他们已经做到了。Docker在Dockerfile文档中包含有关VOLUME使用的警告,并建议在运行时指定源:
  • 在Dockerfile内更改卷:如果任何构建步骤在声明卷后更改卷内的数据,则这些更改将被丢弃。

...

  • 主机目录在容器运行时声明:主机目录(挂载点)本质上是与主机相关的。这是为了保留镜像的可移植性,因为给定的主机目录不能保证在所有主机上都可用。出于这个原因,您不能在Dockerfile内挂载主机目录。 VOLUME指令不支持指定host-dir参数。您必须在创建或运行容器时指定挂载点。

随着buildkit的引入,在Dockerfile中定义VOLUME后跟RUN步骤的行为已经发生了变化。以下是两个示例。首先是Dockerfile:

$ cat df.vol-run 
FROM busybox

WORKDIR /test
VOLUME /test
RUN echo "hello" >/test/hello.txt \
 && chown -R nobody:nobody /test

接下来,不使用buildkit进行构建。注意RUN步骤中的更改将会丢失:

$ DOCKER_BUILDKIT=0 docker build -t test-vol-run -f df.vol-run .
Sending build context to Docker daemon  23.04kB
Step 1/4 : FROM busybox
 ---> beae173ccac6
Step 2/4 : WORKDIR /test
 ---> Running in aaf2c2920ebd
Removing intermediate container aaf2c2920ebd
 ---> 7960bec5b546
Step 3/4 : VOLUME /test
 ---> Running in 9e2fbe3e594b
Removing intermediate container 9e2fbe3e594b
 ---> 5895ddaede1f
Step 4/4 : RUN echo "hello" >/test/hello.txt  && chown -R nobody:nobody /test
 ---> Running in 2c6adff98c70
Removing intermediate container 2c6adff98c70
 ---> ef2c30f207b6
Successfully built ef2c30f207b6
Successfully tagged test-vol-run:latest

$ docker run -it test-vol-run /bin/sh
/test # ls -al 
total 8
drwxr-xr-x    2 root     root          4096 Mar  6 14:35 .
drwxr-xr-x    1 root     root          4096 Mar  6 14:35 ..
/test # exit

然后使用buildkit构建。请注意,来自RUN步骤的更改已被保留:

$ docker build -t test-vol-run -f df.vol-run .
[+] Building 0.5s (7/7) FINISHED                                                                         
 => [internal] load build definition from df.vol-run                                                0.0s
 => => transferring dockerfile: 154B                                                                0.0s
 => [internal] load .dockerignore                                                                   0.0s
 => => transferring context: 34B                                                                    0.0s
 => [internal] load metadata for docker.io/library/busybox:latest                                   0.0s
 => CACHED [1/3] FROM docker.io/library/busybox                                                     0.0s
 => [2/3] WORKDIR /test                                                                             0.0s
 => [3/3] RUN echo "hello" >/test/hello.txt  && chown -R nobody:nobody /test                        0.4s
 => exporting to image                                                                              0.0s
 => => exporting layers                                                                             0.0s
 => => writing image sha256:8cb3220e3593b033778f47e7a3cb7581235e4c6fa921c5d8ce1ab329ebd446b6        0.0s
 => => naming to docker.io/library/test-vol-run                                                     0.0s

$ docker run -it test-vol-run /bin/sh
/test # ls -al
total 12
drwxr-xr-x    2 nobody   nobody        4096 Mar  6 14:34 .
drwxr-xr-x    1 root     root          4096 Mar  6 14:34 ..
-rw-r--r--    1 nobody   nobody           6 Mar  6 14:34 hello.txt
/test # exit

70
为了更好地理解dockerfile中的"volume"指令,请让我们学习mysql官方docker文件实现中的典型卷用法。
VOLUME /var/lib/mysql

参考资料: https://github.com/docker-library/mysql/blob/3362baccb4352bcf0022014f67c1ec7e6808b8c5/8.0/Dockerfile

/var/lib/mysql 是 MySQL 默认的数据文件存储位置。

当您仅为测试目的运行测试容器时,可以不指定其挂载点,例如:

docker run mysql:8

如果没有在Dockerfile中指定volume指令,则mysql容器实例将使用默认的挂载路径。卷是在Docker根目录内以类似于ID的名称创建的,这称为“未命名”或“匿名”卷。在底层主机系统的文件夹 /var/lib/docker/volumes 中。

/var/lib/docker/volumes/320752e0e70d1590e905b02d484c22689e69adcbd764a69e39b17bc330b984e4

这样做非常方便快速进行测试而无需指定挂载点,但是仍然可以通过使用卷数据存储获得最佳性能,而不是使用容器层。

对于正式使用,您需要使用命名卷或绑定挂载来指定挂载路径,例如:

docker run  -v /my/own/datadir:/var/lib/mysql mysql:8
该命令将底层主机系统中的/my/own/datadir目录挂载为容器内的/var/lib/mysql目录。即使删除了容器,数据目录/my/own/datadir也不会自动删除。 mysql官方镜像的用法(请查看“Where to Store Data”部分)。 参考链接: https://hub.docker.com/_/mysql/

6
我非常喜欢你的解释。 - LukaszTaraszka
1
但是Docker仍然保存更改。此外,您可以设置挂载路径-v,无需在Dockerfile中设置卷。 - Alex78191
这是最好的答案,让它变得非常清晰。简洁明了,有例子支持。 - Kumar Manish
一个例子!多么清新明了 :)(认真的) - Vano
这么多教程,你的解释非常清晰易懂...你是个传奇... - Umair Ayub

59
Dockerfile中,VOLUME命令是相当合法、完全常规的,可以放心使用,并且在任何方面都没有被弃用。只需要理解它即可。
我们使用它来指向容器中应用程序经常写入的任何目录。我们不会仅仅因为想共享配置文件来使用VOLUME
该命令只需要一个参数;一个相对于容器内部的WORKDIR路径的文件夹路径。然后,docker将在其图形(/var/lib/docker)中创建一个卷,并将其挂载到容器中的文件夹上。现在容器就有了可以高性能地写入的位置。如果没有VOLUME命令,指定文件夹的写入速度将非常慢,因为现在容器正在使用其自身的copy on write策略。copy on write策略是存在卷的主要原因之一。
如果在VOLUME命令指定的文件夹上进行挂载,则该命令将不会运行,因为VOLUME仅在容器启动时执行,类似于ENV
基本上,使用VOLUME命令可以在不外部挂载任何卷的情况下获得性能。数据也可以在容器运行期间保存,而无需任何外部挂载。然后在准备好的时候,只需将某些东西挂载到其上即可。
一些很好的用例示例:
-日志
- 临时文件夹

一些不良的使用情况:
- 静态文件
- 配置文件
- 代码


7
关于Dockerfile的最佳实践,Docker官网上的页面建议使用VOLUME来处理任何可变和/或用户可修改的镜像部分。我认为这其中包括配置文件。 - OmerSch
3
在配置文件中明确指定VOLUME目录是可以的。但是,一旦您实际挂载了一个配置文件,您将不得不在该目录上进行挂载,因此VOLUME命令不会运行。因此,在为配置文件指定的目录上使用VOLUME命令是没有意义的。此外,使用单个静态只读文件初始化卷图表是严重的过度设计。因此,我坚持我的观点,不需要在配置文件上使用VOLUME命令。 - mr haven
卷由于实现细节可能带来不同的性能特征。数据库数据文件适用于此用例,但将数据存储在(短暂的)容器存储旁边有什么意义呢?即将卷的存在归因于性能是不正确的。 - André Werlang
+1;为什么要使用 VOLUME(以及它存在的原因)的唯一有效理由 - zeawoas

23

除非您正在为自己创建映像并且没有其他人会使用它,否则我不认为在任何情况下使用VOLUME是好的。

由于基础映像中暴露的VOLUME,我受到了负面影响,而我扩展了这些映像,并且只有在映像已经运行后才知道了问题,例如WordPress将/var/www/html文件夹声明为VOLUME,这意味着在构建阶段添加或更改的任何文件都不会被考虑,实时更改会持久存在,即使您不知道。虽然有一个丑陋的解决方法可以在其他地方定义Web目录,但这只是更简单的解决方案的不良解决方法:只需删除VOLUME指令即可。

您可以使用-v选项轻松实现卷的意图,这不仅清楚地说明容器的卷是什么(无需查看Dockerfile和父Dockerfile),而且还使消费者有选择使用卷与否的选项。

使用VOLUMES也是不好的,原因如此答案所述:

然而,VOLUME指令确实会付出代价。
  • 用户可能不知道未命名的卷在容器删除后继续占用Docker主机的存储空间。
  • 无法删除在Dockerfile中声明的卷。下游镜像不能向存在卷的路径添加数据。
后面这个问题会导致以下问题: 有选择地取消声明卷将会有所帮助,但前提是你需要了解生成图像的Dockerfile(以及父Dockerfiles!)。此外,新版本的Dockerfile可能会添加VOLUME并出人意料地破坏图像使用者的东西。 另一个很好的解释(关于oracle镜像有VOLUME, 被 删除): https://github.com/oracle/docker-images/issues/640#issuecomment-412647328

更多 VOLUME 导致问题的案例:

有一个拉取请求,添加了重置父镜像属性(包括VOLUME)的选项,已经关闭并正在这里讨论(您可以看到几个 受到在dockerfile中定义卷的负面影响的案例),其中有一条评论反对使用VOLUME并给出了充分的解释:

在Dockerfile中使用VOLUME是没有意义的。如果用户需要持久性,他们一定会在运行指定容器时提供卷映射。很难追踪到我的问题是由于InfluxDB的Dockerfile中的VOLUME声明导致无法设置目录所有权(/var/lib/influxdb)。没有UNVOLUME类型的选项,或者完全摆脱它,我无法更改与指定文件夹相关的任何内容。这不是理想的情况,特别是当您关注安全并希望指定镜像应以某个UID运行时,为了避免一个随机的用户,拥有比必要权限更多的权限,在您的主机上运行软件。

我唯一能看到关于VOLUME的好处就是文档,如果它只做到这一点(没有任何副作用),我会考虑它是好的。

更新(2021-10-19)

mysql官方镜像还有一个相关问题: https://github.com/docker-library/mysql/issues/255

更新(2022-01-26)

我找到了一篇很好的文章解释VOLUME的问题。它已经几年了,但是同样的问题仍然存在:

https://boxboat.com/2017/01/23/volumes-and-dockerfiles-dont-mix/

TL;DR

我认为VOLUME最好的用法是被弃用。


1
这就像全局变量一样。通过副作用来自毁长城的完美方式。 - SerG
关于使用示例的好坏,Docker的“Dockerfile最佳实践”页面指出:“强烈建议您对镜像中任何可变和/或用户可维护的部分使用VOLUME。” - colynn liu
@colynnliu 是的,我知道这一点,但是正如预期的那样,没有解释为什么这是好的。因为实际上它并不好(至少在其他人应该使用的图像中),而且你可以从我的帖子中看到它有多糟糕,特别是考虑到 你可以轻松地在docker run中映射卷并满足你发布的用例。我能想到的一个例外是多阶段构建中的中间图像,因为这不会影响镜像的消费者。你可以看看我上次发布链接的作者在这个SO答案中的回答:https://dev59.com/eFcP5IYBdhLWcg3worn_#44060560 - Lucas Basquerotto

1
尽管这是一篇非常旧的文章,但如果您在volumebind mounts之间存在一些困惑,我仍然希望您能查看最新的docker官方文档。
自从Docker诞生以来,Bind mounts就一直存在着。但我认为这也不是一个完美的设计,例如“绑定挂载允许访问敏感文件”,而且您可以从docker官方获取信息,他们更喜欢您使用VOLUME而不是bind mounts
您可以从这里了解关于卷的好用例。
参考资料:
- docker volume docs - docker storage overview

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接