如何在 Docker 的构建上下文之外包含文件?

849
如何使用Docker文件中的“ADD”命令包含来自Docker构建上下文之外的文件?
来自Docker文档:
路径必须在构建上下文内; 你不能ADD ../something/something,因为docker build的第一步是将上下文目录(和子目录)发送到docker守护进程中。
我不想重组整个项目来适应Docker。 我希望保持所有Docker文件在同一个子目录中。
此外,似乎Docker尚未(可能永远不会)支持符号链接:Dockerfile ADD command does not follow symlinks on host #1676. 我唯一能想到的是包括一个预构建步骤来将文件复制到Docker构建上下文中(并配置我的版本控制以忽略这些文件)。 有没有更好的解决方法?

295
这就是 Docker 最糟糕的一点。在我看来,“Docker 项目”并不存在。 Docker 用于发布项目,它只是一个工具。我不想重新构建整个项目以适应 Docker,并添加 .dockerignore 等文件。最终,谁知道 Docker 将持续多久?将代码(即 Angular 项目)和部署方式(即 Docker)分开会很好。毕竟,将 Docker 文件放在其他所有文件旁边真的没有任何好处。这只是一种连接事物以创建映像的方法。 - TigerBear
15
是的,这真是令人沮丧。我面临着同样的问题,我有一个更大的二进制文件(已经压缩了),我不想将它复制到每个Docker构建上下文中,我更愿意从其当前位置(Docker构建上下文之外)获取它。我也不想在运行时映射卷,因为我试图在构建时COPY/ADD文件,然后解压缩并执行必要的操作,以便某些二进制文件被烘焙到镜像中。这样启动容器就会很快。 - jersey bean
我找到了一个好的结构,并在https://dev59.com/GlUL5IYBdhLWcg3wCEQA#53298446上详细解释。 - Marcello DeSales
10
Docker构建的问题在于“context”这个虚构的概念。除非Dockerfile放置在战略目录(又称上下文)下,例如"/"作为极端情况,以便您可以访问任何路径,否则它们无法定义构建过程(请注意,在健全的项目中也不应该这样做...此外,这会使Docker构建变得非常缓慢,因为Docker在启动时扫描整个上下文)。您可以考虑使用所有所需文件构建Docker映像,并使用FROM从那里继续。我不打算改变项目结构来适应Docker(或任何构建工具)。 - Devis L.
3
在新的功能中,如果您有dockerfile 1.4+和buildx 0.8+,您可以执行类似于以下内容的操作:docker buildx build --build-context othersource= ../something/something .。请参见下面的答案。 - Sami Wood
21个回答

697

解决这个问题的最佳方式是独立指定 Dockerfile,而不是使用构建上下文,可以使用 -f 参数。

例如,此命令将使 ADD 命令可以访问当前目录中的任何内容。

docker build -f docker-files/Dockerfile .

更新: Docker现在允许将Dockerfile放在构建上下文之外(在18.03.0-ce中修复)。因此,您也可以执行类似以下的操作:

docker build -f ../Dockerfile .

13
жӮЁеҸҜд»ҘеңЁComposeж–Ү件зҡ„build:йғЁеҲҶдёӯдҪҝз”Ёdockerfile:еұһжҖ§гҖӮе…·дҪ“иҜ·еҸӮиҖғhttps://docs.docker.com/compose/compose-file/#/compose-file-referenceгҖӮ - Emerson Farrugia
121
这是否解决了原帖作者想要“添加”一个在上下文目录之外的文件的问题?这就是我试图做的,但我不认为使用“-f”可以使外部文件可添加。 - Sridhar Sarnobat
13
如果您尝试从Docker构建上下文之外的完全不同的地方获取文件,那么这种解决方案并不实用。例如,假设您的文件位于/src/my_large_file.zip下,而您的Docker构建上下文位于/home/user1/mydocker_project下。我不想将文件复制到Docker构建上下文中,因为它太大了,我希望将其中的一些内容嵌入到映像中,以便启动容器不会变慢。 - jersey bean
43
无法赞同这个观点。 在我的docker-compose.yml中,我有:build:context:..,dockerfile:dir / Dockerfile。 现在我的构建上下文是父目录! - Mike Gleason jr Couturier
13
我正在运行一个含有大量文件的目录,并且结果是我看到了一条信息,上面写着 sending build context to Docker deamon,并且它似乎要复制几个GB的数据。 - oarfish
显示剩余16条评论

93

