翻译·原文地址

Docker 可以通过读取 Dockerfile 中的指令自动构建镜像。Dockerfile 是一个文本文档,其中包含用户可以在命令行上调用以组合镜像的所有命令。使用 docker build,用户可以创建一个连续执行多个命令行指令的自动构建。

本页介绍了可以在 Dockerfile 中使用的命令。阅读完本页后,请参阅 Dockerfile 最佳实践 以获取面向技巧的指南。

一、用法

docker build 命令从 Dockerfile 和上下文构建镜像。构建的上下文是 PATHURL 指定位置的文件集合。PATH 是本地文件系统上的目录。URL 是 Git Repository 地址。

上下文被递归处理。因此,PATH 包括其任何子目录,URL 包括 Repository 及其子模块。此示例显示了使用当前目录作为上下文的构建命令:

$ docker build .

构建由 Docker 守护程序运行,而不是由 CLI 运行。构建过程的第一件事是将整个上下文(递归地)发送到守护进程。在大多数情况下,最好从空目录作为上下文开始,并将 Dockerfile 保存在该目录中。仅添加构建 Dockerfile 所需的文件。

警告:不要将根目录 / 用作 PATH,因为它会导致构建将硬盘驱动器的全部内容传输到 Docker 守护程序。

该命令会将当前目录中的所有内容发送至 Docker 守护进程进行处理。Docker 守护进程会读取该目录下的 Dockerfile 文件,并根据文件内容构建镜像。

要在构建上下文中使用文件,Dockerfile 引用指令中指定的文件,例如 COPY 指令。要提高构建的性能,请通过将 .dockerignore 文件添加到上下文目录来排除文件和目录。有关如何创建 .dockerignore 文件的信息,请参阅 六、.dockerignore 文件

习惯上,Dockerfile 放在上下文的根目录中。你也可以将 -f 标记与 docker build 一起使用,以指向文件系统中任何位置的 Dockerfile。

$ docker build -f /path/to/a/Dockerfile .

如果构建成功,您可以指定一个 Repository 和标记以保存新镜像:

$ docker build -t practicemp/myapp .

要在构建后将镜像标记为多个 Repositories,在运行 build 命令时添加多个 -t 参数:

$ docker build -t practicemp/myapp:1.0.2 -t prcticemp/myapp:latest .

在 Docker 守护程序运行 Dockerfile 中的指令之前,它会对 Dockerfile 进行初步验证,如果语法不正确则返回错误:

$ docker build -t test/myapp .
Sending build context to Docker daemon 2.048 kB
Error response from daemon: Unknown instruction: RUNCMD

Docker 守护程序逐个运行 Dockerfile 中的指令,在必要时将每条指令的结果提交为一个新镜像,最后输出新镜像的 ID。Docker 守护程序将自动清理你发送的上下文。

请注意,每条指令都是独立运行的,并且会导致创建新镜像 - 因此 RUN cd /tmp 对下一条指令不会产生任何影响(译注:应使用 WORKDIR 指令切换工作目录)。

只要有可能,Docker将复用中间镜像(缓存),以显着加速 docker build 过程。这由控制台输出中的 Using cache 消息提示。(有关更多信息,请参阅 Dockerfile 最佳实践指南中的构建缓存部分):

$ docker build -t svendowideit/ambassador .
Sending build context to Docker daemon 15.36 kB
Step 1/4 : FROM alpine:3.2
 ---> 31f630c65071
Step 2/4 : MAINTAINER SvenDowideit@home.org.au
 ---> Using cache
 ---> 2a1c91448f5f
Step 3/4 : RUN apk update &&      apk add socat &&        rm -r /var/cache/
 ---> Using cache
 ---> 21ed6e7fbb73
Step 4/4 : CMD env | grep _TCP= | (sed 's/.*_PORT_\([0-9]*\)_TCP=tcp:\/\/\(.*\):\(.*\)/socat -t 100000000 TCP4-LISTEN:\1,fork,reuseaddr TCP4:\2:\3 \&/' && echo wait) | sh
 ---> Using cache
 ---> 7ea8aef582cc
Successfully built 7ea8aef582cc

构建缓存仅用于具有本地父链的镜像。这意味着这些镜像是在以前版本的基础上构建的,或者使用 docker load 加载了整个镜像链。如果你希望使用指定镜像的构建缓存,可以使用 --cache-from 选项指定它。使用 --cache-from 指定的镜像不需要具有父链,可以从其它注册表中拉取。

二、格式

Dockerfile 基本格式如下:

# Comment
INSTRUCTION arguments

指令不区分大小写。但是,惯例是大写,以便更容易地将它们与参数区分开来。

Docker 按顺序在 Dockerfile 中运行指令。Dockerfile 必须由 “FROM” 指令开始FROM 指令指定要构建的基础镜像FROM 之前只允许存在一个或多个 ARG 指令,ARG 指令声明在 Dockerfile 中的 FROM 行中使用的参数。

Docker 将以 开头的行视为注释,除非该行是有效的解析器指令。行中任何其他位置的 标记都被视为参数。这允许这样的表达:

# 注释
RUN echo '我们运行一些 # 有趣的东西'

注释行不支持续行符。

三、解析器指令

解析器指令是可选的,并且会影响处理 Dockerfile 中后续行的方式。解析器指令不会向构建添加镜像,也不会显示为构建步骤。解析器指令是以 #directive=value 的形式编写为特殊类型的注释。单个指令只能使用一次。

一旦处理了注释,空行或构建器指令,Docker 就不再查找解析器指令。相反,它将解析器指令格式的任何内容视为注释,并且不会尝试验证它是否可能是解析器指令。因此,所有解析器指令必须位于 Dockerfile最顶层

解析器指令不区分大小写。但是,惯例是它们是小写的。还约定在任何解析器指令后面添加一个空行。解析器指令不支持续行符。

由于以上规则,以下示例均无效:

由于续行而无效:

# direc \
tive=value

由于出现两次而无效:

# directive=value1
# directive=value2

FROM ImageName

出现在构建指令后面而被视为注释:

FROM ImageName
# directive=value

出现在注释而非解析器指令后面被视为注释:

# About my dockerfile
# directive=value
FROM ImageName

由于未被识别,未知指令被视为注释。此外,由于出现在不是解析器指令的注释之后,已知指令被视为注释。

# unknowndirective=value
# knowndirective=value

解析器指令中允许使用非断行空格。因此,以下几行都是相同的:

#directive=value
# directive =value
#    directive= value
# directive = value
#      dIrEcTiVe=value

可用的解析器指令有:

  • escape

四、escape

# escape=\

或者

# escape=`

escape指令指定用于转义 Dockerfile 中字符的字符。如果未指定,则默认转义字符为 \

转义字符即可用于转义,也用来当做续行符。这允许 Dockerfile 指令跨越多行。请注意,无论 escape 解析器指令是否包含在 Dockerfile 中,都不会在 RUN 命令中执行转义,除非在该行的末尾作为续行符使用。

对于 Windows 镜像来说,将转义字符设置为 ` 非常有用,因为 \ 被用来当做路径分隔符。Windows PowerShell 中的转义字符正是 `

参考以下示例,该示例在 Windows 系统上将以不显著的方式失败。在第二行尾部的第二个 \ 将被解析为续行符。而不是被第一个 \ 转义的字符。类似地,第三行尾部的 \ 也会被当做续行符处理。解析这个 Dockerfile 的结果就是,第二行和第三行被当做一个指令:

FROM microsoft/nanoserver
COPY testfile.txt c:\\
RUN dir c:\

结果为:

PS C:\John> docker build -t cmd .
Sending build context to Docker daemon 3.072 kB
Step 1/2 : FROM microsoft/nanoserver
 ---> 22738ff49c6d
Step 2/2 : COPY testfile.txt c:\RUN dir c:
GetFileAttributesEx c:RUN: The system cannot find the file specified.
PS C:\John>

解决方案之一是使用 / 作为路径分隔符。但是这种写法对于 Windows 路径来说并不正常,并且所有 Windows 系统上的命令都不支持 / 作为路径分隔符。

通过添加 escape 解析器指令,下面的 Dockerfile 按预期成功使用 Windows 上的文件路径的正常平台语义:

# escape=`

FROM microsoft/nanoserver
COPY testfile.txt c:\
RUN dir c:\

