Dockerfile中的有条件COPY/ADD是什么?

219

在我的Dockerfile中,如果存在一个文件,我想把它复制到我的镜像中,requirements.txt文件似乎是pip的一个不错的选择,但这应该如何实现?

COPY (requirements.txt if test -e requirements.txt; fi) /destination
...
RUN  if test -e requirements.txt; then pip install -r requirements.txt; fi
或者
if test -e requirements.txt; then
    COPY requiements.txt /destination;
fi
RUN  if test -e requirements.txt; then pip install -r requirements.txt; fi

请查看此处:https://docs.docker.com/reference/builder/ - Tuan
12
那个链接中具体哪些内容有助于做到这一点? - ToolmakerSteve
10个回答

177
这里有一个简单的解决方法:
COPY foo file-which-may-exist* /target

请确保foo存在,因为COPY至少需要一个有效的源。

如果file-which-may-exist存在,它也会被复制。

注意: 您应该注意确保通配符没有选择到其他不想复制的文件。为了更加小心谨慎,您可以使用file-which-may-exist??只匹配一个字符)。

或者更好的方法是,使用类似这样的字符类来确保只匹配一个文件:

COPY foo file-which-may-exis[t] /target

2
你能用一个文件夹做同样的事情吗? - Benjamin Toueg
1
@BenjaminToueg:是的,根据文档,您可以复制文件和文件夹。 - jdhildeb
2
这个很好用。对于有多个目的地的文件,我先复制到一个临时目录,然后再移动到需要的位置。COPY --from=docker /usr/bin/docker /usr/lib/libltdl.so* /tmp/docker/ RUN mv /tmp/docker/docker /usr/bin/docker RUN mv /tmp/docker/libltdl.so.7 /usr/lib/libltdl.so.7 || true(其中共享库是未知实体)。 - Adam K Dean
6
那么答案是“确保有文件”,然后演示如何使用COPY运算符?我看不出这与原问题有什么关系。 - derrend
7
应该接受这个答案。不过需要注意的是我们需要使用如此奇怪的解决方法... - greatvovan
显示剩余4条评论

108

根据这条评论所述,Santhosh Hirekerur的回答仍会复制该文件。要真正实现有条件的复制,您可以使用此方法。

ARG BUILD_ENV=copy

FROM alpine as build_copy
ONBUILD COPY file /file

FROM alpine as build_no_copy
ONBUILD RUN echo "I don't copy"

FROM build_${BUILD_ENV}
# other stuff
ONBUILD指令可确保只有在通过BUILD_ENV选择了“分支”时才会复制文件。在调用docker build之前,请使用一个小脚本设置此变量。请注意,保留HTML标签。

3
我喜欢这个答案,因为它不仅向我展示了超级方便的ONBUILD,而且似乎是最容易与其他传入的变量集成的,例如,如果您想基于 BUILD_ENV 设置标签,或在 ENV 中存储一些状态。 - DeusXMachina
3
我刚刚尝试了类似的操作,但是出现了以下错误:来自守护程序的错误响应:Dockerfile解析错误第52行:构建阶段名称无效:"site_builder_${host_env}",名称不能以数字开头或包含符号。 - paulecoyote
12
我确信你已经理解了,但是为了在FROM中使用ARG,必须在第一个FROM之前定义ARG。如果你还想在FROM中像使用ENV一样使用相同的ARG,那么你也必须在FROM的作用域内定义它。理解ARG和FROM如何交互 - Anthony Ledesma
太高兴找到这个答案了!在条件多阶段构建中解决了我的问题! - Shan Dou
1
我得到的是:解决失败:RPC错误:代码=未知描述=无法使用前端dockerfile.v0解决:无法创建LLB定义:无法解析阶段名称"build_":无效的引用格式。 - 404usernamenotfound

51

2021年以后,从这个答案中得知,使用全局模式,Docker COPY命令将不会因为找不到有效的源文件而失败

COPY requiements.tx[t] /destination

2015年:目前不支持此功能(因为我怀疑会导致一个无法重现的映像,由于同一 Dockerfile 会根据文件是否存在而复制或不复制该文件)。