我花了很多时间尝试找出一个好的模式,并更好地解释这个功能支持的情况。我意识到最好的解释方法如下...

  • Dockerfile: 只会查看其自己相对路径下的文件
  • Context: 一个在“空间”中的位置,其中包含您想要共享的文件和您的Dockerfile将要复制到的位置

所以,说了这些,以下是需要重用名为start.sh的文件的Dockerfile示例

Dockerfile

它将始终从其相对路径加载,以其当前目录作为指定路径的本地引用。

COPY start.sh /runtime/start.sh

文件

考虑这个想法,我们可以考虑为构建特定东西的Dockerfiles拥有多个副本,但它们都需要访问start.sh

./all-services/
   /start.sh
   /service-X/Dockerfile
   /service-Y/Dockerfile
   /service-Z/Dockerfile
./docker-compose.yaml

考虑到这个结构和上面的文件,这里是一个docker-compose.yml

docker-compose.yaml

  • 在这个例子中,您的共享上下文目录是运行时目录。
    • 在这里有相同的心理模型,认为在该目录下的所有文件都会被移动到所谓的上下文中。
    • 同样,只需指定要复制到该目录的Dockerfile即可。您可以使用dockerfile来指定它。
  • 您的主内容所在的目录是要设置的实际上下文。

docker-compose.yml如下所示

version: "3.3"
services:
  
  service-A
    build:
      context: ./all-service
      dockerfile: ./service-A/Dockerfile

  service-B
    build:
      context: ./all-service
      dockerfile: ./service-B/Dockerfile

  service-C
    build:
      context: ./all-service
      dockerfile: ./service-C/Dockerfile
  • all-service设置为上下文,同时复制共享文件start.sh和每个dockerfile指定的Dockerfile到该目录中。
  • 每个都可以按自己的方式构建,共享启动文件!

8
关于Dockerfile,你的观点并不完全正确,正如被接受的答案所指出的那样,如果你在一个文件夹层次结构 a/b/c 中,那么在 c 中运行 docker build . 将无法访问 ../file-in-b。但是,我认为这个普遍的误解(或者至少是我的)是,上下文是由构建命令的第一个参数所指定的位置定义的,而不是由Dockerfile的位置定义的。因此,如被接受的答案所述:从 a 开始执行:docker build -f a/b/c/Dockerfile . 意味着在 Dockerfile 中,. 现在是文件夹 a - β.εηοιτ.βε
4
摘自Dockerfile文档: 文件和目录的路径将被解释为相对于构建上下文源的路径。 - Nishant George Agrwal
1
@RobertSinclair,没问题伙计!这在开发过程中对我很有帮助...我很高兴它能帮到你!!! - Marcello DeSales
这应该是解决此问题的选择方案,我以前从未在docker build中使用过context,但现在我无法没有它工作!这是最优雅和有用的解决方案。 - Pini Cheyni
@PiniCheyni 我完全有同感 :) - Marcello DeSales
显示剩余2条评论

83

我经常使用--build-arg选项来实现这个目的。例如,在Dockerfile中添加以下内容:

ARG SSH_KEY
RUN echo "$SSH_KEY" > /root/.ssh/id_rsa

你只需要这样做:

docker build -t some-app --build-arg SSH_KEY="$(cat ~/file/outside/build/context/id_rsa)" .

但是请注意来自Docker文档的以下警告:

警告: 不建议使用构建时变量传递像github密钥、用户凭据等机密信息。构建时变量的值可以被docker历史命令查看,因此会对镜像的任何用户可见。


21
这是一条糟糕的建议,需要加上严重警告。引自Docker文档:“警告:不建议使用构建时间变量传递像github密钥、用户凭据等秘密信息。 构建时间变量值可通过docker history命令被镜像中的任何用户看到。” 换句话说,在该示例中给出的例子会在docker镜像中泄露私有SSH密钥。 在某些情况下,这可能没问题。 https://docs.docker.com/engine/reference/builder/#arg - sheldonh
5
为了解决这个安全问题,你可以使用像“压缩”或“多阶段构建”这样的技术:https://vsupalov.com/build-docker-image-clone-private-repo-ssh-key/ - Jojo
1
这与OP所寻求的不同。你只是在本地环境中执行cat命令,并将结果传递给docker build。这并不是docker本身引用上下文区域之外的密钥文件(OP的问题)。 - undefined

59

在Linux中,您可以挂载其他目录,而不是创建符号链接。

mount --bind olddir newdir

更多细节请见https://superuser.com/questions/842642

我不知道其他操作系统是否有类似的东西。我也尝试过使用Samba共享一个文件夹并重新挂载到Docker上下文中,这也是可行的。