结果为:

PS C:\John> docker build -t succeeds --no-cache=true .
Sending build context to Docker daemon 3.072 kB
Step 1/3 : FROM microsoft/nanoserver
 ---> 22738ff49c6d
Step 2/3 : COPY testfile.txt c:\
 ---> 96655de338de
Removing intermediate container 4db9acbb1682
Step 3/3 : RUN dir c:\
 ---> Running in a2c157f842f5
 Volume in drive C has no label.
 Volume Serial Number is 7E6D-E0F7

 Directory of c:\

10/05/2016  05:04 PM             1,894 License.txt
10/05/2016  02:22 PM    <DIR>          Program Files
10/05/2016  02:14 PM    <DIR>          Program Files (x86)
10/28/2016  11:18 AM                62 testfile.txt
10/28/2016  11:20 AM    <DIR>          Users
10/28/2016  11:20 AM    <DIR>          Windows
           2 File(s)          1,956 bytes
           4 Dir(s)  21,259,096,064 bytes free
 ---> 01c7f3bef04f
Removing intermediate container a2c157f842f5
Successfully built 01c7f3bef04f
PS C:\John>

五、环境切换

环境变量(使用 ENV 语句声明)可以被某些指令所引用。如果要使用变量格式的字符串,可以使用转义进行处理。

使用 $variable_name 或者 ${variable_name} 来引用环境变量。两种写法是等效的。大括号写法通常用于解决变量名没有空格的地址问题,如 ${foo}_bar

${variable_name} 语法还支持以下指定的一些标准 bash 修饰符:

  • ${variable:-word} 表示:如果 variable 没有定义,word 将被输出。
  • ${variable:+word} 表示:如果 variable 已定义,word 将被输出,否则输出一个空字符串。

在任何情况下,word 可以是任何字符串,包括可以是其它环境变量的引用。

可以在变量之前添加 \ 进行转义:\$foo 或者 \${foo},这会将其解析为字符串 $foo${foo}

例如(在 # 后显示解析后的表现):

FROM busybox
ENV foo /bar
WORKDIR ${foo}    # WORKDIR /bar
ADD . $foo        # ADD . /bar
COPY \$foo /quux  # COPY $foo /quux

环境变量可以被下面的指令所引用:

  • ADD
  • COPY
  • ENV
  • EXPOSE
  • FROM
  • LABEL
  • STOPSIGNAL
  • USER
  • VOLUME
  • WORKDIR

以及

  • ONBUILD (当与上面支持的指令之一结合使用时)

注意:在 1.4 之前,ONBUILD 指令支持环境变量,即使与上面列出的任何指令结合使用也是如此。

一个完整的指令中,对环境变量的置换使用相同的值。换句话说,在这个例子中:

ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

def 的值为 hello,而不是 bye。然而,ghi 的值是 bye,因为它不在将 abc 设置为 bye 的那一行中。

六、.dockerignore 文件

在 Docker CLI 将上下文发送给 Docker 守护进程之前,它将会在上下文的根目录中寻找名为 .dockerignore 的文件。如果存在该文件,CLI 将会修改上下文以排除在该文件中匹配到的文件和目录。这有助于避免将大型或敏感的文件或目录发送给 Docker 守护进程,并且避免使用 ADD 或者 COPY 将其添加到镜像中。

CLI 将 .dockerignore 文件内容解析为由其每行模式组成的列表,模式类似于 Unix shell 中的 file globs。出于匹配的目的,上下文的根被当做是工作目录和根目录。例如,/foo/barfoo/bar 模式都会在 PATHfoo 子目录中或位于 URL 的 git repository 的根目录中排除名为 bar 的文件或目录。

如果 .dockerignore 中的一行以 # 开头,则这一行被视为注释并在 CLI 解析之前被忽略。

这是一个 .dockerignore 文件的示例:

# comment
*/temp*
*/*/temp*
temp?

这个文件会引起下面的构建行为:

规则 行为
# comment 被忽略
*/temp* 在根目录的直接子目录中排除所有以 temp 开头的文件或者目录。例如,纯文本文件 /somedir/temporary.txt 会被排除,同样目录 /somedir/temp 也会被排除。
*/*/temp* 在根目录的二级子目录中排除所有以 temp 开头的文件或者目录。例如,/somedir/subdir/temporary.txt 会被排除。
temp? ? 匹配单字符,在根目录中,排除所有以 temp 开头的 5 字符文件和目录。例如,/tempa/tempb 会被排除。

匹配是使用 Go 语言的 filepath.Match 规则完成的。在匹配之前会进行预处理,预处理会移除首尾的空格,并使用 Go 语言的 filepath.Clean 消除 ... 元素。空行在预处理后将被忽略。

除了 Go 语言的 filepath.Match 规则之外,Docker 还支持一个特别的通配符 **:匹配任何层级的目录。例如,**/*.go 将排除在所有目录中找到的以 .go 结尾的文件,包括上下文的根中。

! (感叹号)开头的行用来标记排除的例外规则。下面是一个使用该机制的示例:

*.md
!README.md

这将会排除上下文中所有 markdown 文件,除了 README.md

另外,! 规则的位置会影响其行为:.dockerignore 最后一行匹配到文件将会决定 ! 规则是包括还是被排除。考虑下面的例子:

*.md
!README*.md
README-secret.md

除了 README-secret.md 之外的 README 文件,所有 markdown 文件都将被排除。

再考虑下面的例子:

*.md
README-secret.md
!README*.md

所有的 README 文件都不会被排除。第二行不起作用,因为 !README*.md 匹配到了 README-secret.md 并且更靠后。

你甚至可以用 .dockerignore 文件来排除 Dockerfile.dockerignore本身。这两个文件还是会被发送给 Docker 守护进程,因为守护进程的需要它们来完成工作。但是 ADDCOPY 指令则不会将这个两个文件拷贝到镜像。

最后,你可能想要指定哪些文件会在上下文中被包含,而不是被排除。要想这样,指定 * 作为第一个匹配规则,接着指定一个或多个 ! 规则。

注意:由于历史原因,. 模式会被忽略。

七、FROM

FROM <image> [AS <name>]

或者

FROM <image>[:<tag>] [AS <name>]

或者

FROM <image>[@<digest>] [AS <name>]

FROM 指令初始化新的构建阶段并为后续指令设置基础镜像。因此,有效的 Dockerfile 必须以 FROM 指令开头。基础镜像可以是任何有效镜像 - 通过从公共 Repositories 中拉取镜像尤其容易。

  • Dockerfile 文件中,ARG 是唯一可以在 FROM 之前出现的指令,见 理解 ARG 和 FORM 如何相互影响
  • 在一个 Dockerfile 文件中,FROM 可以出现多次,来创建多个镜像或使用一个构建阶段作为其它构建阶段的依赖。在每个 FROM 指令之前简单记录提交输出的最后一个镜像 ID。每个 FROM 指令会清除之前指令生成的任何声明。
  • 通过为 FORM 指令添加 AS name 来给一个新构建阶段命名。这个名字可以被随后的 FROMCOPY --from=<name|index> 指令使用,用来指向在这个阶段构建的镜像。
  • tagdigest 是可选的。如果你把这两个都省略了,构建器默认会指定 latest 为其标记。如果构建器不能找到 tag 值,将会返回一个错误。

7.1 理解 ARG 和 FORM 如何相互影响

FROM 指令支持在第一个 FROM 之前出现的任何 ARG 指令声明的变量。

ARG  CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD  /code/run-app

FROM extras:${CODE_VERSION}
CMD  /code/run-extras

FROM 之前声明的 ARG 在构建阶段之外,因此在 FROM 之后的任何指令中都不能使用它。要使用在第一个 FROM 之前声明的 ARG 的默认值,请使用构建阶段内的无值的 ARG指令:

ARG VERSION=latest
FROM busybox:$VERSION
ARG VERSION
RUN echo $VERSION > image_version

八、RUN

RUN 有两种形式:

  • RUN <command>shell 形式,命令在 shell 中执行,Linux 上默认是 /bin/sh -c,Windows 上默认是 cmd /S /C
  • RUN ["executable", "param1", "param2"]exec 形式)

RUN 指令可以在当前镜像之上的一层执行任何命令并提交结果。作为提交结果的镜像将被用于 Dockerfile 中下一步骤。

