以下是我的“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
文件夹中。
请让我知道这是否是正确的方法?
简而言之:不,您的 VOLUME
指令不正确。
Dockerfile 的 VOLUME
指定一个或多个给定容器端路径的卷。但它不允许图像作者指定主机路径。在主机端,卷是在 Docker 根目录中使用一个非常长的类似 ID 的名称创建的。在我的机器上这是 /var/lib/docker/volumes
。
注意:由于自动生成的名称非常长,从人类的角度来看毫无意义,因此这些卷通常被称为“未命名”或“匿名”。
您使用“。”的示例无论我将点作为第一个参数还是第二个参数,字符甚至都不会在我的机器上运行。我收到此错误消息:
docker:来自守护进程的错误响应:oci 运行时错误:container_linux.go:265:启动容器进程导致“process_linux.go:368:容器初始化导致“打开/dev/ptmx:没有这样的文件或目录””。
我知道到目前为止所说的对于试图理解 VOLUME
和 -v
的人来说可能不是很有价值,而且它当然不能为您尝试完成的任务提供解决方案。因此,希望以下示例能够进一步阐明这些问题。
Minitutorial:指定卷
鉴于此 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 很陌生,前面提到的“教程”反映了我从为期 3 天的命令行黑客马拉松收集的信息。我几乎很惭愧,我无法提供链接到清晰的类似英语的文档来支持我的陈述,但老实说,我认为这是由于缺乏文档而不是个人努力。我确实知道这些示例使用我当前的设置(即“Windows 10 -> Vagrant 2.0.0 -> Docker 17.09.0-ce”)进行宣传。
教程并没有解决“我们如何在Dockerfile中指定容器的路径,让run命令只指定主机路径”的问题。可能有办法,只是没找到。
最后,我有一种直觉,在 Dockerfile 中指定 VOLUME
不仅不常见,而且从不使用 VOLUME
可能是最佳实践。有两个原因。我们已经确定的第一个原因:我们不能指定主机路径——这是一件好事,因为 Dockerfile 应该与主机的细节非常无关。但第二个原因是人们在运行容器时可能会忘记使用 --rm
选项。人们可能记得删除容器但忘记删除卷。另外,即使人类记忆力最好,找出所有匿名卷中的哪一个可以安全删除也可能是一项艰巨的任务。
官方 docker 教程说:
数据卷是一个或多个容器中的一个特别指定的目录,它绕过了联合文件系统。数据卷为持久性或共享数据提供了几个有用的特性: 卷在创建容器时被初始化。如果容器的基础映像包含指定挂载点的数据,则在卷初始化时,现有数据将复制到新卷中。 (请注意,这不适用于挂载主机目录。)数据卷可以在容器之间共享和重用。直接对数据卷进行更改。更新映像时不会包括对数据卷的更改。即使容器本身被删除,数据卷仍然存在。
在 Dockerfile
中,您只能指定容器内部的卷的目的地。例如/usr/src/app
。
当您运行容器(例如 docker run --volume=/opt:/usr/src/app my_image
)时,您可以但不必在主机上指定其安装点 (/opt
)。如果您不指定 --volume
参数,则将自动选择安装点,通常在 /var/lib/docker/volumes/
下。
In Dockerfile you can specify only the destination of a volume inside a container. e.g. /usr/src/app.
这正是我需要阅读以停止在此解决方案上投入更多时间的确切内容
-v
。 CLI 语法为 -v source_path_on_localohst:destination_path_in_container
,Dockerfile 指令 VOLUME
改为 VOLUME destination_path_in_container
(然后您使用 docker run 指定 source_path_on_localohst
)
在 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 以在下次重新启动时启动比特币矿工,或在文件系统中搜索信用卡、SSN 和私钥以发送到远程站点。
VOLUME 线有什么作用?如前所述,它设置了一些图像元数据来表示图像内的目录是一个卷。如何使用此元数据?每次你从这个镜像创建一个容器时,docker 都会强制该目录成为一个卷。如果您没有在运行命令或撰写文件中提供卷,则 docker 的唯一选择是创建一个匿名卷。这是一个本地命名卷,其名称具有一个长的唯一 ID,并且没有其他说明创建它的原因或它包含哪些数据(匿名卷是数据丢失的情况)。如果您覆盖该卷,指向一个命名或主机卷,您的数据将转到那里。
VOLUME 破坏了事情:一旦在 Dockerfile 中定义了卷,就不能禁用它。更重要的是,docker 中的 RUN
命令是使用带有经典构建器的临时容器实现的。这些临时容器将获得一个临时匿名卷。该匿名卷将使用您的图像内容进行初始化。您的 RUN
命令对容器内的任何写入都将写入该卷。 RUN
命令完成后,会保存对映像的更改,对匿名卷的更改将被丢弃。因此,我强烈建议不要在 Dockerfile 中定义 VOLUME
。对于希望使用卷位置中的初始数据扩展映像的映像下游用户,这会导致意外行为。
应如何指定卷?要指定要在映像中包含卷的位置,请提供 docker-compose.yml
。用户可以修改它以将卷位置调整到他们的本地环境,它会捕获其他运行时设置,如发布端口和网络。
应该有人记录下来!他们有。 Docker 在其 documentation on the 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
为了更好地理解dockerfile中的volume
指令,让我们学习一下mysql官方docker文件实现中典型的volume使用。
VOLUME /var/lib/mysql
参考:https://github.com/docker-library/mysql/blob/3362baccb4352bcf0022014f67c1ec7e6808b8c5/8.0/Dockerfile
/var/lib/mysql
是 MySQL 存储数据文件的默认位置。
当您仅出于测试目的运行测试容器时,您可能不会指定其安装点,例如
docker run mysql:8
那么 mysql 容器实例将使用 dockerfile 中 volume
指令指定的默认挂载路径。这些卷是在 Docker 根目录中使用一个非常长的类似 ID 的名称创建的,这称为“未命名”或“匿名”卷。在底层主机系统 /var/lib/docker/volumes 的文件夹中。
/var/lib/docker/volumes/320752e0e70d1590e905b02d484c22689e69adcbd764a69e39b17bc330b984e4
这对于快速测试目的非常方便,无需指定挂载点,但仍然可以通过使用 Volume 进行数据存储而不是容器层来获得最佳性能。
对于正式使用,您需要使用命名卷或绑定挂载来指定挂载路径,例如
docker run -v /my/own/datadir:/var/lib/mysql mysql:8
该命令将底层主机系统的/my/own/datadir目录挂载为容器内的/var/lib/mysql。数据目录/my/own/datadir不会被自动删除,即使容器被删除。
mysql官方镜像的使用(请查看“数据存储位置”部分):
参考:https://hub.docker.com/_/mysql/
-v
使用它而不在 Dockerfile 中设置卷
Dockerfile
中的 VOLUME
命令非常合法,完全传统,绝对可以使用,而且无论如何都不会被弃用。只需要了解它。
我们使用它来指向容器中的应用程序将写入的任何目录。我们不使用 VOLUME
只是因为我们想像配置文件一样在主机和容器之间共享。
该命令只需要一个参数;容器内相对于 WORKDIR
的文件夹路径(如果已设置)。然后 docker 会在它的 graph(/var/lib/docker) 中创建一个卷,并将它挂载到容器中的文件夹中。现在容器将有一个高性能的地方可以写入。如果没有 VOLUME
命令,对指定文件夹的写入速度将非常慢,因为现在容器在容器本身中使用它的 copy on write
策略。 copy on write
策略是卷存在的主要原因。
如果您在 VOLUME
命令指定的文件夹上挂载,则该命令永远不会运行,因为 VOLUME
仅在容器启动时执行,有点像 ENV
。
基本上使用 VOLUME
命令,您无需外部安装任何卷即可获得性能。数据也将跨容器运行保存,无需任何外部挂载。然后,当准备好时,只需在上面安装一些东西。
一些很好的示例用例: - 日志 - 临时文件夹
一些不好的用例: - 静态文件 - 配置 - 代码
VOLUME
目录。但是,一旦您实际挂载了配置,您将不得不挂载该目录,因此 VOLUME
命令不会运行。因此,在为配置指定的目录上使用 VOLUME
命令是没有意义的。使用单个静态只读文件初始化卷图也是严重的矫枉过正。所以我坚持我所说的,不需要在配置上使用 VOLUME
命令。
VOLUME
的唯一答案(以及为什么它首先存在)
在任何情况下,我都不认为使用 VOLUME 是好的,除非您正在为自己创建一个图像并且没有其他人会使用它。
我受到了负面影响,因为 VOLUME 在我扩展的基础图像中暴露,并且只是在图像已经运行后才知道问题,例如将 /var/www/html
文件夹声明为 VOLUME 的 wordpress,这意味着任何不考虑在构建阶段添加或更改的文件,即使您不知道,实时更改也会持续存在。在另一个地方定义 web 目录有一个丑陋的解决方法,但这只是一个更简单的解决方案的糟糕解决方案:只需删除 VOLUME 指令。
您可以使用 -v
选项轻松实现卷的意图,这不仅可以明确容器的卷(无需查看 Dockerfile 和父 Dockerfile),而且还可以为消费者提供是否使用音量的选项。
正如 this answer 所说,由于以下原因,使用 VOLUMES 也很糟糕:
但是,VOLUME 指令确实是有代价的。用户可能不知道正在创建未命名的卷,并且在删除容器后继续占用其 Docker 主机上的存储空间。无法删除 Dockerfile 中声明的卷。下游映像无法将数据添加到存在卷的路径。后一个问题会导致这样的问题。如何在 docker 镜像中“取消声明”卷? Docker 上的 GitLab:如何在部署之间保留用户数据?
选择取消声明卷会有所帮助,但前提是您知道生成映像的 dockerfile 中定义的卷(以及父 dockerfile!)。此外,可以在 Dockerfile 的较新版本中添加 VOLUME,并为映像的使用者带来意外的破坏。
另一个很好的解释(about the oracle image having VOLUME,即 removed):https://github.com/oracle/docker-images/issues/640#issuecomment-412647328
更多 VOLUME 为人们破坏东西的案例:
https://github.com/datastax/docker-images/issues/31
https://github.com/docker-library/wordpress/issues/232
https://github.com/docker-library/ghost/issues/195
https://github.com/samos123/docker-drupal/issues/10
pull request 添加选项以重置父映像(包括 VOLUME)的属性,已关闭并正在讨论 here(您可以看到 people 的 several cases 由于 dockerfiles 中定义的卷而受到不利影响),它有一个 comment,对 VOLUME 有很好的解释:
在 Dockerfile 中使用 VOLUME 毫无价值。如果用户需要持久性,他们一定会在运行指定容器时提供卷映射。很难找到我无法设置目录所有权(/var/lib/influxdb)的问题是由于 InfluxDB 的 Dockerfile 中的 VOLUME 声明造成的。如果没有 UNVOLUME 类型的选项,或者完全摆脱它,我将无法更改与指定文件夹相关的任何内容。这不太理想,尤其是当您具有安全意识并希望指定某个 UID 时,应该以某个 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;博士
我认为不推荐使用 VOLUME 的最佳用途。
虽然这是一篇很老的帖子,但如果您对 volume
和 bind mounts
有一些混淆,我仍然希望您可以查看最新的 docker
官方文档
Bind mounts
从 Docker 早期就已经存在,我认为它也不应该是一个完美的设计,例如 "Bind mounts allow access to sensitive files",你可以得到信息 docker 官方更喜欢你使用 VOLUME
而不是 bind mounts
。
您可以从 here 获得卷的良好用例
参考
码头工人卷文档
泊坞窗存储概述
docker volume prune
可用于清理未附加到正在运行的容器的剩余卷。并不是说仅通过 id 就可以轻松区分潜在重要的内容.../
,所以vol1
相对于/
,它解析为/vol1
。如果您使用WORKDIR
指定除/
之外的工作目录,则vol1
和/vol1
将不再指向同一目录。