在使用 Dockerfile 构建镜像时,通常都需要将宿主机上的文件“拷贝”到容器中。Docker 提供了两个相关的指令:ADD 和 COPY。
这两个指令都可以将 Docker Context 中的文件进行“拷贝”到容器,似乎没什么区别。既然没什么区别为啥子 Docker 还要提供两个指令呢?
下面就来具体说明下。音乐,起~
基本语法
ADD 和 COPY 的基本语法是一样的,如下:
ADD/COPY [--chown=<user>:<group>] <src>... <dest>ADD/COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
其中 --chown=<user>:<group> 是可选的,就是用于指令文件所属权限的,不做具体说明。
src 指的是宿主机上的目录,dest 指的是容器目录。
SRC
很重要的一点是 src 必须是 Docker 上下文所在的目录,即执行 docker build 所在的目录,比如下面的示例:
$ pwd/opt/const
当前所处的目录在 /opt/const 下,该目录下有如下文件(其中 conf 是一个目录):
$ lsconf application.properties Dockerfile
如果在当前目录下执行 docker build 命令, src 指的就是当前目录,在 Dockerfile 的 ADD/COPY 指令可以这么写:
FROM debian:buster-slim
WORKDIR app
# ADD 指令
ADD application.properties .
ADD config .
# 或 COPY 指令
COPY application.properties .
COPY config .
却不能这么写:
FROM debian:buster-slim
WORKDIR app
# ADD 指令
ADD /opt/const/application.properties .
ADD /opt/const/config .
# 或 COPY 指令
COPY /opt/const/application.properties .
COPY /opt/const/config .
也就是说 ADD/COPY 能够拷贝的只是执行 docker build 命令所在目录下的文件以及子目录下的文件(一本称为上下文),而不能使用绝对目录或者拷贝其他目录中的文件。比如下面就是不行的:
FROM debian:buster-slim
WORKDIR app
# ADD 指令
ADD ./../test/* .
ADD /opt/test/* .
# 或 COPY 指令
COPY ./../test/* .
COPY /opt/test/* .
DEST
dest 指的是容器目录。容器目录可以使用绝对目录和相对目录,如下:
相对目录:
ADD test.txt relativeDir/
注意:如果在 Dockerfile 中定义了 WORKDIR 上面的相对目录其实是隐式的觉得目录,即:
ADD test.txt <WORKDIR>/relativeDir/
绝对目录:
ADD test.txt /absoluteDir/
ADD/COPY 通配符批量拷贝
ADD/COPY 指令支持通配符形式的拷贝,比如下面的情况:
$ ls
Dockerfile application.properties application-local.properties application-i18n.properties
如果想要将所有以 application 开头的文件拷贝到容器中在 Dockerfile 中可以使用 * 进行批量拷贝:
FROM debian:buster-slim
WORKDIR app
# 使用 ADD
ADD application* .
# 使用 COPY
COPY application* .
另外,也可以匹配以 .properties 为后缀的文件:
FROM debian:buster-slim
WORKDIR app
# 使用 ADD
ADD *.properties .
# 使用 COPY
COPY *.properties .
除了使用 * 通配符匹配还可以使用 ? 进行单字符匹配。
比如下面的情况:
ADD hom?.txt /mydir/
可以匹配的有 home.txt、hom1.txt 但是却不能匹配 homee.txt。
这么一看两者似乎都差不多,那区别在哪里呢?我们就来自定义一个 JDK 进行进行演示具体区别:
使用本地压缩包构建 JDK 镜像
简单点,基于 Debian 制作一个 JDK8 的镜像。到 Oracle 官网下载一个Linux解压版安装包上传到当前目录:
$ ls
jdk-8u271-linux-x64.tar.gz
编写一个 Dockerfile:
FROM debian:buster-slim
RUN mkdir -p /opt/jvm
ADD jdk-8u271-linux-x64.tar.gz /opt/jvm
CMD ["/bin/bash", "-c", "ls -l /opt/jvm"]
注意 ADD 指令,是直接将 tar.gz 包文件上传到容器的 /opt/jvm 目录,并没有做其他任何操作。
现在直接构建:
$ docker build -t custom-jdk:v1 .
之后以交互式方式运行并进入容器:
$ docker run --rm -it custom-jdk:v1 /bin/bash
命令中的 **--rm** 参数指的是容器退出后自动删除。
回车后即可进入容器,之后到容器中的 /opt/jvm 目录下看下安装包信息:

你会发现压缩包文件 jdk-8u271-linux-x64.tar.gz 被自动解压了,就是说 **ADD** 指令会自动解压压缩包文件。
现在再回头看 COPY 指令,Dockerfile 内容不变,仅仅将 ADD 指令修改为 COPY 指令:
FROM debian:buster-slim
RUN mkdir -p /opt/jvm
COPY jdk-8u271-linux-x64.tar.gz /opt/jvm
CMD ["/bin/bash", "-c", "ls -l /opt/jvm"]
直接构建并以交互式方式运行进入容器:
$ docker build -t custom-jdk:v2 .
$ docker run --rm -it custom-jdk:v2 /bin/bash
进入 /opt/jvm 目录后你会发现还是一个压缩包文件:

现在就很明显看去区别了,COPY 对压缩文件不会自动解压。
从上面的示例中我们可以看出 ADD 和 COPY 的一个区别:
**ADD** 指令能够自动识别压缩文件并进行解压,在 Docker 官网文档给的说明是:如果通过 ADD 指令拷贝的文件如果是 tar 压缩文件,在构建容器时会自动解压成相应的文件夹,这些压缩文件格式包括 gzip, bzip2 以及 xz。docker 在构建时如果识别出是相应的压缩格式就会使用 **tar -x** 命令进行解压。
使用网络压缩包构建 JDK 镜像
上面我们使用的是宿主机上的一个 jdk 压缩包文件进行构建了一个 “不完整的 JDK 镜像”,这回咱用网络文件试试能不能构建。
将压缩包上次到网络仓库,必然这里我将文件上次至七牛云:

之后修改上面的 Dockerfile 文件,这次还是先使用 ADD 指令做测试,如下:
FROM debian:buster-slim
RUN mkdir -p /opt/jvm
ADD http://qvpc9jp4s.hn-bkt.clouddn.com/jdk-8u271-linux-x64.tar.gz /opt/jvm
CMD ["/bin/bash", "-c", "ls -l /opt/jvm"]
注意看 ADD 指令,之后开始构建:
$ docker build -t custom-jdk:v3 .
构建过程输出如下:
Step 1/4 : FROM debian:buster-slim
---> 7c5871f12659
Step 2/4 : RUN mkdir -p /opt/jvm
---> Using cache
---> 457c17963ba4
Step 3/4 : ADD http://qvpc9jp4s.hn-bkt.clouddn.com/jdk-8u271-linux-x64.tar.gz /opt/jvm
Downloading [==================================================>] 143.1MB/143.1MB
---> 6400a604ccd0
Step 4/4 : CMD ["/bin/bash", "-c", "ls /opt/jvm"]
---> Running in b8d1779aa00b
Removing intermediate container b8d1779aa00b
---> 7690ab906d80
Successfully built 7690ab906d80
Successfully tagged custom-jdk:v3
注意看 Step 3,它会自动识别网络文件并进行下载。之后的话相应的也会进行解压,就不做具体演示,有兴趣的自己测试就好。
现在再使用 COPY 指令试试:
FROM debian:buster-slim
RUN mkdir -p /opt/jvm
COPY http://qvpc9jp4s.hn-bkt.clouddn.com/jdk-8u271-linux-x64.tar.gz /opt/jvm
CMD ["/bin/bash", "-c", "ls -l /opt/jvm"]
执行构建:

构建的时候就直接提示失败~
现在又体现出了另外一个区别:**ADD** 指令能够自动识别网络文件并进行下载,而 **COPY** 却不行。
我在使用的时候基本上就发现了这些,至于其他的暂时还没发现。不过 ADD 指令明显更加强大,而且 COPY 有的功能 ADD 都有为什么 Docker 还会提供一个 COPY 指令呢?这个在官方文档的最佳实践中有关 COPY 和 ADD 指令有如下一段描述:
Although ADD and COPY are functionally similar, generally speaking, COPY is preferred.
That’s because it’s more transparent than ADD. COPY only supports the basic copying of
local files into the container, while ADD has some features (like local-only tar extraction
and remote URL support) that are not immediately obvious. Consequently, the best use for
ADD is local tar file auto-extraction into the image, as in ADD rootfs.tar.xz /.
官网文档地址:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#add-or-copy
所以改怎么选还是看自己喜欢吧~
上面介绍的都是 COPY 和 ADD 指令在使用上的区别,现在我们就基于 ADD 指令制作一个镜像吧~
开始构建 JDK 镜像
铺垫就不做了,直接写 Dockerfile:
FROM debian:buster-slim
ADD jdk-8u271-linux-x64.tar.gz /usr/local/jdk
ENV JAVA_HOME /usr/local/jdk/jdk1.8.0_271
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib
ENV PATH $JAVA_HOME/bin:$PATH
CMD ["java", "-version"]
开始构建:
$ docker build -t jvm:8 .
运行测试:

这样,一个简单的 JDK 镜像就制作完成了。不过呢,如果在实际使用中还是建议使用 openjdk 吧。
完结,撒花~