分层的 RUN 指令和生成提交符合 Docker 的核心概念:提交的成本很低,并且可以从镜像历史中的任何点创建容器,就像源代码控制一样(译注:类似 git 的版本控制)。

exec 形式可以避免 shell 字符串重写,并可使用不包含指定 shell 可执行文件的基础镜像来运行 RUN 指令。

可以使用 SHELL 指令更改 shell 形式的默认 shell。

shell 模式中,你可以使用 \(反斜杠)将单个 RUN 指令延续到下一行。例如,考虑下面两行:

RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'

上面两行相当于下面一行:

RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'

注意:要使用除 ‘/bin/sh’ 之外的其它 shell,使用 exec 形式传入所需的 shell。例如:RUN ["/bin/bash", "-c", "echo hello"]

注意exec 形式被解析为一个 JSON 数组,这意味着你必须使用双引号包裹字符串,而不是单引号。

注意:不同于 shell 形式,exec 形式不会调用 shell。这意味着不会发生正常的 shell 处理。例如,RUN [ "echo", "$HOME" ] 不会对 $HOME 执行变量替换。如果你想要使用 shell 处理,要么使用 shell 形式,要么直接运行一个 shell,例如:RUN [ "sh", "-c", "echo $HOME" ]。当使用 exec 形式直接运行一个 shell 时,和使用 shell 形式的效果一样,它执行的是环境变量扩展的 shell,而不是 Docker。

注意:在 exec 形式中的 JSON,必须转义反斜杠。这在反斜杠作为路径分隔符的 Windows 上尤为重要。由于不是有效的 JSON,下面一行将被视为 shell 形式,并以意外方式失败:
RUN ["c:\windows\system32\tasklist.exe"]
正确的语法是:RUN ["c:\\windows\\system32\\tasklist.exe"]

RUN 指令的缓存在下一次构建期间不会自动失效。像 RUN apt-get dist-upgrade -y 这样指令的缓存将在下一次构建期间重用。可以使用 --no-cache 标记使 RUN 指令的缓存失效,例如 docker build --no-cache

查看 Dockerfile 最佳实践 了解更多。

ADD 指令可以使 RUN 的缓存失效,详见 ADD

8.1 已知问题(RUN)

  • Issue 783 是关于使用 AUFS 文件系统时可能出现的文件权限问题。例如,在尝试 rm 一个文件时,你可能会注意到它。

    对于具有较新的 AUFS 版本的系统(即,可以设置 dirperm1 挂载选项),Docker 将尝试通过使用 dirperm1 选项挂载层来自动修复问题。有关 dirperm1 选项的更多详细信息,请参见 AUFS 手册页

    如果你的系统不支持 dirperm1,则该问题中描述了一种解决方法。

九、CMD

CMD 指令有三种形式:

  • CMD ["executable","param1","param2"]exec 形式,这是首选形式)
  • CMD ["param1","param2"]作为 ENTRYPOINT 的默认参数
  • CMD command param1 param2shell 形式)

一个 Dockerfile 文件中只能有一条 CMD 指令。如果列出多个 CMD,则只有最后一个 CMD 才会生效。

CMD 的主要目的是为可执行容器提供默认值。这些默认值可以包含可执行文件,也可以省略可执行文件,在省略的情况下,你还必须指定 ENTRYPOINT 指令。

注意:如果 CMD 用于为 ENTRYPOINT 指令提供默认参数,则应使用 JSON 数组格式指定 CMDENTRYPOINT 指令。

注意exec 形式被解析为一个 JSON 数组,这意味着应该使用双引号而不是单引号来包裹字符串。

注意:不同于 shell 形式,exec 形式不会调用 shell。这意味着不会发生正常的 shell 处理。例如,CMD [ "echo", "$HOME" ] 不会对 $HOME 执行变量替换。如果你想要使用 shell 处理,要么使用 shell 形式,要么直接运行一个 shell,例如:CMD [ "sh", "-c", "echo $HOME" ]。当使用 exec 形式直接运行一个 shell 时,和使用 shell 形式的效果一样,它执行的是环境变量扩展的 shell,而不是 Docker。

shellexec 形式使用时,CMD 指令设置运行镜像时要执行的命令。

如果你使用 CMDshell 形式,那么 <command> 将在 /bin/sh -c 中执行:

FROM ubuntu
CMD echo "This is a test." | wc -

如果想要在没有 shell 的情况下运行 <command>,则必须将该命令表示为 JSON 数组,并提供可执行文件的完整路径。数组形式是 CMD 的首选格式。任何其它参数必须在数组中单独表示为字符串:

FROM ubuntu
CMD ["/usr/bin/wc","--help"]

如果你希望容器每次都运行相同的可执行文件,那么你应该考虑将 ENTRYPOINTCMD 结合使用。请参阅 ENTRYPOINT

如果用户指定了 docker run 的参数,那么它们将覆盖 CMD 中指定的默认值。

注意:不要将 RUNCMD 混淆。RUN 实际上运行一个命令并提交结果; CMD 在构建时不执行任何操作,但指定了镜像的预期命令。

十、LABEL

LABEL <key>=<value> <key>=<value> <key>=<value> ...

LABEL 指令将元数据添加到镜像。LABEL 以键值对表现。要在 LABEL 值中包含空格,请使用引号和反斜杠,就像在命令行解析中一样。一些用法示例:

LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."

一个镜像可以有不止一个标签。你可以在单独一行中指定多个标签。在 Docker 1.10 之前,这减小了最终镜像的大小,但现在不再是这种情况了。你仍然可以选择在单个指令中指定多个标签,方法有以下两种:

LABEL multi.label1="value1" multi.label2="value2" other="value3"
LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"

基础或父镜像(FROM 行中的镜像)中包含的标签由你的镜像继承。如果标签已存在但具有不同的值,则最新应用的值将覆盖任何先前设置的值。

要查看镜像的标签,请使用 docker inspect 命令。

"Labels": {
    "com.example.vendor": "ACME Incorporated",
    "com.example.label-with-value": "foo",
    "version": "1.0",
    "description": "This text illustrates that label-values can span multiple lines.",
    "multi.label1": "value1",
    "multi.label2": "value2",
    "other": "value3"
},

十一、MAINTAINER (不赞成使用)

MAINTAINER <name>

MAINTAINER 指令设置生成镜像的 Author 字段。但LABEL 指令是一个更加灵活的形式,你应该使用 LABEL,因为它可以设置你需要的任何元数据,并且可以轻松查看,例如使用 docker inspect 命令。设置对应于MAINTAINER字段的标签,你可以使用:

LABEL maintainer="SvenDowideit@home.org.au"

然后,这将通过 docker inspect 与其它标签一起显示。

十二、EXPOSE

EXPOSE <port> [<port>/<protocol>...]

EXPOSE 指令通知 Docker 容器在运行时侦听指定的网络端口。你可以指定侦听 TCP 还是 UDP 端口,如果未指定协议,则默认为 TCP。

EXPOSE 指令实际上并不开放端口。它的功能是作为构建镜像的人和运行容器的人之间的一种文档,用于有意要开放的端口。要在运行容器时实际开放端口,请在 docker run 上使用 -p 选项开放和映射一个或多个端口,或使用 -P 选项开放所有公开的端口并将它们映射到高序位端口。

默认情况下,EXPOSE 假定为 TCP。你还可以指定为 UDP:

EXPOSE 80/udp

如果 TCP 和 UDP 都指定,请包含两行:

EXPOSE 80/tcp
EXPOSE 80/udp

在这种情况下,如果将 -Pdocker run 一起使用,则端口将针对 TCP 开放一次,针对 UDP 开放一次。请记住,-P 在主机上使用暂时的高序位主机端口,因此 TCP 和 UDP 的端口不一样。

无论 EXPOSE 设置如何,你都可以使用 -p 选项在运行时覆盖它们。例如:

docker run -p 80:80/tcp -p 80:80/udp ...

要在主机系统上设置端口重定向,参阅 使用 -P 选项docker network 命令支持创建用于容器之间通信的网络,而无需公开或发布特定端口,因为连接到网络的容器可以通过任何端口相互通信。有关详细信息,请参阅 此功能的概述)。

十三、ENV

ENV <key> <value>
ENV <key>=<value> ...