issue 13045 中仍有人提出此需求,使用通配符:COPY foo/* bar/ 如果 foo 中没有文件,则不起作用(2015年5月)。
目前 Docker 不会实现它(2015 年 7 月),但是另一个构建工具,如bocker,可以支持此功能。


2021

COPY source/. /source/ 对我有效(即,无论目录是否为空,都会复制目录,就像 "将目录复制到 docker build,而不管它是否为空-在"COPY failed: no source files were specified"上失败" 一文中所述)。

2022

Here is my suggestion:

# syntax=docker/dockerfile:1.2

RUN --mount=type=bind,source=jars,target=/build/jars \
 find /build/jars -type f -name '*.jar' -maxdepth 1  -print0 \
 | xargs -0 --no-run-if-empty --replace=source cp --force source >"${INSTALL_PATH}/modules/"

That works around:

COPY jars/*.jar "${INSTALL_PATH}/modules/"

But copies no *.jar if none is found, without throwing an error.


69
答案不错,但是个人认为 Docker 的逻辑有缺陷。如果你使用不同的构建上下文来运行相同的 Dockerfile,你将会得到一个不同的镜像,这是可以预期的。如果使用相同的构建上下文,你将获得相同的镜像。如果在同一构建上下文中插入条件性的 COPY/ADD 指令,你将会得到相同的镜像。所以这些都没问题。这只是我的看法。 - nathan g
Docker 是关于不可变基础设施的。您的开发、暂存和生产环境应该尽可能地接近 99.99%,如果不是完全相同的话。请使用环境变量。 - AndrewMcLagan
6
如果一个前端开发环境使用Webpack开发服务器运行,而相应的生产环境使用静态/dist文件夹,该怎么办?这是大多数前端设置的情况,显然在这里“dev”和“prod”不能相同。那么该如何处理? - Jivan
我不使用Docker来开发我的Node前端。我使用普通的Webpack本地主机:localhost:3000等。虽然仍要启动本地Docker开发环境,以便您的node/react/angular前端与正常的Docker容器环境中运行的任何内容进行通信。例如API、Redis、MySQL、Mongo、Elasticsearch和任何其他微服务。你是可以在容器中运行Webpack开发环境,但我感觉这有点过头了。 - AndrewMcLagan
@Jivan,使用onbuild镜像来定义通用指令,然后为开发和生产构建特定的镜像如何?Docker Hub Node存储库似乎包含每个Node版本的onbuild镜像:https://hub.docker.com/_/node/。或者你可以自己制作。 - david_i_smith

43

我认为我想出了一个有效的解决方法,使用这个Dockerfile

FROM alpine
COPy always_exist_on_host.txt .
COPY *sometimes_exist_on_host.txt .
always_exist_on_host.txt 文件将始终被复制到镜像中,即使 sometimes_exist_on_host.txt 文件不存在,构建也不会因为无法复制该文件而失败。此外,当 sometimes_exist_on_host.txt 文件存在时,它也将被复制到镜像中。
例如:
.
├── Dockerfile
└── always_exist_on_host.txt

构建成功

docker build . -t copy-when-exists --no-cache
[+] Building 1.0s (7/7) FINISHED                                                                                                                            
 => [internal] load .dockerignore                                                                                                                      0.0s
 => => transferring context: 2B                                                                                                                        0.0s
 => [internal] load build definition from Dockerfile                                                                                                   0.0s
 => => transferring dockerfile: 36B                                                                                                                    0.0s
 => [internal] load metadata for docker.io/library/alpine:latest                                                                                       1.0s
 => [internal] load build context                                                                                                                      0.0s
 => => transferring context: 43B                                                                                                                       0.0s
 => CACHED [1/2] FROM docker.io/library/alpine@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a                                 0.0s
 => [2/2] COPY always_exist_on_host.txt *sometimes_exist_on_host.txt .                                                                                 0.0s
 => exporting to image                                                                                                                                 0.0s
 => => exporting layers                                                                                                                                0.0s
 => => writing image sha256:e7d02c6d977f43500dbc1c99d31e0a0100bb2a6e5301d8cd46a19390368f4899                                                           0.0s               

.
├── Dockerfile
├── always_exist_on_host.txt
└── sometimes_exist_on_host.txt

构建仍然成功

docker build . -t copy-when-exists --no-cache
[+] Building 1.0s (7/7) FINISHED                                                                                                                            
 => [internal] load build definition from Dockerfile                                                                                                   0.0s
 => => transferring dockerfile: 36B                                                                                                                    0.0s
 => [internal] load .dockerignore                                                                                                                      0.0s
 => => transferring context: 2B                                                                                                                        0.0s
 => [internal] load metadata for docker.io/library/alpine:latest                                                                                       0.9s
 => [internal] load build context                                                                                                                      0.0s
 => => transferring context: 91B                                                                                                                       0.0s
 => CACHED [1/2] FROM docker.io/library/alpine@sha256:c0e9560cda118f9ec63ddefb4a173a2b2a0347082d7dff7dc14272e7841a5b5a                                 0.0s
 => [2/2] COPY always_exist_on_host.txt *sometimes_exist_on_host.txt .                                                                                 0.0s
 => exporting to image                                                                                                                                 0.0s
 => => exporting layers                                                                                                                                0.0s
 => => writing image sha256:4c88e2ffa77ebf6869af3c7ca2a0cfb9461979461fc3ae133709080b5abee8ff                                                           0.0s
 => => naming to docker.io/library/copy-when-exists                                                                                                    0.0s

1
我喜欢它,但希望不需要有一个“always_exist_on_host.txt”文件。 - derrend
1
@derrend请检查我的编辑。为了示例的缘故,我将COPY分成了两层。 - aidanmelen
2
5年了 - 终于解决方案可行 :-) - Piotr Żak
3
@corwin.amber 我加入了 COPY always_exist_on_host.txt . 来演示 COPY 通常是如何使用的。而 COPY *sometimes_exist_on_host.txt . 则是你所需要的。 - aidanmelen
@MarcellodeSales 你确定你在构建时使用通配符有条件地复制文件吗?我用的是 Docker version 20.10.8, build 3967b7d,完全没有问题。 - aidanmelen
显示剩余7条评论

15

将所有文件复制到一个临时目录中,手动挑选需要的文件,删除其余文件。

COPY . /throwaway
RUN cp /throwaway/requirements.txt . || echo 'requirements.txt does not exist'
RUN rm -rf /throwaway

您可以使用构建阶段来实现类似的功能,它依赖于相同的解决方案,使用cp进行有条件复制。通过使用构建阶段,您的最终镜像将不包含来自初始COPY的所有内容。

FROM alpine as copy_stage
COPY . .
RUN mkdir /dir_for_maybe_requirements_file
RUN cp requirements.txt /dir_for_maybe_requirements_file &>- || true

FROM alpine
# Must copy a file which exists, so copy a directory with maybe one file
COPY --from=copy_stage /dir_for_maybe_requirements_file /
RUN cp /dir_for_maybe_requirements_file/* . &>- || true
CMD sh

1
虽然这在技术上解决了问题,但它并没有减小图像的大小。如果您试图有条件地复制某些庞大的东西(比如深度网络模型),由于叠加文件系统的工作方式,仍会使图像的大小膨胀。 - DeusXMachina
@DeusXMachina,你使用的是哪个版本的Docker?文档与你所说的相矛盾https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds。层不应从非最终构建阶段保留。 - cdosborn
@cdosburn - 我在18.09上观察到了这个问题。我主要是在谈论第一个示例,分阶段构建应该避免该问题。我认为现在每个FROM阶段都会压缩,但你让我对我的回忆产生了怀疑。我需要用一些方法进行实验。 - DeusXMachina
@DeusXMachina,只有第二个解决方案可以减小图像大小。 - cdosborn
这是我情况下的一个不错的解决方法。我复制了一个“缓存”,根据缓存的内容,我选择在脚本文件中要执行什么操作! - Paschalis

14

解决方案

我需要根据环境变量将文件夹复制到服务器上。我使用了一个空的服务器镜像,在本地文件夹中创建所需的部署文件夹结构,然后在DockerFile中添加以下行以将文件夹复制到容器中。最后一行添加入口点以在docker启动服务器之前执行init file.sh。

#below lines added to integrate testing framework
RUN mkdir /mnt/conf_folder
ADD install /mnt/conf_folder/install
ADD install_test /mnt/conf_folder/install_test
ADD custom-init.sh /usr/local/bin/custom-init.sh
ENTRYPOINT ["/usr/local/bin/custom-init.sh"]

然后在本地创建 custom-init.sh 文件,脚本类似于以下内容:

#!/bin/bash
if [ "${BUILD_EVN}" = "TEST" ]; then
    cp -avr /mnt/conf_folder/install_test/* /mnt/wso2das-3.1.0/
else
    cp -avr /mnt/conf_folder/install/* /mnt/wso2das-3.1.0/
fi;
在以下的 docker-compose 文件中,有如下代码:

environment: - BUILD_EVN=TEST

这些更改会在 Docker 构建期间将文件夹复制到容器中。当我们执行 docker-compose up 命令时,在服务器启动之前它会将实际需要的文件夹复制或部署到服务器上。

9
Docker镜像是分层的。无论你提到的if语句是否成立,ADD命令都会将它们复制到镜像中... - MyUserInStackOverflow
@MyUserInStackOverflow - 我认为这个“变通”的想法是将install和install_test都复制到镜像中,但在运行镜像时,只有其中一个文件夹被复制到最终位置。如果两者都在镜像中的某个地方,那么这可能是一种合理的技术手段。 - ToolmakerSteve

2
尝试了其他想法,但都不符合我们的要求。这个想法是创建基础nginx镜像,用于子静态Web应用程序。出于安全、优化和标准化的原因,基础镜像必须能够在子映像添加的目录上运行命令。基础镜像不能控制子映像添加哪些目录。假设子映像将资源复制到COMMON_DEST_ROOT下的某个地方。
这种方法是一种hack,但是这个想法是基础镜像将支持1到N个目录的COPY指令,这些目录是由子映像添加的。ARG PLACEHOLDER_FILEENV UNPROVIDED_DEST用于满足任何不需要的COPY指令的<src><dest>要求。
#
# base-image:01
#
FROM nginx:1.17.3-alpine
ENV UNPROVIDED_DEST=/unprovided
ENV COMMON_DEST_ROOT=/usr/share/nginx/html
ONBUILD ARG PLACEHOLDER_FILE
ONBUILD ARG SRC_1
ONBUILD ARG DEST_1
ONBUILD ARG SRC_2
ONBUILD ARG DEST_2
ONBUILD ENV SRC_1=${SRC_1:-PLACEHOLDER_FILE}
ONBUILD ENV DEST_1=${DEST_1:-${UNPROVIDED_DEST}}
ONBUILD ENV SRC_2=${SRC_2:-PLACEHOLDER_FILE}
ONBUILD ENV DEST_2=${DEST_2:-${UNPROVIDED_DEST}}

ONBUILD COPY ${SRC_1} ${DEST_1}
ONBUILD COPY ${SRC_2} ${DEST_2}

ONBUILD RUN sh -x \
    #
    # perform operations on COMMON_DEST_ROOT
    #
    && chown -R limited:limited ${COMMON_DEST_ROOT} \
    #
    # remove the unprovided dest
    #
    && rm -rf ${UNPROVIDED_DEST}

#
# child image
#
ARG PLACEHOLDER_FILE=dummy_placeholder.txt
ARG SRC_1=app/html
ARG DEST_1=/usr/share/nginx/html/myapp
FROM base-image:01

这个解决方案存在明显的缺陷,比如虚拟的PLACEHOLDER_FILE和支持COPY指令的硬编码数量。此外,没有办法摆脱在COPY指令中使用的ENV变量。


1

COPY不再需要至少存在一个源,如果没有匹配项,globbing也不会失败,因此您可以只

COPY requirements.tx[t] /destination

如果存在,将复制requirements.txt,如果不存在也不会失败。


0

我有其他解决方案。想法是在构建上下文中触摸文件,并在Dockerfile内使用复制语句。如果文件存在,它将只创建一个空文件,docker build不会失败。如果已经有一个文件,它将只更改时间戳。

touch requirements.txt

还有Dockerfile文件

FROM python:3.9
COPY requirements.txt .

0
使用支架非常简单。
RUN --mount=target=/mnt \
    [ -f /mnt/requirements.txt ] && cp /mnt/requirements.txt . || :

更复杂的用例——寻找特定的子目录:
RUN --mount=target=/mnt \
    if [ -d /mnt/toolchain/out ]; then \
      mkdir toolchain; \
      cp -r /mnt/toolchain/out toolchain; \
    fi

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