7
只有根用户可以绑定目录。 - jjcf89
可以访问docker的用户无论如何都拥有某种根访问权限,因为任意的docker命令都可以用来打破chroot jail(或者将必需的文件挂载到容器中)。 - SOFe

23
我相信更简单的解决方法是改变“上下文”本身。
例如,不要提供以下内容:
docker build -t hello-demo-app .

如果想要将父目录作为上下文,只需使用以下命令:

..

docker build -t hello-demo-app ..

10
我认为这违反了 .dockerignore 文件的规定 :-\ - NullVoxPopuli
1
我放弃了使用.dockerignore,转而创建了一个由Makefile管理的docker文件夹,其中只包含构建上下文所需的文件... 我只需要调用make build,它会拉取所有需要的文件(如果它们已更新),然后调用适当的docker构建... 我需要做额外的工作,但它可以完美地运行,因为我拥有完全的控制权。 - user11877195
1
.dockerignore 必须位于上下文的根目录中。如果你将上下文从 . 更改为 ..,那么你必须将 .dockerignore 文件向上移动一个目录。 - cowlinator

23
如果您阅读了问题2745的讨论,不仅Docker可能永远不支持符号链接,它们可能永远不支持在上下文之外添加文件。这似乎是一种设计哲学,即进入Docker构建的文件应明确成为其上下文的一部分,或者来自URL,在那里它被部署到具有固定版本的地方,以便使用已知的URL或随Docker容器一起提供的文件进行可重复的构建。

我更喜欢从受版本控制的源代码构建 - 即docker build   -t stuff http://my.git.org/repo - 否则,我会从某个随机位置和随机文件构建。

基本上不行...-- SvenDowideit, Docker Inc

只是我的想法,但我认为您应该重组以将代码和Docker存储库分开。这样,容器就可以在运行时拉取任何版本的代码,而不是在构建时。

或者,将Docker作为您的基本代码部署工件,然后将dockerfile放在代码存储库的根目录中。如果您选择这条路线,可能有意义为通用系统级详细信息创建父容器,并为特定于您的代码的设置创建子容器。


9
那么为什么要使用Docker呢? - lscoughlin

14

这种行为是由dockerpodman使用的上下文目录来呈现文件给构建过程的。

这里有一个很好的技巧,在构建指令中将上下文目录更改为要暴露给守护程序的目录的完整路径。例如:

docker build -t imageName:tag -f /path/to/the/Dockerfile /mysrc/path

使用 /mysrc/path 代替 .(当前目录),你将会以该目录为上下文,因此构建过程可以看到该目录下的所有文件。
在这个例子中,你将会向docker daemon暴露整个/mysrc/path树。
当与docker一起使用时,触发构建的用户ID必须对上下文目录中任何单个目录或文件具有递归读取权限

在以下情况下,这可能非常有用:你有一个/home/user/myCoolProject/Dockerfile,但希望将不在同一目录中的文件引入到该容器构建上下文中。

这里提供一个使用上下文目录进行构建的示例,但这次使用的是podman而不是docker

假设你的Dockerfile中有一条COPYADD指令,将要从项目外部的一个目录中复制文件,如下所示:

FROM myImage:tag
...
...
COPY /opt/externalFile ./
ADD /home/user/AnotherProject/anotherExternalFile ./
...

为了构建这个项目,需要使用位于/home/user/myCoolProject/Dockerfile路径下的容器文件,只需执行以下操作:
cd /home/user/myCoolProject
podman build -t imageName:tag -f Dockefile /

一些已知的更改上下文 dir 的用例是在使用容器作为构建源代码的工具链时。
例如:

podman build --platform linux/s390x -t myimage:mytag -f ./Dockerfile /tmp/mysrc

或者它可以是一个相对路径,比如:

podman build --platform linux/s390x -t myimage:mytag -f ./Dockerfile ../../

下面是使用全局路径的另一个例子:

FROM myImage:tag
...
...
COPY externalFile ./
ADD  AnotherProject ./
...

注意现在Dockerfile命令层的COPYADD指令中完整的全局路径已被省略。在这种情况下,如果externalFileAnotherProject都在/opt目录下,则构建时context dir必须相对于这些文件所在的位置。
podman build -t imageName:tag -f ./Dockerfile /opt
注意在使用 docker 中的上下文目录时使用 COPYADD:
docker 守护进程将尝试将可见的上下文目录树中的所有文件“流式传输”到守护进程,这可能会减缓构建速度。并需要用户从上下文目录递归授予权限。 当使用 API 进行构建时,这种行为可能更加昂贵。然而,podman 可以即时构建,无需递归权限,因为它不枚举整个上下文目录,并且也没有使用客户端/服务器结构。
在面对使用不同上下文目录时遇到此类问题时,使用 podman 更加有趣且值得尝试。podman 的相关构建可以参考以下链接:https://docs.podman.io/en/latest/markdown/podman-build.1.html。如果想获得更多信息,可以查阅以下参考资料链接:https://docs.docker.com/engine/reference/commandline/build/

