前面提及了 FROMRUN,还提及了 COPY, ADD,其实 Dockerfile 功能很强大,它提供了十多个指令。下面我们继续讲解其他的指令。

COPY

格式:

  • COPY <源路径>… <目标路径>
  • COPY ["<源路径1>",… “<目标路径>”]
    RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。
    COPY指令将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径> 位置。比如:
COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候

ADD

ADD 指令和COPY的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。
如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为gzip, bzip2 以及xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。
比如:

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /

COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用COPY指令,仅在需要自动解压缩的场合使用 ADD

CMD

CMD 指令的格式和RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD [“可执行文件”, “参数1”, “参数2”…]
    参数列表格式:CMD [“参数1”, “参数2”…]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

在指令格式上,一般推荐使用exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。
如果使用 shell格式的话,实际的命令会被包装为sh -c的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]

一些初学者将 CMD写为:

CMD service nginx start

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。
正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

CMD ["nginx", "-g", "daemon off;"]

WORKDIR

格式为 WORKDIR <工作目录路径>
使用 WORKDIR指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

RUN cd /app
RUN echo "hello" > world.txt

如果将这个Dockerfile 进行构建镜像运行后,会发现找不到/app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

更多操作可查看官方文档
lDockerfie 官方文档Dockerfile 最佳实践文档