ENV 指令将环境变量 <key> 设置为值 <value>。此值将存在于构建阶段中所有后续指令的环境中,并且也可以进行替换。

ENV 指令有两种形式。第一种形式,ENV <key> <value>,将单个变量设置为一个值。第二个空格后面的整个字符串将被视为 <value> - 包括空格字符。该值将针对其它环境变量进行解释,因此如果未对引号进行转义,则将删除引号。

第二种形式,ENV <key> = <value> ...,允许一次设置多个变量。请注意,第二种形式在语法中使用等号(=),而第一种形式则不然。与命令行解析一样,引号和反斜杠可用于在值内包含空格。

例如:

ENV myName="John Doe" myDog=Rex\ The\ Dog \
    myCat=fluffy

ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy

将在最终镜像中产生相同的结果。

当从生成的镜像运行容器时,使用 ENV 设置的环境变量将保持不变。你可以使用 docker inspect 查看值,并使用 docker run --env <key> = <value> 更改它们。

注意:环境固化可能会导致意外的副作用。例如,设置 ENV DEBIAN_FRONTEND noninteractive 可能会使基于 Debian 的镜像上的 apt-get 用户感到困惑。要为单个命令设置值,请使用 RUN <key> = <value> <command>

十四、ADD

ADD 有两种形式:

  • ADD [--chown=<user>:<group>] <src>... <dest>
  • ADD [--chown=<user>:<group>]["<src>",... "<dest>"](如果路径中包含空格则使用这种形式)

注意--chown 功能仅在用于构建 Linux 容器的 Dockerfiles 上受支持,并不适用于 Windows 容器。由于用户和组所有权概念不能在 Linux 和 Windows 之间进行转换,因此使用 /etc/passwd/etc/group 将用户名和组名转换为 ID 会限制此功能仅适用于基于 Linux OS 的容器。

ADD 指令从 <src> 复制文件,目录或远程 URL 文件,并将它们添加到路径 <dest> 的镜像文件系统中。

可以指定多个 <src> 资源,但如果它们是文件或目录,则它们的路径将被解释为相对于构建上下文的根。

<src> 可以包含通配符,匹配将使用 Go 语言的 filepath.Match 规则完成。例如:

ADD hom* /mydir/        # 添加所有已 "hom" 开头的文件
ADD hom?.txt /mydir/    # ? 匹配单个字符, 例如, "home.txt"

<dest> 是一个绝对路径,或相对于 WORKDIR 的路径,<src> 将在目标容器中被复制到该路径中。

ADD test relativeDir/          # 添加 "test" 到 `WORKDIR`/relativeDir/
ADD test /absoluteDir/         # 添加 "test" 到 /absoluteDir/

当添加包含特殊字符(例如 [])的文件或目录时,需要按照 Golang 规则转义这些路径,以防止它们被视为匹配模式。例如,要添加名为 arr[0].txt 的文件,使用以下命令:

ADD arr[[]0].txt /mydir/    # 拷贝一个文件名为 "arr[0].txt" 的文件到 /mydir/

Docker 使用 UID 和 GID 为 0 来创建所有新文件和目录,除非使用可选的 --chown 标记指定给定的用户名,组名或 UID/GID 组合以指定添加内容的特定所有权。--chown 标记的格式既允许字符串形式的用户名和组名,也允许与任意整数 UID 和 GID 组合。如果提供没有组名的用户名或没有 GID 的 UID,将使用与 GID 相同的数字的 UID。如果提供了用户名或组名,则容器的根文件系统中的 /etc/passwd/etc/group 文件将分别用于执行从名称到整数UID或GID的转换。以下示例显示了 --chown 标记的有效定义:

ADD --chown=55:mygroup files* /somedir/
ADD --chown=bin files* /somedir/
ADD --chown=1 files* /somedir/
ADD --chown=10:11 files* /somedir/

如果容器根文件系统不包含 /etc/passwd/etc/group 文件,并且在 --chown 标记中使用了用户名或组名,ADD 操作将会导致构建失败。而使用数字 ID 则不需要查找,也不依赖于容器根文件系统内容。

<src> 是远程文件 URL 的情况下,<dest> 将具有 600 的权限。如果正在检索的远程文件具有 HTTP Last-Modified 标头,则该标头的时间戳将用于设置 <dest> 文件上的 mtime。但是,与 ADD 处理的任何其它文件期间一样,mtime 将不影响判断文件是否已更改导致更新缓存。

注意:如果通过 STDIN(docker build - < somefile)输入重定向传递 Dockerfile 来构建,则不存在构建上下文,因此 Dockerfile 只能包含基于 URL 的 ADD 指令。你还可以通过 STDIN 传递压缩文件:(docker build - < archive.tar.gz),文件根目录下的 Dockerfile 和文件的其余部分将用作构建的上下文。

注意:如果你的 URL 文件使用身份验证进行保护,则需要使用 RUN wgetRUN curl或使用容器内的其它工具,因为 ADD 指令不支持身份验证。

注意:如果 <src> 的内容已更改,则第一个遇到的 ADD 指令将使 Dockerfile 的所有后续指令的缓存失效。这包括使 RUN 指令的缓存失效。有关详细信息,请参阅 Dockerfile 最佳实践指南

ADD 遵守以下规则:

  • <src> 路径必须位于构建的上下文中; 你不能添加 ADD ../something /something,因为 docker build 的第一步是将上下文目录(和子目录)发送到 Docker 守护进程。
  • 如果 <src> 是一个 URL 且 <dest> 不以斜杠结尾,则从 URL 下载文件并将其复制到 <dest>
  • 如果 <src> 是一个 URL 并且 <dest> 以斜杠结尾,则从 URL 推断文件名,并将文件下载到 <dest>/<filename>。例如,ADD http://example.com/foobar / 将创建文件 /foobar。这种情况下,URL 必须具有合理路径,以便可以发现适当的文件名(http://example.com 将不起作用)。
  • 如果 <src> 是目录,则复制目录的全部内容,包括文件系统元数据。

注意:不复制目录本身,只复制其内容。

  • 如果 <src> 是可识别的压缩格式(identity,gzip,bzip2 或 xz)的本地 tar 归档文件,则将其解压缩为目录。远程 URL 中的资源会被解压缩。当一个目录被复制或解压缩时,它与 tar -x 具有相同的表现,结果是下面两个的结合:

    1. Whatever existed at the destination path and

    2. The contents of the source tree, with conflicts resolved in favor of “2.” on a file-by-file basis.

    (译注:上面之所以贴出原文,是因为原文偏于理论,难以理解。这里我用例子解释一下:上面 2 条说的就是如何合并 <src><dest> 的内容。如果 <src> 为目录或 tar 归档文件,假设,<src> 中存在文件 ab<dest> 中存在文件 bcADD 后,<dest> 的内容为 abc,其中 b 来自 <src>。也就是说在合并的过程中,如果发生文件冲突,则优先使用来自 <src> 的文件。更进一步,如果 <src><dest> 中都含有目录 foo,那么 foo 中文件的合并也会按照前面的原则执行。合并过程会递归处理 <src><dest> 中的目录。)

    注意:文件是否被识别为可识别的压缩格式仅基于文件的内容而不是文件的名称。例如,如果一个空文件恰好以 .tar.gz 结尾,则不会将其识别为压缩文件,也不会生成任何类型的解压缩错误消息,而是将文件简单地复制到目标。

  • 如果 <src> 是任何其它类型的文件,则将其与其元数据一起单独复制。在这种情况下,如果 <dest> 以斜杠 / 结束,则将其视为目录,<src> 的内容将写入 <dest>/base(<src>)

  • 如果直接指定或使用通配符指定了多个 <src> 资源,则 <dest> 必须是目录,并且必须以斜杠 / 结尾。
  • 如果 <dest> 不以斜杠结尾,则它将被视为常规文件,<src> 的内容将写入 <dest>
  • 如果 <dest> 不存在,则会在其路径中创建所有缺少的目录。

十五、COPY

COPY 有两种形式:

  • COPY [--chown=<user>:<group>] <src>... <dest>
  • COPY [--chown=<user>:<group>]["<src>",... "<dest>"] (如果路径中包含空格则使用这种形式)

