以下是我的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上 容器文件夹。

请告诉我这是正确的方式吗?


当前回答

在Dockerfile中指定VOLUME行会在映像上配置一些元数据,但是如何使用这些元数据是很重要的。

首先,这两行做了什么:

WORKDIR /usr/src/app
VOLUME . /usr/src/app

如果目录不存在,那么WORKDIR行将创建该目录,并更新一些图像元数据以指定所有相对路径,同时,RUN等命令的当前目录将位于该位置。这里的VOLUME行指定了两个卷,一个是相对路径,另一个是/usr/src/app,两者恰好是相同的目录。大多数情况下,VOLUME行只包含一个目录,但它可以像您所做的那样包含多个目录,或者它可以是一个json格式的数组。

You cannot specify a volume source in the Dockerfile: A common source of confusion when specifying volumes in a Dockerfile is trying to match the runtime syntax of a source and destination at image build time, this will not work. The Dockerfile can only specify the destination of the volume. It would be a trivial security exploit if someone could define the source of a volume since they could update a common image on the docker hub to mount the root directory into the container and then launch a background process inside the container as part of an entrypoint that adds logins to /etc/passwd, configures systemd to launch a bitcoin miner on next reboot, or searches the filesystem for credit cards, SSNs, and private keys to send off to a remote site.

What does the VOLUME line do? As mentioned, it sets some image metadata to say a directory inside the image is a volume. How is this metadata used? Every time you create a container from this image, docker will force that directory to be a volume. If you do not provide a volume in your run command, or compose file, the only option for docker is to create an anonymous volume. This is a local named volume with a long unique id for the name and no other indication for why it was created or what data it contains (anonymous volumes are were data goes to get lost). If you override the volume, pointing to a named or host volume, your data will go there instead.

VOLUME breaks things: You cannot disable a volume once defined in a Dockerfile. And more importantly, the RUN command in docker is implemented with temporary containers with the classic builder. Those temporary containers will get a temporary anonymous volume. That anonymous volume will be initialized with the contents of your image. Any writes inside the container from your RUN command will be made to that volume. When the RUN command finishes, changes to the image are saved, and changes to the anonymous volume are discarded. Because of this, I strongly recommend against defining a VOLUME inside the Dockerfile. It results in unexpected behavior for downstream users of your image that wish to extend the image with initial data in volume location.

如何指定卷?要指定在映像中包含卷的位置,请提供docker-compose.yml。用户可以对其进行修改,以根据本地环境调整卷的位置,它还可以捕获其他运行时设置,如发布端口和网络。

应该有人把这个记录下来!他们有。Docker在Dockerfile文档中包含了关于VOLUME使用的警告,以及在运行时指定源文件的建议:

Changing the volume from within the Dockerfile: If any build steps change the data within the volume after it has been declared, those changes will be discarded. ... The host directory is declared at container run-time: The host directory (the mountpoint) is, by its nature, host-dependent. This is to preserve image portability, since a given host directory can’t be guaranteed to be available on all hosts. For this reason, you can’t mount a host directory from within the Dockerfile. The VOLUME instruction does not support specifying a host-dir parameter. You must specify the mountpoint when you create or run the container.


随着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

其他回答

我不认为VOLUME的使用在任何情况下都是好的,除非你为自己创建一个图像,没有其他人会使用它。

I was impacted negatively due to VOLUME exposed in base images that I extended and only came up to know about the problem after the image was already running, like wordpress that declares the /var/www/html folder as a VOLUME, and this meant that any files added or changed during the build stage aren't considered, and live changes persist, even if you don't know. There is an ugly workaround to define web directory in another place, but this is just a bad solution to a much simpler one: just remove the VOLUME directive.

你可以使用-v选项轻松地实现卷的目的,这不仅可以明确容器的卷是什么(而不必查看Dockerfile和父Dockerfiles),而且还可以让使用者选择是否使用卷。

由于以下原因,使用卷也是不好的,正如这个答案所说:

However, the VOLUME instruction does come at a cost. Users might not be aware of the unnamed volumes being created, and continuing to take up storage space on their Docker host after containers are removed. There is no way to remove a volume declared in a Dockerfile. Downstream images cannot add data to paths where volumes exist. The latter issue results in problems like these. How to “undeclare” volumes in docker image? GitLab on Docker: how to persist user data between deployments?

选择不声明卷会有帮助,但前提是您知道生成映像的dockerfile(以及父dockerfile !)中定义的卷。此外,VOLUME可以添加到新版本的Dockerfile中,从而意外地破坏映像消费者的工作。

另一个很好的解释(关于oracle映像具有VOLUME,已被删除):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

一个添加选项来重置父映像属性(包括VOLUME)的拉请求已经关闭,这里正在讨论(你可以看到几个由于dockerfiles中定义的卷而受到不利影响的案例),其中有一个关于VOLUME的很好的解释:

Using VOLUME in the Dockerfile is worthless. If a user needs persistence, they will be sure to provide a volume mapping when running the specified container. It was very hard to track down that my issue of not being able to set a directory's ownership (/var/lib/influxdb) was due to the VOLUME declaration in InfluxDB's Dockerfile. Without an UNVOLUME type of option, or getting rid of it altogether, I am unable to change anything related to the specified folder. This is less than ideal, especially when you are security-aware and desire to specify a certain UID the image should be ran as, in order to avoid a random user, with more permissions than necessary, running software on your host.

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的最佳用途是弃用。

Dockerfile中的VOLUME命令是非常合法的,完全常规的,绝对可以使用,无论如何它都没有被弃用。只需要理解它。

我们使用它来指向容器中应用程序将经常写入的任何目录。我们不使用VOLUME只是因为我们想要像配置文件一样在主机和容器之间共享。

The command simply needs one param; a path to a folder, relative to WORKDIR if set, from within the container. Then docker will create a volume in its graph(/var/lib/docker) and mount it to the folder in the container. Now the container will have somewhere to write to with high performance. Without the VOLUME command the write speed to the specified folder will be very slow because now the container is using it's copy on write strategy in the container itself. The copy on write strategy is a main reason why volumes exist.

如果挂载在VOLUME命令指定的文件夹上,则该命令永远不会运行,因为VOLUME只在容器启动时执行,有点像ENV。

基本上,使用VOLUME命令,无需从外部挂载任何卷即可获得性能。数据将保存跨容器运行也没有任何外部挂载。然后当准备好时,简单地安装在它上面的东西。

一些好的用例: - - - - - -日志 -临时文件夹

一些糟糕的用例: -静态文件 ——配置 ——代码

官方docker教程说:

A data volume is a specially-designated directory within one or more containers that bypasses the Union File System. Data volumes provide several useful features for persistent or shared data: Volumes are initialized when a container is created. If the container’s base image contains data at the specified mount point, that existing data is copied into the new volume upon volume initialization. (Note that this does not apply when mounting a host directory.) Data volumes can be shared and reused among containers. Changes to a data volume are made directly. Changes to a data volume will not be included when you update an image. Data volumes persist even if the container itself is deleted.

在Dockerfile中,你只能在容器中指定卷的目的地。例如/usr/src/app.

当你运行一个容器时,例如docker run——volume=/opt:/usr/src/app my_image,你可以但不必指定它在主机上的挂载点(/opt)。如果你没有指定——volume参数,那么挂载点将自动选择,通常在/var/lib/docker/volumes/下。

简而言之:不,你的VOLUME指令不正确。

Dockerfile的VOLUME指定给定容器侧路径的一个或多个卷。但是它不允许映像作者指定主机路径。在主机端,卷是在Docker根目录中使用一个非常长的类似id的名称创建的。在我的机器上,这是/var/lib/docker/volumes

注意:由于自动生成的名称非常长,从人类的角度来看没有任何意义,因此这些卷通常被称为“未命名的”或“匿名的”。

你的例子中使用了a '。'字符甚至不会在我的机器上运行,无论我将圆点作为第一个参数还是第二个参数。我得到这个错误消息:

docker:来自daemon的错误响应:oci运行时错误:container_linux。Go:265:正在启动容器进程,导致“process_linux. exe”。Go:368: container init导致“open /dev/ptmx: no such file or directory”。

我知道在这一点上所说的对于试图理解VOLUME和-v的人来说可能不是很有价值,它当然不能为您试图实现的目标提供解决方案。所以,希望下面的例子能更清楚地说明这些问题。

Minitutorial:指定卷

给定这个Dockerfile:

FROM openjdk:8u131-jdk-alpine
VOLUME vol1 vol2

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

构建:

docker build -t my-openjdk

Run:

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(在指定位置创建一个空白文件)。

这个文件现在在主机上可用,在一个未命名的卷lol中。这花了我两次尝试,因为我第一次尝试了第一个列出的卷,但最终我在第二个列出的卷中找到了我的文件,在主机上使用以下命令:

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

这上面印着“你好,世界!”

最好的部分是,我们完全可以在第二次运行时使用新消息修改.java文件,以获得另一个输出-而不必重新构建image =)

最后的评论

我对Docker很陌生,前面提到的“教程”反映了我从3天的命令行黑客马拉松中收集到的信息。我几乎感到羞愧,我不能提供清晰的英文文档链接来支持我的声明,但我真的认为这是由于缺乏文档,而不是个人努力。我确实知道这些例子是用我目前的设置“Windows 10 -> Vagrant 2.0.0 -> Docker 17.09.0-ce”来工作的。

本教程并没有解决“如何在Dockerfile中指定容器的路径,而让run命令只指定主机路径”的问题。也许有办法,只是我还没找到。

Finally, I have a gut feeling that specifying VOLUME in the Dockerfile is not just uncommon, but it's probably a best practice to never use VOLUME. For two reasons. The first reason we have already identified: We can not specify the host path - which is a good thing because Dockerfiles should be very agnostic to the specifics of a host machine. But the second reason is people might forget to use the --rm option when running the container. One might remember to remove the container but forget to remove the volume. Plus, even with the best of human memory, it might be a daunting task to figure out which of all anonymous volumes are safe to remove.

为了更好地理解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官方镜像的使用(请检查“Where to storage Data”部分):

参考: https://hub.docker.com/_/mysql/