213

Inside of my Dockerfiles I would like to COPY a file into my image if it exists, the requirements.txt file for pip seems like a good candidate but how would this be achieved?

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

or

if test -e requirements.txt; then
    COPY requiements.txt /destination;
fi
RUN  if test -e requirements.txt; then pip install -r requirements.txt; fi
Braiam
  • 1
  • 11
  • 47
  • 78
derrend
  • 4,176
  • 4
  • 27
  • 40

9 Answers9

174

Here is a simple workaround:

COPY foo file-which-may-exist* /target

Make sure foo exists, since COPY needs at least one valid source.

If file-which-may-exist is present, it will also be copied.

NOTE: You should take care to ensure that your wildcard doesn't pick up other files which you don't intend to copy. To be more careful, you could use file-which-may-exist? instead (? matches just a single character).

Or even better, use a character class like this to ensure that only one file can be matched:

COPY foo file-which-may-exis[t] /target
jdhildeb
  • 3,322
  • 3
  • 17
  • 25
  • 2
    Can you do the same thing with a folder? – Benjamin Toueg Jan 03 '18 at 11:06
  • 1
    @BenjaminToueg: Yes, according to the [docs](https://docs.docker.com/engine/reference/builder/#copy) you can copy files as well as folders. – jdhildeb Jan 04 '18 at 14:42
  • 2
    This works great. For files with multiple destinations, I copied to a temporary directory and then moved them to where needed. `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` (where the shared library is the unknown entity.) – Adam K Dean Apr 18 '18 at 15:37
  • When copying multiple extant files the destination must be a directory. How does this work when both foo and your file-which-may-exist* exist? – melchoir55 Sep 07 '18 at 17:30
  • 5
    So the answer is 'make sure there is a file' and then a demonstration on how to use the COPY operator? I fail to see how this relates to the original question. – derrend Sep 23 '18 at 12:31
  • @derrend: You can have logic which creates a file under certain conditions. Then use the COPY command with a wildcard as described, so that if the file exists, it will be copied into the image. This achieves what the OP is trying to do with `requirements.txt`. – jdhildeb Sep 24 '18 at 14:30
  • 6
    This should be the accepted answer. It is weird though that we need so weird workarounds... – greatvovan Feb 04 '21 at 18:20
  • To test whether the file was copied you can this in the Dockerfile `RUN test -f /tmp/myfile.txt && echo "file exists"; exit 0`. Exit 0 is needed afterwards otherwise the `test -f` command will exit with an error code if there's no file. – Aidan Gallagher Jun 13 '23 at 22:07
105

As stated by this comment, Santhosh Hirekerur's answer still copies the file, to achieve a true conditional copy, you can use this method.

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

The ONBUILD instructions ensures that the file is only copied if the "branch" is selected by the BUILD_ENV. Set this var using a little script before calling docker build

Jingguo Yao
  • 7,320
  • 6
  • 50
  • 63
Siyu
  • 11,187
  • 4
  • 43
  • 55
  • 3
    I like this answer because it opened my eyes not only to ONBUILD, which is super handy, but it also seems the easiest to integrate with other variables passed in, e.g. if you want to set the tag based on BUILD_ENV, or store some state in ENV. – DeusXMachina Mar 13 '20 at 19:19
  • 2
    I just tried something like that and got: Error response from daemon: Dockerfile parse error line 52: invalid name for build stage: "site_builder_${host_env}", name can't start with a number or contain symbols – paulecoyote Apr 08 '20 at 02:28
  • 10
    @paulecoyote I am sure that you have since figured it out but in order to use an `ARG` in a `FROM` the `ARG` must be defined prior to the first `FROM`. If you also want to use the same `ARG` within a `FROM`, like an `ENV`, then you must also define it inside of the scope of the `FROM`. [Understand how ARG and FROM interact](https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact) – Anthony Ledesma May 12 '21 at 21:59
  • So glad to find this answer! Solved my problem in a conditional multi-stage build! – Shan Dou Oct 21 '22 at 04:07
  • I get: failed to solve: rpc error: code = Unknown desc = failed to solve with frontend dockerfile.v0: failed to create LLB definition: failed to parse stage name "build_": invalid reference format – 404usernamenotfound Jun 14 '23 at 19:39
46

2021+, from this answer, using glob pattern, Docker COPY will not fail if it won't find any valid source

COPY requiements.tx[t] /destination

2015: This isn't currently supported (as I suspect it would lead to a non-reproducible image, since the same Dockerfile would copy or not the file, depending on its existence).

This is still requested, in issue 13045, using wildcards: "COPY foo/* bar/" not work if no file in foo" (May 2015).
It won't be implemented for now (July 2015) in Docker, but another build tool like bocker could support this.


2021:

COPY source/. /source/ works for me (i.e. copies directory when empty or not, as in "Copy directory into docker build no matter if empty or not - fails on "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.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • 68
    good answer, but the docker logic, IMO, is flawed. if you run the same dockerfile with a different build-context, you would get a different image. that's to be expected. using the same build-context will give the same image. and if you insert conditional COPY/ADD instructions on the same build-context, you'll get the same image. so that checks out. that's just my 2 cents though. – nathan g Aug 06 '15 at 17:09
  • Docker is about immutable infrastructure. Your environments dev, staging and prod should be 99.99% as close as possible if not identical. Use environment variables. – AndrewMcLagan Dec 05 '15 at 23:35
  • 6
    @AndrewMcLagan what if, for instance, a front-end `dev` environment runs with a webpack dev server, and the equivalent `prod` environment works with a `/dist` static folder? This is the case in most front-end setups today, and obviously `dev` and `prod` cannot be the same here. So how to deal with that? – Jivan Jun 29 '16 at 17:21
  • I don't use docker to develop my node front ends. The normal webpack localhost:3000 etc... Although still boot your local docker dev environment so your node/react/angular front end communicates to whatever is running in your normal docker container environment. E.g. APIs, redis, MySQL, mongo, elastic search and any other micro service. You ..could.., run a webpack dev environment in a container. But I feel it's too far... – AndrewMcLagan Jun 29 '16 at 21:27
  • @Jivan How about using an onbuild image to define the common instructions and then building specific images for dev and prod. The Docker Hub Node repo appears to contain onbuild images for each Node version: https://hub.docker.com/_/node/. Or maybe you could roll your own. – david_i_smith Nov 28 '16 at 03:54
42

I think I came up with a valid workaround with this Dockerfile

FROM alpine
COPy always_exist_on_host.txt .
COPY *sometimes_exist_on_host.txt .

The always_exist_on_host.txt file will always be copied to the image and the build won't fail to COPY the sometimes_exist_on_host.txt file when it doesn't exist. Furthermore, it will COPY the sometimes_exist_on_host.txt file when it does exist.

For example:

.
├── Dockerfile
└── always_exist_on_host.txt

build succeeds

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

build still succeeds

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
aidanmelen
  • 6,194
  • 1
  • 23
  • 24
  • 1
    I like it but wish there didn't need to be an 'always_exist_on_host.txt' file. – derrend Dec 04 '20 at 07:12
  • 1
    @derrend check my edit. I broke the COPY into two layers for the sake of the example. – aidanmelen Dec 05 '20 at 16:07
  • 2
    5 years - and finally solution is working :-) – Piotr Żak Apr 27 '21 at 07:30
  • I love this, but an explanation of how it works would be nice with the asterisk and without it. – Branden May 07 '21 at 15:47
  • @AidanMelen so do you say that one can choose to remove the first `COPY` and then we do not need the dummy file anymore? – corwin.amber Jun 12 '21 at 12:32
  • 3
    @corwin.amber I included `COPY always_exist_on_host.txt .` to demonstrate how the COPY is typically used. The `COPY *sometimes_exist_on_host.txt .` is all you need. – aidanmelen Jul 05 '21 at 17:12
  • Not working with the latest docker `Docker version 20.10.8, build 3967b7d` – Marcello DeSales Sep 19 '21 at 04:00
  • @MarcellodeSales are you absolutely sure you're using the wildcard to conditionally copy a file at build time? It works for me with `Docker version 20.10.8, build 3967b7d`. no problems at all. – aidanmelen Sep 24 '21 at 02:50
  • `COPY scripts/container/initdb/dev/*initdb-01.sh /docker-entrypoint-initdb.d/` does not work on `Docker version 19.03.14, build 5eb3275d40`. The directory also exists exists, only the file is absent. `COPY` command fails with : Step 11/20 : COPY scripts/container/initdb/dev/*initdb-01.sh /docker-entrypoint-initdb.d/ COPY failed: no source files were specified FAILED – Binita Bharati May 02 '22 at 04:30
  • A perfect solution! We have a generic `Dockerfile` which uses parameters to decide what the base image is, and then copies in a `package.json` and runs `npm i`. However, some of the packages we are building also want to run a postinstall step. This solution lets us copy in the postinstall script when it's present, and do nothing otherwise. Thanks! – Mark Birbeck Jun 22 '22 at 19:51
  • Doesn't appear to work when the optional file is in a subdirectory, i.e. `COPY subdir/*sometimes_exist_on_host.txt .` – Chuck Batson Dec 02 '22 at 19:32
  • it does, please double check the example – aidanmelen Dec 16 '22 at 19:59
15

Copy all files to a throwaway dir, hand pick the one you want, discard the rest.

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

You can achieve something similar using build stages, which relies on the same solution, using cp to conditionally copy. By using a build stage, your final image will not include all the content from the initial 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
cdosborn
  • 3,111
  • 29
  • 30
  • 1
    While this technically solves the problem, it does not decrease the size of the image. If you are trying to conditionally copy something huge (like a deep network model) you still inflate the size of the image, due to the way overlay fs works. – DeusXMachina Mar 13 '20 at 17:56
  • @DeusXMachina, what version of docker are you using? The docs contradict what you're saying https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds. The layers should not be preserved from a non-final build stage. – cdosborn Mar 13 '20 at 18:22
  • @cdosburn - I have observed this on 18.09. I was speaking mostly about the first example, staged builds should avoid that issue. And I think every FROM stage compactifies now, but you have me second guessing my recollection. I will have to experiment with some things. – DeusXMachina Mar 13 '20 at 18:33
  • @DeusXMachina, right only the second solution reduces image size. – cdosborn Mar 13 '20 at 19:11
  • that is nice workaround for my case. I copy a `cache` and depending whats it the cache I choose what to do in script files! – Paschalis Apr 08 '20 at 01:51
14

Work around Solution

I had requirement on copy FOLDER to server based on ENV Variables. I took the empty server image. created required deployment folder structure at in local folder. then added below line to DockerFile copy the folder to container. In last line added entry point to execute init file.sh before docker start the server.

#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"]

Then create the custom-init.sh file in local with script something like below

#!/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;

In docker-compose file below lines.

environment: - BUILD_EVN=TEST

These changes copy folder to container during docker build. when we execute docker-compose up it copy or deploy the actual required folder to server before server starts.

  • 9
    But docker images are layered. ADD would copy these to the image regardless of the if statement you mentioned... – MyUserInStackOverflow Jan 31 '17 at 04:50
  • @MyUserInStackOverflow - I think the idea of this "workaround" is that both install and install_test are copied into the image, but when the image is run, only one of those folders is copied to the final location. If its okay that both are somewhere in the image, this could be a reasonable technique. – ToolmakerSteve Apr 04 '19 at 12:06
2

Tried the other ideas, but none met our requirement. The idea is to create base nginx image for child static web applications. For security, optimization, and standardization reasons, the base image must be able to RUN commands on directories added by child images. The base image does not control which directories are added by child images. Assumption is child images will COPY resources somewhere under COMMON_DEST_ROOT.

This approach is a hack, but the idea is base image will support COPY instruction for 1 to N directories added by child image. ARG PLACEHOLDER_FILE and ENV UNPROVIDED_DEST are used to satisfy <src> and <dest> requirements for any COPY instruction not needed.

#
# 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

This solution has obvious shortcomings like the dummy PLACEHOLDER_FILE and hard-coded number of COPY instructions that are supported. Also there is no way to get rid of the ENV variables that are used in the COPY instruction.

brianNotBob
  • 548
  • 6
  • 9
1

COPY no longer requires at least one source to exist, and globbing doesn't fail if there are no matches, so you can just

COPY requirements.tx[t] /destination

This will copy requirements.txt if it exists, and won't fail if it doesn't.

Danny W. Adair
  • 12,498
  • 4
  • 43
  • 49
0

I have other workarounds for the same. The idea is to touch the file in the build context and use the copy statement inside the Dockerfile. If the file exists it will just create an empty file and the docker build will not fail. If there is already a file it will just change the time stamp.

touch requirements.txt

and for Dockerfile

FROM python:3.9
COPY requirements.txt .