注意--chown 功能仅在用于构建 Linux 容器的 Dockerfiles 上受支持,并不适用于 Windows 容器。由于用户和组所有权概念不能在 Linux 和 Windows 之间进行转换,因此使用 /etc/passwd/etc/group 将用户名和组名转换为 ID 会限制此功能仅适用于基于 Linux OS 的容器。

COPY 指令从 <src> 复制新文件,目录或远程 URL 文件,并将它们添加到路径 <dest> 的镜像文件系统中。

可以指定多个 <src> 资源,但如果它们是文件或目录,则它们的路径将被解释为相对于构建上下文的根。

每个 <src> 可以包含通配符,匹配将使用 Go 语言的 filepath.Match 规则完成。例如:

COPY hom* /mydir/        # 添加所有已 "hom" 开头的文件
COPY hom?.txt /mydir/    # ? 匹配单个字符, 例如, "home.txt"

<dest> 是一个绝对路径,或相对于 WORKDIR 的路径,<src> 将在目标容器中被复制到该路径中。

COPY test relativeDir/   # 添加 "test" 到 `WORKDIR`/relativeDir/
COPY test /absoluteDir/  # 添加 "test" 到 /absoluteDir/

当添加包含特殊字符(例如 [])的文件或目录时,需要按照 Golang 规则转义这些路径,以防止它们被视为匹配模式。例如,要添加名为 arr[0].txt 的文件,使用以下命令:

COPY arr[[]0].txt /mydir/    # 拷贝一个文件名为 "arr[0].txt" 的文件到 /mydir/

Docker 使用 UID 和 GID 为 0 来创建所有新文件和目录,除非使用可选的 --chown 标记指定给定的用户名,组名或 UID/GID 组合以指定添加内容的特定所有权。--chown 标记的格式既允许字符串形式的用户名和组名,也允许与任意整数 UID 和 GID 组合。如果提供没有组名的用户名或没有 GID 的 UID,将使用与 GID 相同的数字的 UID。如果提供了用户名或组名,则容器的根文件系统中的 /etc/passwd/etc/group 文件将分别用于执行从名称到整数UID或GID的转换。以下示例显示了 --chown 标记的有效定义:

COPY --chown=55:mygroup files* /somedir/
COPY --chown=bin files* /somedir/
COPY --chown=1 files* /somedir/
COPY --chown=10:11 files* /somedir/

如果容器根文件系统不包含 /etc/passwd/etc/group 文件,并且在 --chown 标记中使用了用户名或组名,COPY 操作将会导致构建失败。而使用数字ID则不需要查找,也不依赖于容器根文件系统内容。

注意:如果通过 STDIN(docker build - < somefile)输入重定向传递 Dockerfile 来构建,则不存在构建上下文,这种情况不能使用 COPY

COPY 接受一个可选的标记 --from=<name|index>,可用于将源位置设置为先前的构建阶段(使用 FROM .. AS <name> 创建),而不是由用户发送的构建上下文。该标记还接受为使用 FROM 指令启动的所有先前构建阶段分配的数字索引。如果找不到具有指定名称的构建阶段,则尝试使用具有相同名称的镜像。

COPY 遵守以下规则:

  • <src> 路径必须位于构建的上下文中; 你不能添加 COPY ../something /something,因为 docker build 的第一步是将上下文目录(和子目录)发送到 Docker 守护进程。

  • 如果 <src> 是目录,则复制目录的全部内容,包括文件系统元数据。

注意:不复制目录本身,只复制其内容。

  • 如果 <src> 是任何其它类型的文件,则将其与其元数据一起单独复制。在这种情况下,如果 <dest> 以斜杠 / 结束,则将其视为目录,<src> 的内容将写入 <dest>/base(<src>)
  • 如果直接指定或使用通配符指定了多个 <src> 资源,则 <dest> 必须是目录,并且必须以斜杠 / 结尾。
  • 如果 <dest> 不以斜杠结尾,则它将被视为常规文件,<src> 的内容将写入 <dest>
  • 如果 <dest> 不存在,则会在其路径中创建所有缺少的目录。

十六、ENTRYPOINT

ENTRYPOINT 有两种形式:

  • ENTRYPOINT ["executable", "param1", "param2"]exec 形式,首选)
  • ENTRYPOINT command param1 param2shell 形式)

ENTRYPOINT 允许你配置一个可执行的容器。

例如,下面将使用其默认内容启动 nginx,侦听端口 80:

docker run -i -t --rm -p 80:80 nginx

docker run <image> 的命令行参数将被追加在 ENTRYPOINT exec 形式中的所有参数后,并将覆盖使用 CMD 指定的所有参数。这允许将参数传递给 entry point,即 docker run <image> -d 将会把 -d 参数传递给 entry point。你可以使用 docker run --entrypoint 标记覆盖 ENTRYPOINT 指令。

shell 形式阻止使用任何 CMDrun 命令行参数,但缺点是 ENTRYPOINT 将作为 /bin/sh -c 的子命令启动,不传递信号。这意味着可执行文件不是容器的 PID 1 - 并且不会接收 Unix 信号 - 因此你的可执行文件将不会从 docker stop <container> 接收 SIGTERM。

只有 Dockerfile 中的最后一个 ENTRYPOINT 指令才会生效。

16.1 Exec 形式的 ENTRYPOINT 示例

你可以使用 ENTRYPOINTexec 形式设置相当稳定的默认命令和参数,然后使用任一形式的 CMD 来设置有可能更改的其它默认值。

FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

运行容器时,你可以看到 top 是唯一的进程:

$ docker run -it --rm --name test  top -H
top - 08:25:00 up  7:27,  0 users,  load average: 0.00, 0.01, 0.05
Threads:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.1 us,  0.1 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:   2056668 total,  1616832 used,   439836 free,    99352 buffers
KiB Swap:  1441840 total,        0 used,  1441840 free.  1324440 cached Mem

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
    1 root      20   0   19744   2336   2080 R  0.0  0.1   0:00.04 top

要进一步检查结果,可以使用 docker exec

$ docker exec -it test ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  2.6  0.1  19752  2352 ?        Ss+  08:24   0:00 top -b -H
root         7  0.0  0.1  15572  2164 ?        R+   08:25   0:00 ps aux

并且你可以优雅地使用 docker stop test 关闭 top

下面的 Dockerfile 展示使用 ENTRYPOINT 在前台运行 Apache(即,作为 PID 1):

FROM debian:stable
RUN apt-get update && apt-get install -y --force-yes apache2
EXPOSE 80 443
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]

如果需要为单个可执行文件编写启动脚本,可以使用 execgosu 命令确保最终的可执行文件接收到 Unix 信号:

#!/usr/bin/env bash
set -e

if [ "$1" = 'postgres' ]; then
    chown -R postgres "$PGDATA"

    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi

    exec gosu postgres "$@"
fi

exec "$@"

最后,如果你需要在关机时进行一些额外的清理(或与其它容器通信),或者协调多个可执行文件,你可能需要确保 ENTRYPOINT 脚本接收到 Unix 信号,传递它们,然后做一些工作:

#!/bin/sh
# 注意: 我使用 sh 写是为了也能够在 busybox 容器中使用

# 使用 trap 是为了,如果你需要在服务停止时做一些手动清理,
# 或者在容器中开启多个服务。
trap "echo TRAPed signal" HUP INT QUIT TERM

# 在后台开启服务
/usr/sbin/apachectl start

echo "[hit enter key to exit] or run 'docker stop <container>'"
read

# 停止服务并清理
echo "stopping apache"
/usr/sbin/apachectl stop

echo "exited $0"

如果使用 docker run -it --rm -p 80:80 --name test apache 运行此镜像,则可以使用 docker execdocker top 检查容器的进程,然后让脚本停止 Apache:

$ docker exec -it test ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  0.0   4448   692 ?        Ss+  00:42   0:00 /bin/sh /run.sh 123 cmd cmd2
root        19  0.0  0.2  71304  4440 ?        Ss   00:42   0:00 /usr/sbin/apache2 -k start
www-data    20  0.2  0.2 360468  6004 ?        Sl   00:42   0:00 /usr/sbin/apache2 -k start
www-data    21  0.2  0.2 360468  6000 ?        Sl   00:42   0:00 /usr/sbin/apache2 -k start
root        81  0.0  0.1  15572  2140 ?        R+   00:44   0:00 ps aux
$ docker top test
PID                 USER                COMMAND
10035               root                {run.sh} /bin/sh /run.sh 123 cmd cmd2
10054               root                /usr/sbin/apache2 -k start
10055               33                  /usr/sbin/apache2 -k start
10056               33                  /usr/sbin/apache2 -k start
$ /usr/bin/time docker stop test
test
real    0m 0.27s
user    0m 0.03s
sys    0m 0.03s