2
这是危险的且不可取的。Docker构建上下文将是您整个计算机。首先,将整个上下文发送到守护程序将需要很长时间。其次,构建过程本身实际上可以做任何想做的事情。恶意的Dockerfile可以连接具有完全文件系统读取访问权限的远程服务器。最后,您的Dockerfile指令(如ADD)将与您的计算机紧密耦合,需要为所有内容提供完整的绝对路径。它们将不再具有可移植性。 - Alex Povel
这里的重点是解释 entrypoint 及其工作原理,而不是评判最佳标准。请记住,最好的方法是将所有内容都自包含在同一个项目中。但问题是如何实现这种行为并演示 entrypoint 的工作原理。由于守护程序中没有枚举,因此不会花费太长时间才能做到这一点。上下文是通过具有访问权限的 ID 在构建中定义的,而不是在守护程序中的固定路径,因此恶意 Dockefile 在这里没有任何意义。 - isca
你测试了你回答中的代码片段吗?作为一个普通用户,假设你使用的是Unix操作系统,你甚至没有/目录下的读取权限。它只会因为权限被拒绝而出现错误。以root身份运行上述命令可能会解决问题,但这是一个糟糕的想法。无论如何,在我进行测试的构建过程中,当加载了3GB的/时,我按下了CTRL+C退出了。上述方法对我根本不起作用! - Alex Povel
当然,对于这两种情况,它们都是可行的,这不是标准的问题,而是与上下文目录存在的原因有关。在这里,我使用“/”作为示例来说明其暴露的情况。但是,我已经改进了答案以解决您在此处提出的问题。 - isca
聪明的技巧,只要你掌控构建命令和Docker文件,否则调皮的开发人员可能会从你的构建机器上拿走任何文件 :) - Illegal Operator
这就是方法。是的,将“/”作为构建上下文是非常可疑的。对于大多数项目来说,使用-f path/to/Dockerfile .来将项目的根目录作为上下文,同时保持Dockerfile的有序性似乎是合理的。 - undefined

14

5
太棒了!我发现你甚至可以将tarball作为标准输入传递给docker build作为上下文:tar zc /dir1 /dir2 |docker build -。这对我的情况非常有帮助。 - Tore Olsen
同时,也可以从本地现有的tar包进行源码编译,参见此答案 - ceztko

10

我认为在今年早些时候,buildx添加了一个功能来实现这一点。

如果您有dockerfile 1.4+和buildx 0.8+,您可以像这样操作:

docker buildx build --build-context othersource= ../something/something .

然后在您的Docker文件中,您可以使用from命令添加上下文

ADD –-from=othersource . /stuff

查看这篇相关的帖子


1
注意!正确的语法是 ADD --from=othersource . /stuff(双破折号) - TurboHz
这只适用于目录吗?我无法传递文件路径,会出现错误ERROR: failed to get build context path {/builds/my_file <nil>}: not a directory - anatol
我其实是来提及多个构建上下文的,真遗憾我要滚动这么久才找到你的答案..! - undefined

5
使用docker-compose,我通过创建一个挂载所需卷并提交容器映像的服务来完成此操作。然后,在随后的服务中,我依赖于先前提交的映像,该映像在挂载位置存储了所有数据。然后,您将不得不将这些文件复制到其最终目标,因为运行docker commit命令时,主机挂载目录不会被提交。

您并不需要使用docker-compose来完成此操作,但它可以让生活变得更轻松一些。

# docker-compose.yml

version: '3'
  services:
    stage:
      image: alpine
      volumes:
        - /host/machine/path:/tmp/container/path
      command: bash -c "cp -r /tmp/container/path /final/container/path"
    setup:
      image: stage

# setup.sh

# Start "stage" service
docker-compose up stage

# Commit changes to an image named "stage"
docker commit $(docker-compose ps -q stage) stage

# Start setup service off of stage image
docker-compose up setup

使用Compose,您现在可以使用build.additional_contexts来定义多个上下文:https://github.com/compose-spec/compose-spec/pull/307 - undefined

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