注意:你可以使用 --entrypoint 覆盖 ENTRYPOINT 设置,但这只能将二进制设置为 exec(不会使用 sh -c)。

注意exec 形式被解析为 JSON 数组,这意味着你必须使用双引号来包裹字符串,而不是使用单引号。

注意:不同于 shell 形式,exec 形式不会调用 shell。这意味着不会发生正常的 shell 处理。例如,RUN [ "echo", "$HOME" ] 不会对 $HOME 执行变量替换。如果你想要使用 shell 处理,要么使用 shell 形式,要么直接运行一个 shell,例如:RUN [ "sh", "-c", "echo $HOME" ]。当使用 exec 形式直接运行一个 shell 时,和使用 shell 形式的效果一样,它执行的是环境变量扩展的 shell,而不是 Docker。

16.2 Shell 形式的 ENTRYPOINT 示例

你可以为 ENTRYPOINT 指定一个纯字符串,它将在 /bin/sh -c 中执行。此形式将使用 shell 处理来替代shell 环境变量,并将忽略任何 CMDdocker run 命令行参数。要确保 docker stop 能正确的给任何长时间运行的 ENTRYPOINT 可执行文件发出信号,你需要记住用 exec 启动它:

FROM ubuntu
ENTRYPOINT exec top -b

运行此镜像时,你将看到单个 PID 1 进程:

$ docker run -it --rm --name test top
Mem: 1704520K used, 352148K free, 0K shrd, 0K buff, 140368121167873K cached
CPU:   5% usr   0% sys   0% nic  94% idle   0% io   0% irq   0% sirq
Load average: 0.08 0.03 0.05 2/98 6
  PID  PPID USER     STAT   VSZ %VSZ %CPU COMMAND
    1     0 root     R     3164   0%   0% top -b

哪个将在使用 docker stop 时干净地退出:

$ /usr/bin/time docker stop test
test
real    0m 0.20s
user    0m 0.02s
sys    0m 0.04s

如果你忘记将 exec 添加到 ENTRYPOINT 的开头:

FROM ubuntu
ENTRYPOINT top -b
CMD --ignored-param1

然后,运行它(为下一步命名):

$ docker run -it --name test top --ignored-param2
Mem: 1704184K used, 352484K free, 0K shrd, 0K buff, 140621524238337K cached
CPU:   9% usr   2% sys   0% nic  88% idle   0% io   0% irq   0% sirq
Load average: 0.01 0.02 0.05 2/101 7
  PID  PPID USER     STAT   VSZ %VSZ %CPU COMMAND
    1     0 root     S     3168   0%   0% /bin/sh -c top -b cmd cmd2
    7     1 root     R     3164   0%   0% top -b

你可以从 top 的输出中看到指定的 ENTRYPOINT 不是 PID 1

如果接着运行 docker stop test,容器将不会干净地退出 - stop 命令将被强制在超时后发送 SIGKILL

$ docker exec -it test ps aux
PID   USER     COMMAND
    1 root     /bin/sh -c top -b cmd cmd2
    7 root     top -b
    8 root     ps aux
$ /usr/bin/time docker stop test
test
real    0m 10.19s
user    0m 0.04s
sys    0m 0.03s

16.3 理解 CMD 和 ENTRYPOINT 如何相互作用

CMDENTRYPOINT 指令都定义了运行容器时执行的命令。有一些规则描述他们的合作。

  1. Dockerfile 应至少指定一个 CMDENTRYPOINT 命令。
  2. 使用容器作为可执行文件时,应定义 ENTRYPOINT
  3. CMD 应该用作一种为 ENTRYPOINT 命令定义默认参数或在容器中执行一个 ad-hoc 命令的途径。
  4. 使用备用参数运行容器时,将覆盖 CMD

下表显示了针对不同 ENTRYPOINT / CMD 组合时执行的命令:

No ENTRYPOINT ENTRYPOINT exec_entry p1_entry ENTRYPOINT [“exec_entry”, “p1_entry”]
No CMD error, not allowed /bin/sh -c exec_entry p1_entry exec_entry p1_entry
CMD [“exec_cmd”, “p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry exec_cmd p1_cmd
CMD [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry p1_cmd p2_cmd
CMD exec_cmd p1_cmd /bin/sh -c exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd

十七、VOLUME

VOLUME ["/data"]

VOLUME 指令创建一个具有指定名称的挂载点,并将其标记为在本机主机或其它容器保存的外部挂载卷。其值可以是一个 JSON 数组,VOLUME ["/var/log/"],或具有多个参数的纯字符串,例如 VOLUME /var/logVOLUME /var/log /var/db。有关 Docker 客户端提供的更多信息/示例和挂载说明,请参阅通过卷共享目录文档。

docker run 命令使用基础镜像中指定位置存在的任何数据初始化新创建的卷。例如,请考虑以下 Dockerfile 片段:

FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

此 Dockerfile 会生成一个镜像,该镜像会导致 docker run/myvol 上创建新的挂载点,并将 greeting 文件复制到新创建的卷中。

17.1 关于指定卷的注意事项

关于 Dockerfile 中的卷,请记住以下事项。

  • 基于 Windows 的容器上的卷:使用基于 Windows 的容器时,容器中卷的目标必须是以下之一:

    • 不存在的目录或空目录
    • C: 以外的驱动器
  • 从 Dockerfile 中更改卷:如果任何构建步骤在声明卷后更改卷内的数据,那么这些更改将被丢弃。

  • JSON 格式:列表被解析为 JSON 数组。你必须用双引号(")而不是单引号(')括起来。
  • 主机目录在容器运行时声明:主机目录(挂载点)本质上是依赖于主机的。这是为了保持镜像的可移植性,因为不能保证给定的主机目录在所有主机上都可用。因此,你无法在 Dockerfile 中挂载主机目录。VOLUME 指令不支持指定 host-dir 参数。你必须在创建或运行容器时指定挂载点。

十八、USER

USER <user>[:<group>] or
USER <UID>[:<GID>]

USER 指令设置用户名(或UID)以及可选的用户组(或GID),以供在运行镜像时使用,以及在 Dockerfile 中声明 USER 指令后的运行任何 RUNCMDENTRYPOINT 指令时使用。

警告:当用户没有主用户组时,将使用 root 组运行镜像(或下一条指令)。

在 Windows 上,如果用户不是内置帐户,则必须先创建用户。可以在 Dockerfile 中调用的 net user 命令来完成。

FROM microsoft/windowsservercore
# 在容器中创建 Windows 用户
RUN net user /add patrick
# 为后续命令指定用户
USER patrick

十九、WORKDIR

WORKDIR /path/to/workdir

WORKDIR 指令为 Dockerfile 中的任何 RUNCMDENTRYPOINTCOPYADD 指令设置工作目录。如果 WORKDIR 指定的目录不存在,即使它未被任何后续 Dockerfile 指令使用,也将创建该目录。

WORKDIR 指令可以在 Dockerfile 中多次使用。如果提供了相对路径,则它将相对于先前 WORKDIR 指令的路径。例如:

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

Dockerfile 中最终 pwd 命令的输出为 /a/b/c

WORKDIR 指令可以解析先前使用 ENV 设置的环境变量。你只能使用 Dockerfile 中显式设置的环境变量。例如:

ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

Dockerfile 中最后一个 pwd 命令的输出将是 /path/$DIRNAME

二十、ARG

ARG <name>[=<default value>]

ARG 指令使用 --build-arg <varname>=<value> 标记定义一个变量,用户可以使用 docker build 命令在构建时将该变量传递给构建器。如果用户指定了未在 Dockerfile 中定义的构建参数,则构建器会输出警告。

(译注:由上可知,ARGENV 的一个显著区别在于:ARG 设置的变量,可以在 docker build 时传值进来。另外,两者在有效范围、持久化等方面的区别也要注意)

[Warning] One or more build-args [foo] were not consumed.

Dockerfile 可以包括一个或多个 ARG 指令。例如,以下是一个有效的 Dockerfile:

FROM busybox
ARG user1
ARG buildno
...

警告:建议不要使用构建变量来传递密码,例如 github 密钥,用户凭据等。使用 docker history 命令,任何镜像用户都可以看到构建时变量值。

20.1 默认值

ARG 指令可以设置一个默认值:

FROM busybox
ARG user1=someuser
ARG buildno=1
...

如果 ARG 指令具有默认值,并且在构建时没有传递值,则构建器将使用默认值。

20.2 范围

ARG 变量从 Dockerfile 中定义的行开始生效,而不是在命令行或其它地方使用参数时生效。例如,考虑这个 Dockerfile:

1 FROM busybox
2 USER ${user:-some_user}
3 ARG user
4 USER $user
...

一个用户通过以下命令构建:

$ docker build --build-arg user=what_user .

第2行的 USER 赋值为 some_user,因为在后续第3行才定义了 user 变量。第4行的 USER 在赋值为 what_user,因为 user 已被定义,并在通过命令行传递了 what_user 值。在通过 ARG 指令定义之前,对变量的任何使用结果都是空字符串。

ARG 指令在构建阶段结束时将超出范围。要在多个阶段中使用 ARG 变量,每个阶段都必须包含 ARG 指令。

FROM busybox
ARG SETTINGS
RUN ./run/setup $SETTINGS

FROM busybox
ARG SETTINGS
RUN ./run/other $SETTINGS

20.3 使用 ARG 变量

你可以使用 ARGENV 指令指定 RUN 指令可用的变量。使用 ENV 指令定义的环境变量始终覆盖同名的 ARG 指令。考虑这个带有 ENVARG 指令的 Dockerfile。

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER v1.0.0
RUN echo $CONT_IMG_VER

然后,假设使用下面的命令构建此镜像:

$ docker build --build-arg CONT_IMG_VER=v2.0.1 .

在这种情况下,RUN 指令使用 v1.0.0 而不是用户传递的 ARG 设置:v2.0.1。这种表现类似于 shell 脚本:本地作用域的变量会覆盖作为参数传递或从环境继承的变量。

使用上面的示例,但用不同的 ENV 表达式,你可以在 ARGENV 指令之间创建更有用的交互:

1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER ${CONT_IMG_VER:-v1.0.0}
4 RUN echo $CONT_IMG_VER

ARG 指令不同,ENV 值始终保留在构建的镜像中。考虑没有 --build-arg 标记的 docker 构建:

$ docker build .

使用此 Dockerfile 示例,CONT_IMG_VER 仍然保留在镜像中,但其值为 v1.0.0,因为它是 ENV 指令在第 3 行中的默认设置。

此示例中的变量扩展技巧允许你从命令行传递参数,并通过利用 ENV 指令将它们保存在最终镜像中。只有一组有限的 Dockerfile 指令支持变量扩展。

20.4 预定义 ARG

Docker 有一组预定义的 ARG 变量,你可以在 Dockerfile 中使用而无需相应的 ARG 指令。

  • HTTP_PROXY
  • http_proxy
  • HTTPS_PROXY
  • https_proxy
  • FTP_PROXY
  • ftp_proxy
  • NO_PROXY
  • no_proxy

要使用它们,只需在命令行上使用标记传递它们:

--build-arg <varname>=<value>

默认情况下,这些预定义变量将从 docker history 的输出中排除。排除它们可降低在 HTTP_PROXY 变量中意外泄露敏感验证信息的风险。

例如,考虑使用 --build-arg HTTP_PROXY=http://user:pass@proxy.lon.example.com 构建下面的 Dockerfile。

FROM ubuntu
RUN echo "Hello World"

在这种情况下,HTTP_PROXY 变量的值在 docker history 中不可用,并且不会被缓存。如果要更改位置,并且代理服务器已更改为 http://user:pass@proxy.sfo.example.com,则后续构建不会导致缓存未命中。

如果你需要覆盖此行为,则可以通过在 Dockerfile 中添加 ARG 语句来达成,如下所示:

FROM ubuntu
ARG HTTP_PROXY
RUN echo "Hello World"

构建此 Dockerfile 时,HTTP_PROXY 将保留在 docker history 中,并且更改其值会使构建缓存无效。

20.5 对构建缓存的影响

ARG 变量不会像 ENV 变量那样持久存储到构建的镜像中。但是,ARG 变量会以类似的方式影响构建缓存。如果 Dockerfile 定义了一个与先前版本不同的 ARG 变量值,那么在第一次使用时会发生“缓存未命中”,而不是在其定义的地方。特别是,ARG 指令之后的所有 RUN 指令都隐式使用 ARG 变量(作为环境变量),因此可能导致缓存未命中。除非 Dockerfile 中存在匹配的 ARG 语句,否则所有预定义的 ARG 变量都将免于缓存。

例如,考虑这两个Dockerfile:

FROM ubuntu
ARG CONT_IMG_VER
RUN echo $CONT_IMG_VER
FROM ubuntu
ARG CONT_IMG_VER
RUN echo hello

如果在命令行上指定 --build-arg CONT_IMG_VER=<value>,则在这两种情况下,第 2 行不会导致缓存未命中; 第 3 行会导致缓存未命中。ARG CONT_IMG_VER 导致 RUN 行被识别为与运行 CONT_IMG_VER=<value> echo hello 相同,因此如果 <value> 发生更改,我们将获得缓存未命中。

(译注:上面这一段可能读起来令人费解,按我自己的理解说一下 - 这两个例子的意义在于说明,不管第三行的 RUN 指令是否引用了 ARG 设置的变量,由于执行 RUN 指令时,ARG 设置的变量已经成为环境变量,所以当 ARG 设置的变量发生变化时(一个从未使用过的值),导致 RUN 运行时的环境变量发生变化,从而导致 RUN 指令缓存未命中。)

考虑同一命令行下的另一个示例:

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER $CONT_IMG_VER
RUN echo $CONT_IMG_VER

在此示例中,缓存未命中发生在第 3 行。由于 ENV 中的变量值引用 ARG 变量并且该变量通过命令行发生更改,因此发生缓存未命中。在此示例中,ENV 命令使镜像包含该值。

如果 ENV 指令覆盖了同名的 ARG 指令,就像这个 Dockerfile:

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER hello
RUN echo $CONT_IMG_VER

第 3 行不会导致缓存未命中,因为 CONT_IMG_VER 的值是常量(hello)。因此,RUN(第 4 行)上使用的环境变量和值在构建时不会发生变化。

二十一、ONBUILD

ONBUILD [INSTRUCTION]

当镜像用作另一个构建的基础时,ONBUILD 指令向镜像添加将在稍后执行的触发器指令。触发器将在下游构建的上下文中执行,就好像它已经在下游 Dockerfile 中的 FROM 指令之后立即插入一样。

任何构建指令都可以注册为触发器。

如果要构建将作为构建其它镜像基础的镜像(例如,应用可以根据用户指定配置自定义的构建环境或守护程序),这将非常有用。

例如,如果你的镜像是可重用的 Python 应用程序构建器,则需要将应用程序源代码添加到特定目录中,并且可能需要在此之后调用构建脚本。你现在不能只调用 ADDRUN,因为你还无法访问应用程序源代码,并且每个应用程序构建都会有所不同。你可以简单地为应用程序开发人员提供一个样板 Dockerfile 来复制粘贴到他们的应用程序中,但这样做效率低,容易出错且难以更新,因为它与特定应用程序的代码混合在一起。

解决方案是使用 ONBUILD 来注册预先指令,以便在下一个构建阶段运行。

以下是它的工作原理:

  1. 当遇到 ONBUILD 指令时,构建器会向正在构建的镜像的元数据添加触发器。该指令不会影响当前构建。
  2. 在构建结束时,所有触发器的列表都存储在镜像清单中的 OnBuild 键下。可以使用 docker inspect 命令检查它们。
  3. 稍后,可以使用 FROM 指令将镜像用作新构建的基础。作为处理 FROM 指令的一部分,下游构建器查找 ONBUILD 触发器,并按照它们注册的顺序执行它们。如果任何触发器失败,则中止 FROM 指令,这反过来导致构建失败。如果所有触发器都成功,则 FROM 指令完成,并且构建继续照常进行。
  4. 执行后,触发器将从最终镜像中清除。换句话说,它们不会被 “grand-children” 构建继承。

例如,你可以添加以下内容:

[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

警告:不允许使用 ONBUILD ONBUILD 这样链接 ONBUILD 指令。

警告:ONBUILD 指令可能不会触发 FROMMAINTAINER 指令。

二十二、STOPSIGNAL

STOPSIGNAL signal

STOPSIGNAL 指令设置发送到容器的系统信号,以退出容器。此信号可以是与内核的系统调用表中的位置匹配的有效无符号数字,例如 9,或 SIGNAME 格式的信号名,例如 SIGKILL。

二十三、HEALTHCHECK

HEALTHCHECK 指令有两种形式:

  • HEALTHCHECK [OPTIONS] CMD command (通过在容器内运行命令来检查容器运行状况)
  • HEALTHCHECK NONE (禁用从基础镜像继承的任何运行状况检查)

HEALTHCHECK 指令告诉 Docker 如何测试容器以检查它是否仍在工作。即使服务器进程仍在运行,也可以检测到陷入无限循环且无法处理新连接的 Web 服务器等情况。

当容器指定了运行状态检查时,除了正常状态外,它还具有运行状态。此状态最初为 starting。每当健康检查通过时,就会变成 healthy(无论以前处于什么状态)。经过一定数量的连续失败后,变成 unhealthy

可以在 CMD 之前出现的选项是:

  • --interval=DURATION (默认: 30s)
  • --timeout=DURATION (默认: 30s)
  • --start-period=DURATION (默认: 0s)
  • --retries=N (默认: 3)

首次运行状态检查将在容器启动后的间隔(interval)秒数后运行,然后在每次检查完成后再在间隔(interval)秒数后运行。

如果单次运行的检查花费的时间超过超时(timeout)秒数,那么检查将被视为失败。

它需要重试(retries)连续的状态检查失败才能将容器视为 unhealthy

start period 为需要时间引导的容器提供初始化时间。在此期间探测失败将不计入最大重试次数。但是,如果在启动期间运行状况检查成功,则会将容器视为已启动,并且所有连续失败将计入最大重试次数。

Dockerfile 中只能有一个 HEALTHCHECK 指令。如果列出多个,那么只有最后一个 HEALTHCHECK 才会生效。

CMD 关键字之后的命令可以是 shell 命令(例如 HEALTHCHECK CMD /bin/check-running)或 exec 数组(与其它 Dockerfile 命令一样;有关详细信息,请参阅如 ENTRYPOINT)。

命令的退出状态表明容器的运行状态。可能的值是:

  • 0: success - 容器状态正常,可使用
  • 1: unhealthy - 容器运行不正常
  • 2: reserved - 不能使用该退出码

例如,要每五分钟检查一次网络服务器是否能够在三秒钟内为网站的主页面提供服务:

HEALTHCHECK --interval=5m --timeout=3s \
  CMD curl -f http://localhost/ || exit 1

为了帮助调试失败的探测,命令在 stdout 或 stderr 上写入的任何输出文本(UTF-8 编码)都将存储在运行状态中,并可以使用 docker inspect 进行查询。此类输出应保持较短(目前仅存储前 4096 个字节)。

当容器的运行状态改变时,将生成具有新状态的 health_status 事件。

Docker 1.12 添加 HEALTHCHECK 功能。

二十四、SHELL

SHELL ["executable", "parameters"]

SHELL 指令允许覆盖使用 shell 形式命令的默认 shell。Linux 上的默认 shell 是 ["/bin/sh", "-c"],在 Windows 上是 ["cmd", "/S", "/C"]SHELL 指令必须以 JSON 格式写入 Dockerfile。

SHELL 指令在 Windows 上特别有用,其中有两个常用且完全不同的原生 shell:cmdpowershell,以及Linux 上包含 sh 的备用 shell(译注:如 zsh 等)。

SHELL 指令可以多次出现。每个 SHELL 指令都会覆盖所有先前的 SHELL 指令,并影响所有后续指令。例如:

FROM microsoft/windowsservercore

# 以 cmd /S /C echo default 执行
RUN echo default

# 以 cmd /S /C powershell -command Write-Host default 执行
RUN powershell -command Write-Host default

# 以 powershell -command Write-Host hello 执行
SHELL ["powershell", "-command"]
RUN Write-Host hello

# 以 cmd /S /C echo hello 执行
SHELL ["cmd", "/S", "/C"]
RUN echo hello

当在 Dockerfile 中使用 RUNCMDENTRYPOINTshell 形式时,SHELL 指令会起作用。

以下示例是在 Windows 上看到的常见模式,可以使用 SHELL 指令简化:

...
RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
...

Docker 调用的命令将是:

cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"

由于两个原因,这是低效的。首先,调用一个不必要的 cmd.exe 命令处理器(也就是 shell)。其次,shell 形式的每个 RUN 指令都需要额外的 powershell -command 前缀命令。

为了提高效率,可以采用两种机制中的一种。一种是使用 RUN 命令的 JSON 形式,例如:

...
RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""]
...

虽然 JSON 形式是明确的,并且不使用不必要的 cmd.exe,但它需要通过双引号和转义来准确表达。替代机制是使用 SHELL 指令和 shell 形式,为 Windows 用户提供更自然的语法,特别是与 escape parser 指令结合使用时:

# escape=`

FROM microsoft/nanoserver
SHELL ["powershell","-command"]
RUN New-Item -ItemType Directory C:\Example
ADD Execute-MyCmdlet.ps1 c:\example\
RUN c:\example\Execute-MyCmdlet -sample 'hello world'

结果为:

PS E:\docker\build\shell> docker build -t shell .
Sending build context to Docker daemon 4.096 kB
Step 1/5 : FROM microsoft/nanoserver
 ---> 22738ff49c6d
Step 2/5 : SHELL powershell -command
 ---> Running in 6fcdb6855ae2
 ---> 6331462d4300
Removing intermediate container 6fcdb6855ae2
Step 3/5 : RUN New-Item -ItemType Directory C:\Example
 ---> Running in d0eef8386e97


    Directory: C:\


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       10/28/2016  11:26 AM                Example


 ---> 3f2fbf1395d9
Removing intermediate container d0eef8386e97
Step 4/5 : ADD Execute-MyCmdlet.ps1 c:\example\
 ---> a955b2621c31
Removing intermediate container b825593d39fc
Step 5/5 : RUN c:\example\Execute-MyCmdlet 'hello world'
 ---> Running in be6d8e63fe75
hello world
 ---> 8e559e9bf424
Removing intermediate container be6d8e63fe75
Successfully built 8e559e9bf424
PS E:\docker\build\shell>

SHELL 指令也可用于修改 shell 的运行方式。例如,在 Windows 上使用 SHELL cmd /S /C /V:ON|OFF,可以修改延迟的环境变量扩展语义。

如果要使用备用 shell,例如 zshcshtcsh 等,也可以在 Linux 上使用 SHELL 指令。

Docker 1.12 添加 SHELL 功能。

二十五、Dockerfile 示例

下面你可以看到 Dockerfile 语法的一些示例。如果你对更现实的东西感兴趣,请查看Docker化示例列表。

# Nginx
#
# VERSION               0.0.1

FROM      ubuntu
LABEL Description="This image is used to start the foobar executable" Vendor="ACME Products" Version="1.0"
RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server
# Firefox over VNC
#
# VERSION               0.3

FROM ubuntu

# Install vnc, xvfb in order to create a 'fake' display and firefox
RUN apt-get update && apt-get install -y x11vnc xvfb firefox
RUN mkdir ~/.vnc
# Setup a password
RUN x11vnc -storepasswd 1234 ~/.vnc/passwd
# Autostart firefox (might not be the best way, but it does the trick)
RUN bash -c 'echo "firefox" >> /.bashrc'

EXPOSE 5900
CMD    ["x11vnc", "-forever", "-usepw", "-create"]
# Multiple images example
#
# VERSION               0.1

FROM ubuntu
RUN echo foo > bar
# Will output something like ===> 907ad6c2736f

FROM ubuntu
RUN echo moo > oink
# Will output something like ===> 695d7793cbe4

# You'll now have two images, 907ad6c2736f with /bar, and 695d7793cbe4 with
# /oink.
(完)