Dockerfile 参考文档中文版

一、概述

原文:https://docs.docker.com/engine/reference/builder/#dockerignore-file
Docker 可以通过从 Dockerfile 读取指令的方式自动创建镜像。Dockerfile 的本质是纯文本,内容是一些用户用于创建Docker 镜像的命令行指令,类似于Windows批处理文件—— .bat 文件。在命令窗口中使用 docker build 命令执行 Dockerfile 文件,当文件里的一系列指令成功执行时就会自动创建镜像。

本文档将列举能够在 Dockerfile 中使用的命令。


二、用法

docker build指令会根据 Dockerfile 中的指令序列和编译上下文去创建 Docker 镜像。所谓编译上下文,是指位于特殊路径 PATHURL 中的文件集。PATH 路径是本地系统上的文件目录,而 URL 则是指位于 Github 等托管平台上的仓库。

编译上下文是递归执行的,所以,PATH 里涉及的文件目录的子目录以及 URL 指定的 git 仓库中的子模块都会被涵盖在编译上下文中。下面的例子演示用当前目录作为编译上下文:

$ docker build .
Sending build context to Docker daemin 6.51 MB
...

整个编译过程是由Docker daemon(Docker守护进程、Docker 服务进程) 进行的,而不是通过 Docker 的命令行接口。因此,编译的第一个步骤就是将整个编译上下文(递归的)告知 Docker 守护进程。大多数情况,建议在一个空目录下进行Docker 镜像编译操作,此时只需将Dockerfile,和构建镜像所需的依赖文件,放入到这个空目录中。

说到这里,必须警告的是:请不要使用根目录或者 PATH 目录作为编译上下文,因为这可能出现将你物理设备上的所有内容传递给 Docker 守护进程导致严重后果

需要用作编译上下文的文件,应当通过在Dockerfile 指令指定的形式去引用,例如 COPY 指令。为了提高编译效率,请在镜像构建目录也就是编译上下文中,增加一个 .dockerignore 文件去排除指定的文件和目录——而如何创建 .dockerignore 文件会在后文提及。

通常,Dockerfile 位于编译上下文的*目录下,但是,我们可以在使用 docker build 指令携带上 -f 参数指定 Dockerfile 在文件系统中的任意位置,示例如下:

docker build -f D:/user/code/java/web/spring-web/dockerDeploy .

当然,我们也可以用 -t 参数指定编译成功后镜像的存储位置:

docker build -t D:/user/product/java/web/spring-web/dockerImage .

如果需要将镜像保存在多个位置,则可参考用如下命令:

docker build -t D:/user/apps/spring-web-docker:1.0.2 -t D:/pyc/myApp:latest

在Docker 守护进程真正执行 Dockerfile 中的指令之前,会先进行 Dockerfile 合法性的初步验证,当 Dockerfile 中存在不合法的标识符时,会返回编译错误,示例如下:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/d247800d25cce9928e7e45ee70df0e95.png#clientId=u92a2b535-85b5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=222&id=ud8da180f&margin=[object Object]&name=image.png&originHeight=444&originWidth=1350&originalType=binary&ratio=1&rotation=0&showTitle=false&size=75136&status=done&style=shadow&taskId=ub7618935-397c-4493-a1b5-f6e8af02032&title=&width=675)
Docker 守护进程是按行执行 Dockerfile 中的指令的,并且会根据情况将每一行的执行结果保存到新镜像中,直到执行完所有指令输出新镜像的 ID,然后会自动清理上下文环境。需要提醒的是,每一条指令都是独立执行的,因此 RUN cd /tmp 不会对下一条指令造成任何影响。任何时候,只要条件运行,Docker 都会使用一个编译缓存来显著加速编译速度;如果用到了编译缓存,控制台会打印出 CACHED 标志(更多内容请参考:https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/),如下:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/ad06ec35f6a749a13e772309a94ccea9.png#clientId=u92a2b535-85b5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=295&id=u28e8e065&margin=[object Object]&name=image.png&originHeight=590&originWidth=1324&originalType=binary&ratio=1&rotation=0&showTitle=false&size=300485&status=done&style=shadow&taskId=u798c8bf2-c663-4697-87ed-af59afe626a&title=&width=662)
默认情况下,编译缓存是基于上一次的编译结果来建立的,而通过 --cache-from 选项则允许指定使用通过镜像注册表的编译缓存,详细内容参考:https://docs.docker.com/engine/reference/commandline/build/#specifying-external-cache-sources


编译成功后,可以根据需要执行 docker scan 指令扫描镜像,和推送镜像到 Docker Hub(类似 github) 上。

三、格式

下面是在Dockerfile 中书写指令的基本格式:

# command
INSTRUCTUION arguments

虽然指令是不分大小写的,但是,为了能够和参数区分开来,习惯上都是将指令写成大写形式。Docker 是按顺序执行 Dockerfile 中的指令序列的,一个 Dockerfile 必须以 FROM 指令开始,但允许在 解析器指令注释全局范围的ARG 指令之后,FROM 指令指定正在构建的镜像的父镜像;FROM 指令之前只能有一个或多个 ARG 指令——声明在 FROM 指令中使用的参数。

Dockerfile 中注释和解析器指令都是以 # 开头,区别它们的方式看 # 之后的内容是否有 = . 其他位置中的 # 会被解析为参数,如:

# comments
RUN echo 'we are running some # of cool things'

注释行不参与 Dockerfile 的执行,像下面例子中 # 后的内容都会被忽略

RUN echo hello \
# comment
world

Docker 在解析上面的 Dockerfile 时,其实看到的是如下的:

RUN echo hello \
world

需要注意的是,注释行不支持换行。

指令和 # 前面的前导空格都会被默认忽略掉,但是还是建议在 Dockerfile 中每一行都是顶格开始书写。

四、解析器指令

解析器指令在 Dockerfile 中不是必须的,但当写了解析器指令,那么对之后 Dockerfile 内容的处理方式会形成影响。解析器指令不会往正在构建的镜像中添加任何层,也不会显示成构建步骤。解析器指令以 #directive=value 的形式进行书写,每个解析器指令只能使用一次。解析器指令也是大小写不敏感的,但是,习惯上会以小写的形式进行书写并与其他指令行间隔一个空行,如下:

# directive1=value1
# directive2=value2

FROM ImageName

和注释行一样,解析器指令不支持换行,因此,下面的书写是不合法的:

# direc \
tive=value

下面也是一些不合法的写法:

  • 1)重复
# directive=value1
# directive=value2

FROM ImageName
  • 2)位于编译指令之后
FROM ImageName
# directive=value
  • 3)位于注释之后被当作注释
# About my Dockerfile
# directive=value
FROM ImageName
  • 4)由于未被识别,未知指令被视为注释。 此外,由于出现在不是解析器指令的注释之后,已知指令被视为注释
# unknowndirective=value
# knowndirective=value

1、syntax

syntax 解析器指令基本写法如下:

# syntax=[remote image reference]

几个例子:

# syntax=docker/dockerfile:1
# syntax=docker.io/docker/dockerfile:1
# syntax=example.com/user/repo:tag@sha256:abcdef...

这种特性只有在使用 BuildKit 终端时可用,在传统终端中会被忽略。
syntax 解析器指令定义书写 Dockerfile 的符号的位置,BuildKit 终端允许无缝使用作为 Docker 镜像分发并在沙箱环境中执行的外部命令。
自定义 Dockerfile 实现支持:

  • 在不更新 Docker 守护进程的情况下自动修正错误;
  • 确保所有的用户都能够使用相同的步骤来执行 Dockerfile
  • 在不用更新 Docker 守护进程的情况下使用最新特性
  • 在将新功能或第三方功能集成到 Docker 守护进程之前试用它们
  • 使用替代的构建定义,或创建自己的

2、escape

escape 指令的基本写法如下两种形式:

# escape=\ (blackslash)
# escape=` (blackslash)

escape 指令设置 Dockerfile 中用于进行转义的符号。如果没有特别指出,那么 Dockerfile 中起转义作用的符号就是反斜杠:\。转义符既可以对字符进行转义,也可以对文本格式空字符进行转义,例如转义换行符——这使得 Dockerfile 指令可以跨越多行。值得注意的是,无论 Dockerfile 中是否包含转义解析器指令,除非转义符在行尾,否则不会在 RUN 指令中执行转义

将反引号作为转义符,适用于 Windows 环境,因为 Windows 系统使用反斜杠作为目录路径分隔符,因此使用反引号作为转义分将非常适合 PowerShell。下面给出一个使用反斜杠作为转义符,在 Windows 环境下会失败的例子:

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

这个例子中,第二行末尾的反斜杠因为别解释为对换行符的转义,而不是倒数第二个反斜杠的转义目标,第三行末尾的反斜杠符号也是类似,而这导致第三行被当作第二行指令的延续,而不是单独的一行指令;当在 Powershell 中执行该 Dockerfile 将会出现下面的结果:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/a1602999521e551662fca78625015f0f.png#clientId=u74d88ae7-df6b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=188&id=u29f36e47&margin=[object Object]&name=image.png&originHeight=376&originWidth=1348&originalType=binary&ratio=1&rotation=0&showTitle=false&size=59090&status=done&style=none&taskId=u15f6c966-091b-4fb6-8759-6eec49cf47c&title=&width=674)
解决这个问题的一个方法是:使用斜杆而非反斜杠来描述 COPY 指令和 dir 的目标,但这并不是很好,一方面斜杆不是 Windows 系统官方提倡的路径分隔符,另一方面并不是所有的 Windows 命令支持使用斜杆作为路径分隔符。

因此,最好的解决方法就是使用 escape 解析器指令声明反引号作为转义符,如下示例:

# escape=`

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

上面的 Dockerfile 执行结果:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/21f16a84a899d4b292e380815e7cb6e8.png#clientId=u74d88ae7-df6b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=506&id=u96b1d0a0&margin=[object Object]&name=image.png&originHeight=1012&originWidth=1324&originalType=binary&ratio=1&rotation=0&showTitle=false&size=414767&status=done&style=none&taskId=ua5fe785f-f4ee-405f-a33e-ba2b590e52a&title=&width=662)

五、更换环境

在 Dockerfile 中除了转义符可以将类似变量的语法按字面意思包含到语句中,ENV 语句也有此功能——ENV 指令用于声明环境变量并在相关指令中使用这些环境变量。环境变量声明格式如下:

ENV variable_name=value

环境变量的基本使用格式如下:

$variable_name
${variable_name}

这两种写法的效果是一样的,并且花括号语法常用于解决没有空格的变量名称的问题,例如${foo}_bar;此外,花括号语法还支持指定的一些标准的 bash 修饰符,如下:

  • ${variable:-word}:表示如果设置了变量,那么变量值就是所设置的;如果没有设置值,则变量值为 word。
  • ${variable:+word}:表示如果设置了 variable,则结果为 word;否则为空字符串。

上面两种规则中,“word” 可以是任何字符串,也可以是环境变量。如果只是单纯的想用美元符号,而不是进行环境变量使用,则可以在美元符号前使用转义符,例如:\$foo or \${foo} 将解析成:$foo or ${foo}。一个简单示例:

FROM busybox
ENV FOO=/bar
WORKDIR ${FOO}
ADD .$FOO
COPY \$FOO /qunx

上述指令会被解析成如下:

FROM busybox
ENV FOO=/bar
WORKDIR /bar
ADD ./bar
COPY $FOO /qunx

在 Dockerfile 中下述指令中都可以使用环境变量:

  • ADD
  • COPY
  • ENV
  • EXPOSE
  • FROM
  • LABEL
  • STOPSIGNAL
  • USER
  • VOLUME
  • WORKDIR
  • ONBUILD(当与上述指令之一结合使用时)

指令中凡是使用到了同名环境变量的,都将被替换成相同的值,看下面的例子:

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

def 的值为 hello,而不是 bye;然而,ghi 的值将是 bye,因为在第二条指令中环境变量 abc 的值已经被更改了。

六、.dockerignore 文件

docker CLI 在将上下文发送给 Docker 守护进程之前,会在上下文的根目录中寻找名为 .dockerignore 的文件;若该文件存在,CLI 会修改上下文以排除 .dockerignore 文件中指定的文件和目录——这有助于避免向 Docker 守护进程发送不必要的大型或敏感的文件或目录,并进一步避免了 ADD 指令或 COPY 将它们添加到新镜像中。

CLI 会将 .dockerignore 文件解析成按行分割的模式匹配列表,类似于 Unix shell 中的文件 glob。出于匹配的需要,上下文的*目录会被视为根目录和工作目录。例如,模式串:/foo/barfoo/bar 都排除了 PATH 的 foo 子目录或位于 URL 的 git 仓库根目录下名为 bar 的文件或目录。此外不排除其他东西。下面给出一个示例

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

会被解析成:

模式串 意义
#comment 注释,会被忽略
/temp 排除根目录的任何一级子目录中名称以 temp 开头的文件和目录。例如,纯文件 /somedir/temporary.txt 被排除在外,而目录 /somedir/temp 也是如此。
//temp* 从根目录下两级的任何子目录中排除以 temp 开头的文件和目录。
temp? 排除根目录中名称为 temp 开头并后续为任意单个字符的文件和目录,例如:排除 /tempa 和 /tempb

匹配操作是使用 GO 语言的 filepath.Match 规则完成的;预处理步骤为使用 Go 语言的 filepath.Clean 删除前导空格和尾随空格和 . 以及 … 。预处理后的空白行将被忽略。
除了 GO 语言的 filepath.Match 规则外,Docker 还支持通配符 ** 匹配任意数量的目录(包括零),如 **/*.go 将排除所有在目录中找到的文件扩展名为 .go 的文件,包括构建上下文的根目录。
以 !开头的行表示不排除(其实就是取非的意思,!false=true),示例如下:

*.md
!README.md

除了 README.md 文件外所有的Markdown 文件都会被排除。再来看个例子:

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

上面的规则中除了 README-secret.md 外不包含任何 Markdown 文件,因为第二行中 README 后面跟上了一个 * 号,因此下一行将会按着这个规则保留具体以 README 开头的 Markdown 文件。再来看一个例子:

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

上述规则将保留所有的 Markdown 文件,虽然第二行写了排除 README-secret.md 文档,但是,第三行的豁免规则导致第二行不起作用。.dockerignore 规则还支持将 Dockerfile 和 .dockerignore 文件本身进行排除,虽然因为编译镜像的需要还是会将这两个文件作为上下文发送给 Docker 守护进程,但是 ADD 和 COPY 指令不会将这两个文件复制到新镜像中。

七、Dockerfile 指令

1、FROM 指令

FROM 指令有如下三种基本书写格式:

# way one
FROM [--platfrom=<platfrom>] <image> [AS <name>]
# way tow
FROM [--platfrom=<platfrom>] <image>[:<tage>] [AS <name>]
# way three
FROM [--platfrom=<platfrom>] <image>[@<digest>][AS <name>]

FROM 指令是开始执行 Dockerfile 的第一个指令,它将进行创建镜像的初始化工作,并为后续指令设置依赖镜像,这也意味着合法且有效的 Dockerfile 文件必须以 FROM 指令开头;FROM 指令指定的依赖镜像可以是任何有效的镜像——建议从公共存储库拉去依赖镜像。FROM 指令有如下几个特点:

  • 一个 Dockerfile 文件中允许有多条 FROM 指令,以便将新镜像所需要的依赖镜像全部拉取到本地,前一条 FROM 指令可以是当前 FROM 指令的依赖,同理当前 FROM 指令也可以是下一条 FROM 指令的依赖,只需要几下每个新 FROM 指令之前提交的最后一个镜像ID的输出;每个 FROM 指令都会清楚先前指令创建的任何状态。
  • 为方便后续使用依赖镜像,FROM 指令允许用 AS name 语法为所拉取的镜像指定名称,而该名称将用于后续的 FROM 指令和 COPY --from= 指令。
  • tag 和 digest 也是可选的;如果不指定 tag,则 FROM 指令将默认拉取最新版本(latest)的依赖镜像;而如果指定 tag,一旦 Docker 守护进程没办法在远程 Docker Hub 中找到对应 tag 的镜像,那么将返回错误。
  • – platform 参数也是可选,主要用于在 FROM 引用多平台镜像的情况下指定镜像的平台环境,例如:linux/amd64、linux/arm64 或 Windows/amd64。默认情况下,使用构建请求的目标平台。全剧构建参数可作为 --platform 参数的值,例如 automatic platform ARGs 允许强制一个阶段(stage)到本机构建平台(–platform=$BUILDPLATFORM),并用它交叉编译到阶段内的目标平台。

ARG 指令与 FROM 指令的协同工作

FROM 指令允许使用第一个 FROM 指令之前的 ARG 指令声明的全局变量,例如:

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

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

ARG 指令声明的全局变量只是当前 Dockerfile 要用的,因此,ARG 指令只能用于第一条 FROM 指令之前,之后的任何地方都不允许再次使用 ARG 指令;但有一种情况是允许的,即修改已经声明过的全局变量的值,如下示例:

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

2、RUN 指令

RUN 指令有两种书写格式:

  • RUN :shell 格式,当命令是在 shell 中运行时推荐,在 Linux 系统上默认为 /bin/sh -c,在 Windows 系统上 cmd /S /C
  • RUN [“executable”, “param1”, “param2”]:exec 格式

RUN 指令将在当前镜像里的新层中执行任何命令并提交结果,所产生的结果将用于 Dockerfile 的下一步。分层执行 RUN 指令并产出中间结果,是完全遵循 Docker 的核心思想的,提交中间结果成本低,并且可以从镜像历史的任何时间点创建容器,就行源代码控制一样。
exec 格式的 RUN 指令,可以避免 shell 字符串修改,并可以使用不包含指定的 shell 的可执行文件的依赖镜像来运行命令。
对于 shell 格式的 RUN 指令,可以使用 SHELL 命令更改默认的 shell,同时,运行使用反斜杠进行换行,示例如下:

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

等价于:

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

如果不想用 /bin/sh,想用其他的 shell,则应该用 exec 格式的 RUN 指令:

RUN ["/bin/bash", "-c", "echo hello"]

由于 exec 格式的 RUN 指令命令会被解析成 JSON 数组,因此命令参数必须用双引号括起来,而非单引号。

不同于 shell 格式的 RUN 指令,exec 格式的 RUN 指令不会进行任何常规的 shell 处理,因为 exec 格式的 RUN 指令不调用命令 shell;例如,RUN [“echo”, “$HOME”] 不会对 $HOME 进行变量替换,若想要进行替换,要么使用 shell 格式的 RUN 指令,要么直接执行 shell,例如:RUN [“sh”,"-c",“echo $HOME”] ,这种方式的 exec 格式的 RUN 指令,和用 sehll 格式的 RUN 指令效果一样,但是,不同之处是 shell 进行环境变量扩展,而非 docker。

在 exec 格式的 RUN 指令,由于会解析成 JSON 数组,因此,需要对反斜杠进行转义,在使用反斜杠作为目录路径分隔符的 Windows 系统上尤为重要;下面给出一个因为反斜杠导致 JSON 格式错误,进而导致失败的例子

RUN ["c:\windows\system32\tasklist.exe"]

正确写法应该是:

RUN ["c:\\windows\\system32\\tasklist.exe"]

RUN 指令所产生的缓存,在下一次镜像构建操作中依然有效,例如 RUN apt-get dist-upgrade -y 产生的缓存;如果想 RUN 指令产生的缓存在指令运行结束后失效,可以用 --no-cache 标志:docker build --no-cache,此外也可以通过 ADD 指令和 COPY 指令让缓存失效,更多内容参考Dockerfile Best Practices guide

3、CMD 指令

CMD 指令有如下三种书写格式:

  • CMD [“executable”, “param1”, “param2”]:exec 格式,也是最推荐的形式。
  • CMD [“param1”, “param2”]:作为 ENTRYPOINT 的默认参数。
  • CMD command param1 param2:shell 格式

在一份 Dockerfile 文件中,只允许一个 CMD 指令,当有多条 CMD 指令出现在一份 Dockerfile 文件中,那么实际上起作用的将只有最后一条 CMD 指令。 CMD 指令主要是为了向正在运行的容器提供默认值,这些默认值可以包含可执行文件,也可省略可执行文件——此种情况则必须指定 ENTRYPOINT 指令。如果,在 Dockerfile 书写 CMD 指令是为了给 ENTRYPOINT 指令提供默认参数,那么 CMD 指令和 ENTRYPOINT 指令最好都使用 JSON 格式——exec 格式。无论是 shell 格式还是 exec 格式的 CMD 指令,都可用于设定镜像启动时所要执行的操作。

如果使用 shell 格式的 CMD 指令,那么默认用 /bin/sh -c 去执行 command:

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

如果不想用 shell 去执行 command,那么最好采用 exec 格式的 CMD 命令,将你期望的用于执行 command 的可执行文件的完整路径在 CMD 指令的第一个参数位置指定,进一步,由于这个格式的 CMD 指令是采用数组形式入参,因此,任何额外的参数都必须单独表示成数组中的字符串:

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

而当你希望容器每次都运行相同的可执行文件,那么最好将 CMD 指令和 ENTRYPOINT 指令结合使用,更多信息参阅 ENTRYPOINT 指令。如果执行 docker run 时指定了参数,那么该参数将覆盖 CMD 中指定的默认值。

不要将 RUN 指令和 CMD 指令混淆,RUN 指令的作用是实际运行一个命令并提交结果,而 CMD 在构建时不执行任何操作——只是指定镜像的预期命令。

4、LABEL 指令

LABEL 指令的格式:LABEL <key>=<value> <key>=<value> <key>=<value> ...
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."

一个镜像允许有多个 label,书写时可以在一行中指定多个 label。 Docker 1.10 之前,采用一行指定多个 label,可以减小镜像的体积,但是现在已经不在如此了。但是一行指定多个 label,或多行指定 label,都是被支持的:

LABEL multi.label1="value1" multi.label2="value2" other="value3"
# 等价于
LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"

基础镜像或者说依赖镜像——FROM 指令中指定的镜像——的 label,允许被你所要创建的镜像继承;并且如果已存在同名标签,那么新值会覆盖旧值。查看一个镜像的 label,可以用命令 docker image inspect,并且可以用 --format 选项设置仅显示镜像 label:

docker image inspect --format='' myimage
{
  "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"
}

5、EXPOSE 指令

EXPOSE 指令的格式:EXPOSE <port> [<port>/<protocol>...]
EXPOSE 指令用于指定 Docker 容器运行时所要监听的网络端口,并且允许在指定端口的同时指定协议:TCP 或 UDP;若不显式指定协议,则默认为 TCP 协议。
EXPOSE 指令实际上并不会发布端口,EXPOSE 指令的功能是,供构建进行的人为使用镜像的人,生成一个应用监听端口的参考信息。要在运行容器时实际发布端口,则应该在 docker run 命令中使用 -p 选项发布和映射一个或多个端口,或者使用 -P 选项发布所有暴露的端口并将它们映射到编号比较大的端口上。
指定 UDP 协议:

EXPOSE 80/udp

同时指定udp 和tcp:

EXPOSE 80/udp
EXPOSE 80/tcp

在这个例子中,如果使用 -P 选项和 docker run 一起运行,那么将同时发布 udp 80 端口和 tcp 80 端口。需要注意得是,-P 使用主机上的临时端口——编号较大的端口,因此,TCP 和 UDP 的端口将不同。
EXPOSE 设置的端口,将会被 docker run 的 -p 选项覆盖:

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

如果需要在主机上设置端口重定向,请参考 -P 选项的使用。docker network 命令可以为容器之间的通信创建网络,而无需公开或发布特定的端口,因为链接到网络的容器可以通过任何端口进行通信,详细内容请参考

6、ENV 指令

ENV 指令的书写格式:ENV <key>=<value> ...
ENV 指令采用键值对的格式进行环境变量,key 指定变量名,value 设置对应的值,而 ENV 声明后的环境变量作用范围为所在行之后的整个 Dockerfile 文件,并且允许在其他指令行中进行內联替换。并且环境变量允许嵌套使用,即允许在使用 ENV 声明新的环境变量时,使用前文已经声明的环境变量:

ENV path = /usr/bin
ENV exe = $path/git.exe

同样的道理,如果想单纯使用 “$" 符号,那么就必须使用转义符;使用双引号或斜杆可以令 包含空格。几个简单示例:

ENV MY_NAME="John Doe"
ENV MY_DOG=Rex\ The\ Dog
ENV MY_CAT=fluffy

ENV 指令也是允许在一行内同时设置多个环境变量:

ENV MY_NAME="John Doe" MY_DOG=Rex\ The\ Dog \
    MY_CAT=fluffy

ENV 指令设置的环境变量是伴随着镜像存在的,也即是我们用镜像创建的容器里是可以使用 ENV 指令设置的环境变量的;要查看容器或镜像中的环境变量,可以使用命令 docker inspect;如果想修改 Dockerfile 中 ENV 设置的环境变量,可以用如下格式的 docker run:
docker run --env <key>=<value>
或者,只是希望 ENV 声明的环境变量仅在 Dockerfile 中使用,不想生成到新镜像中,因为环境变量持久化可能导致意外的错误,例如:ENV DEBIAN_FRONTEND=noninteractive 会影响 apt-get 命令,进而导致生成的镜像并不是预期的;若不想这种问题发生,就应该考虑为单个命令设置一个值:

RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ...

或者使用 ARG 指令——因为ARG 指令设置的变量不会保存到镜像中:

ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y ...

此外,ENV 指令声明环境变量时,可以将等号省略,但不建议用,因为这在未来版本中可能不支持。

7、ADD 指令

ADD 指令有如下两种格式:

  • 1)ADD [–chown=:] …
  • 2)ADD [–chown=:] ["",… “”]

显然,第二种格式允许路径中包含空格。

–chown 功能只能用于构建 Linux 容器的 Dockerfil 中,而不支持 Windows 容器;这是由于用户和组的所有权概念,在 Linux 和 Windows 是截然不同的,Docker 不支持在二者之间进行转换,具体来说就是:将 /etc/passwd 和 /etc/group 对应的用户和组名,转为 ID 的操作限制了此功能仅适用于 Linux 系统的容器。

ADD 指令用于文件或目录拷贝,其中文件可以是位于本地的,也可以是 URL 指定的远程文件;ADD 指令会将文件或目录从 src 指定原路径处拷贝到镜像文件系统中 dest 指定的路径所在处。ADD 指令允许指定多个 src 资源,但,如果是文件或目录,则它们的路径会被解释为相对路径(相对于Dockerfile 所在的构建目录);此外,src 字符串里面允许使用通配符,例如:

ADD hom* /mydir/
ADD hom?.txt /mydir/

上面两个例子中,第一个是将 hom 开头的目录或文件进行拷贝,第二个则是对文件名为4个字符且前三个为 hom 的 txt 文件进行复制。再来看两个例子:

ADD test.txt relativeDir/
ADD test.txt /absoluteDir/

在上面两个例子中,第一个采用相对路径,第二个采用绝对路径。当需要添加包含特俗字符,如[ 和 ] 的文件或目录时,必须按照 Golang 规则对这些路径进行转义,以免当作正则表达式进行解析,例如添加名为 arr[0].txt 的文件,应当使用如下形式的 ADD 指令:

ADD arr[[]0].txt /mydir/

默认情况下,所有新文件和新目录的 UID 和 GID 的值都是 0,当然,也可以用 --chown 选项指定用户名、组名或 UID/GID 组合进行指定内容的所有权设置。–chown 选项的书写格式允许用字符串形式的用户名和组名,以及整数格式的 UID和GID;如果在设置的时候,只填写用户名而组名置空,或者只有 UID 而无 GID,那么会将 GID 默认设置成与 UID 一样的整数;如果用户名和组名都提供了,那么在进行从字符串转换为整数形式的 UID 和 GID 时,将会去容器根文件系统 /etc/passwd 和 /etc/group 目录下匹配。下面是几个简单示例:

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 文件,但又在 ADD 指令中使用了 --chown 选项并设置的用户名和组名,那么在执行 ADD 指令时将失败。使用整数的 ID 不需要到上述目录中进行匹配,也不依赖于容器根文件系统内容。
当 src 为远程文件的 URL 时,目标将具有 600 的权限,并且如果正在检索的远程文件具有 HTTP Last-Modifed 标头,则该标头中的时间戳将用于设置目标上的 mtime 文件。但是,和在 ADD 期间处理的其他文件一样,在确定文件是否已更改以及是否应该更新缓存时,将不包括 mtime。

如果使用 STDIN(docker build -< somefile)传递 Dockerfile 进行构建,那么是不存在构建上下文的,此时,ADD 指令中 src 必须是 url;当然如果 STDIN 传递的是压缩文档(docker build - < archive.tar.gz),那么压缩文档目录下的 Dockerfile 和压缩文档的其余内容都将作为构建上下文。

需要注意的是,由于 ADD 指令不支持身份校验,因此,当 URL 文件使用了身份验证保护,那么需要使用 RUN wget、RUN curl 或使用容器内的其他工具进行文件获取。

如果,src 的内容发生变化,则 Dockerfile 中的第一条 ADD 指令的执行,将导致后续指令的缓存无效,包括 RUN 指令的缓存,有关更多内容请参考

ADD 指令的使用要遵循一下规则:

  • 1)ADD 指令的 src 所指定的目录或文件,必须在构建上下文范围内;src 的值不允许设置为 ../somedir/something,因为 docker 构建的第一步是将 Dockerfile 所在目录及其子目录作为构建上下文,传递给 docker 守护进程;因此,Dockerfile 所在的上级目录 docker 守护进程是无法读取的。
  • 2)如果 ADD 指令的 src 参数为 URL,并且 dest 参数不是以斜杆结尾,那么将会复制 URL 指定的文件到 dest 中。
  • 3)如果 ADD 指令的 src 参数为 URL,并且 dest 参数以斜杆结尾,那么会根据 URL 命名新文件保存到 dest/filename 中;例如,添加 http://example.com/foobar/ 将创建文件 /foobar;此时,URL 中必须有一个重要的路径,以便从中推断出适当的文件名(http://example.com 这种只包含域名的 URL 是不允许的)
  • 4)如果ADD 指令的 src 参数为目录,那么将复制目录的全部内容,包括文件系统的元数据。

不会复制目录本身,只会复制其内容

  • 5)如果 src 值为可识别的压缩格式(identity、gzip、bzip2 或 xz)的本地 tar 压缩文档,则将解压成目录。如果是 URL 指定的压缩文档,则不会进行解压操作。当一个目录被复制或解压时,执行的操作与 tar -x 相同。

文件是否被识别为可识别的压缩格式,完全取决于文件的内容,而非文件名;当一个空文件恰好以 .tar.gz 结尾,并不会被识别为压缩文件,也不会生成任何类型的解压缩错误,只会将该文件复制到目标路径。

  • 6)如果 src 参数为其他类型的文件,则它会与其元数据一起单独复制;此时,如果 dest 以斜杆结尾,dest 将会被视为一个目录,而 src 的内容将写入 dest/base(src) 中。
  • 7)如果直接指定了多个 src 资源,或者由于使用了通配符,那么 dest 必须是一个目录,因此 dest 必须以斜杆结尾。
  • 8)dest 不以斜杆结尾,则会被视为一个普通的文件,src 的内容将会写入 dest 中
  • 9)如果 dest 不存在,则会与路径中所有缺失的目录一起创建。

8、COPY 指令

COPY 指令与 ADD 指令类似,其书写格式就是指令名换一下:

COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]

COPY 指令的功能也是将内容从 src 拷贝到 dest,src 参数也同样允许指定多个资源,src 参数值里同样也支持通配符;而 dest 同样支持相对路径和绝对路径两种,注意事项也基本与 ADD 指令一样。与 ADD 指令不一样的地方在于:COPY 指令有个可选选项 --from=<name>,该选项可用于将源位置设置为先前的构建阶段(使用 FROM … AS 创建),该阶段将用于代替用户发送的构建上下文。如果无法找到具体的指定名称的构建阶段,则会尝试使用具有相同名称的镜像。

9、ENTRYPOINT 指令

ENTRYPOINT 指令也有两种书写格式:

  • 1)exec 格式,也即是首选格式:
ENTRYPOINT ["executable", "param1", "param2"]
  • 2)shell 格式:
ENTRYPOINT command param1 param2

ENTRYPOINT 指令可以将容器配置成像可执行文件一样运行,例如用默认设置启动 nginx 并监听端口 80:

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

dokcer run 的命令行参数,会传递给 exec 格式的 ENTRYPOINT 指令,并将这些参数追加到 ENTRYPOINT 指令的所有元素之后,并覆盖使用 CMD 指定的所有元素。这允许将参数传递给入口点,即 docker run -d 会将 -d 参数传递给入口点。同样的,docker run 命令提供了 --entrypoint 选项来对 ENTRYPOINT 指令进行覆盖设置。
shell 格式的 ENTRYPOINT 指令,可以有效地防止使用任何 CMD 或运行命令行参数,但缺点是 ENTRYPOINT 将作为 /bin/sh -c 的子命令启动且不会传递信号,而这将意味着可执行文件将不是容器的 PID 1 —— 并且不会接收 Unix 信号——因此可执行文件将不会从 docker stop 收到 SIGTERM。
**只有 Dockerfile 中的最后一条 ENTRYPOINT 指令会起作用**

9.1、exec 格式 ENTRYPOINT 示例

建议用该格式的 ENTRYPONT 指令设置不需要经常进行值修改操作的默认命令和参数,而用 CMD 指令设置经常变动的其他默认值:

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

此种情况下,运行容器,将会看到 top 是唯一的进程:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/743aec6362a1191a45a4eac94cdbe335.png#clientId=ued4e3162-1a34-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u067967fc&margin=[object Object]&name=image.png&originHeight=454&originWidth=1344&originalType=binary&ratio=1&rotation=0&showTitle=false&size=83564&status=done&style=shadow&taskId=u8c3ae817-9aa2-48c2-9e01-66b93bae933&title=)
想进一步检查结果,可以用 docker exec:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/612b75015b8fc0819f08daef585e54ae.png#clientId=ued4e3162-1a34-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u29fad761&margin=[object Object]&name=image.png&originHeight=276&originWidth=1338&originalType=binary&ratio=1&rotation=0&showTitle=false&size=40775&status=done&style=shadow&taskId=ubf5280d2-760f-45f2-a761-49923911424&title=)
此时,便可以使用 docker stop test 来停止 top 进程。下面展示一个在前台运行 Apache(即 PID=1)的 ENTRYPOINT:

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

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

#!/us/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
# Note: I've written this using sh so it works in the busybox container too

# USE the trap if you need to also do manual cleanup after the service is stopped,
#     or need to start multiple services in the one container
trap "echo TRAPed signal" HUP INT QUIT TERM

# start service in background here
/usr/sbin/apachectl start

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

# stop service and clean up here
echo "stopping apache"
/usr/sbin/apachectl stop

echo "exited $0"

此时,使用 docker run -it --rm -p 80:80 --name test apache 运行镜像,则可以用 docker exec 或 docker stop 检查容器的进程,然后用脚本停止 Apache:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/fac2e0d0d9f606b825f74123e4101f29.png#clientId=ued4e3162-1a34-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ue547e9a6&margin=[object Object]&name=image.png&originHeight=938&originWidth=1354&originalType=binary&ratio=1&rotation=0&showTitle=false&size=371428&status=done&style=none&taskId=ud9976493-d6e3-4f0d-98a4-5f68d641446&title=)

尽管可以用 --entrypoint 覆盖 ENTRYPOINT 设置,但这只能将二进制文件设置为 exec(不会使用 sh -c)

9.2、shell 格式 ENTRYPOINT

此种格式,允许为 ENTRYPOINT 指定一个纯字符串参数——将在 /bin/sh -c 中执行,这意味着将用 shell 进行处理、替换 shell 环境变量,并将忽略任何 CMD 或 docker run 命令行参数。为了确保 docker stop 能够正确地向任何长时间运行的 ENTRYPOINT 可执行文件发出信号,需要记住用 exec 启动:

FROM ubuntu
ENTRYPOINT exec top -b

运行此镜像,将可以看到 PID=1 的进程:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/37505ed3069d0fa796b868a197b8fd51.png#clientId=ued4e3162-1a34-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u75e71237&margin=[object Object]&name=image.png&originHeight=346&originWidth=1352&originalType=binary&ratio=1&rotation=0&showTitle=false&size=144868&status=done&style=shadow&taskId=u10801aa6-9f49-49f2-93fa-e176c8c8b31&title=)
使用 docker stop 干净地退出:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/e5c5a61e6cbf87fc9dc84ed6b3e068be.png#clientId=ued4e3162-1a34-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u18b43e58&margin=[object Object]&name=image.png&originHeight=318&originWidth=1368&originalType=binary&ratio=1&rotation=0&showTitle=false&size=85401&status=done&style=shadow&taskId=ub512be72-e392-4111-989d-61a1a76b541&title=)
如果忘记将 exec 添加到 ENTRYPOINT 中:

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

再运行镜像(并为下一步命名):
![image.png](https://www.icode9.com/i/ll/?i=img_convert/8c27c13732b7d3c17cad42eeb8a92fd5.png#clientId=ued4e3162-1a34-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=uc6c85978&margin=[object Object]&name=image.png&originHeight=388&originWidth=1338&originalType=binary&ratio=1&rotation=0&showTitle=false&size=170566&status=done&style=none&taskId=ua60db60e-52ef-4869-825c-dcda5cdc31a&title=)
可以从 top 的输出内容中看到指定的 ENTRYPOINT 不是 PID=1 的进程。此时,如果用 docker stop test 命令停止容器,容器并不能干净地退出——stop 命令将在超时后强制发送 SIGKILL:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/a5710a9d020b905830f2388b8747c16d.png#clientId=ued4e3162-1a34-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=uc0380a79&margin=[object Object]&name=image.png&originHeight=578&originWidth=1348&originalType=binary&ratio=1&rotation=0&showTitle=false&size=155731&status=done&style=none&taskId=u194c3993-a97b-4333-adc7-280ba0fc319&title=)

9.3、ENTRYPOINT 与 CMD 的交互

ENTRYPOINT 和 CMD 指令都定义了容器运行时执行的命令,下面是 ENTRYPOINT 与 CMD 协作时的一些规则:

  1. Dockerfile 必须明确指定 ENTRYPOINT 或 CMD 指令之一;
  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

如果 CMD 是根据基础镜像定义,则设置 ENTRYPOINT 会将 CMD 重置为空值;这种情况下,必须在当前镜像中定义 CMD 才能具有值。

10、VOLUME 指令

VOLUME 指令只有如下一种书写格式:

VOLUME ["/data"]

VOLUME 指令用于创建一个具有指定名称的挂载点,并将其标记为保存来自本机主机或其他容器的外部挂载卷。VOLUME 指令的值,可以是 JSON 数组、VOLUME ["/var/log/"] 或带有多个参数的纯字符串,例如:VOLUME /var/log 或 VOLUME /var/log /var/db。更多信息请参考
docker run 命令会用基础镜像中指定位置存在的任何数据对新创建的卷进行初始化,例如:

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

上述 Dockerfile 创建的镜像,会在用 docker run 启动镜像时在 /myvol 创建新的挂载点,并将 greeting 文件拷贝到新建的卷中。

注意事项

请记住下面与 Dockerfile 中的卷相关的事项:

  • 基于 Windows 的容器上的卷:使用基于 Windows 的容器时,容器内卷的目标必须是以下之一:
    • 一个不存在或内容为空的目录;
    • C 盘意外的驱动器。
  • 在 Dockerfile 中更改卷:如果在声明卷之后,有任何构建步骤对卷中的数据进行更改,那么这些更改都会被丢弃。
  • JSON 格式:参数列表被解析为 JSON 数组,因此必须遵循 JSON 书写规范,例如用双引号来表示字符串。
  • 主机目录在容器运行时声明:主机目录(挂载点)本质上是依赖于主机的,然而,为了保证镜像的可移植性,牺牲了主机目录的准确性,也就是说,现在是不能保证设置的主机目录在所有主机上都是可用的,因此,不建议在 Dockerfile 中挂载主机目录,VOLUME 指令也不支持用 host-dir 参数设置主机目录,必须指定其他目录作为在创建镜像或运行容器时的挂载点。

11、USER 指令

USER 指令有如下两种书写格式:

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

USER 指令用于设置用户名(或UID)和可选的用户组(或GID),并在镜像运行时,和在 Dockerfile 中 USER 指令之后的任何需要用的用户和用户组的 RUN 指令、CMD 指令 和 ENTRYPOINT 指令使用。

请注意:为用户指定组时,用户将具有指定组的组成员资格。任何其他配置的组成员资格都将被忽略。
警告:当用户没有主要组时,镜像将与根组一起运行。
在 Windows 上,如果用户不是内置账户,则必须先创建该用户——这可以通过作为 Dockerfile 的一部分调用 net user 命令来完成。

FROM microsoft/windowsservercore
# Create Windows user in the container
RUN net user /add patrick
# Set it for subsequent commands
USER patrick

12、WORKDIR 指令

WORKDIR 指令的书写格式:WORKDIR /path/to/workdir
在 Dockerfile 中,WORKDIR 指令可以为其后面的 RUN 指令、COPY 指令和 ADD 指令设置工作目录。若 WORKDIR 不存在,即使它没有在后续的 Dockerfile 中使用,docker 守护进程在编译镜像时也会自动创建工作目录。
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

13、ARG 指令

ARG 指令书写格式:ARG <name>[=<default value>]
ARG 指令可以定义变量,并且这些变量可以通过在构建时用 docker build 命令的 --build-arg <varname>=<value> 选项传递给构建器;此时,如果用户使用了未在 Dockerfile 中定义的构建参数,那么将会抛出警告:[Warning] one or more build-args [] were not consumed. 一个 Dockerfile 中可以使用多个 ARG 指令:

FROM busybox
ARG user1
ARG buildno
# ...

不建议用构建时变量来传递像 GitHub 密钥、用户凭证等数据,因为这会导致数据泄露,任何用户都可以通过 docker history 命令可以查看构建时变量值,如何传递这些数据请参考

13.1、默认值

ARG 可以选择在声明变量时赋予默认值:

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

若 ARG 指令声明了具有默认值的变量,并且在构建时用户并没有赋予任何新值,那么构建器将使用该默认值。

13.2、作用域

ARG 指令声明的变量,一经声明那么在之后 Dockerfile 通过构建阶段(构建阶段可以以 FROM 指令划分)范围内都是有效的,而不是等到变量在命令行或其他地方使用时才生效,例如:

FROM busybox
USER ${user:-some_user}
ARG user
USER $user
# ...

在编译该 Dockerfile 时使用 user 变量:

docker build --build-arg user=what_user

此时,Dockerfile 中第二行指令中的 user 的值解析为 some_user,而第四行指令中的 user 解析为 what_user,这是由于第三行的 ARG 指令声明了 user 变量,且用户在运行 docker build 命令时传了 user=what_user。在由 ARG 指令声明定义变量之前,对变量的任何使用都会导致空字符串。
一个 ARG 变量在定义它的构建阶段结束时,其作用域也随之结束;要在多个构建阶段使用变量,那么就必须在每个阶段内用 ARG 指令声明变量:

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

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

13.3、使用 ARG 变量

ARG 指令和 ENV 指令都可以用来声明定义 RUN 指令要用的参数值,不过,使用 ENV 指令声明的环境变量总会覆盖同名的 ARG 变量:

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 指令使用的 CONT_IMG_VER 的值为 v1.0.0,而非 docker build 命令传递的 v2.0.1。此行为,类似于 shell 脚本,其中本地范围的变量覆盖作为参数传递或从环境继承的变量。对上述例子稍微修改,演示 ARG 指令与 ENV 指令的更为有用的配合使用方式:

FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER=${CONT_IMG_VER:-v1.0.0}
RUN echo $CONT_IMG_VER

与 ARG 指令不同,ENV 指令声明的变量始终保留在构建的镜像里,此时,使用不带 --build-arg 选项的 docker build 命令:

docker build .

运行之后,CONT_IMG_VER 变量会保存在镜像里,并且其值为 v1.0.0,因为第三行的 ENV 指令为其设置了默认值。在这个简单案例中的变量扩展技术,使得我们可以从命令行传递参数,并通过利用 ENV 指令将这些变量保存到最终镜像里。变量扩展技术仅支持有限的 Dockerfile 指令集

13.4、预定义的 ARG 变量

Docker 提供了一组预定义的 ARG 变量,这些变量,无需在 Dockerfile 中声明便可使用;这组变量具体如下:

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

在 docker build 命令中使用 --build-arg 便可使用,例如:

docker build --build-arg HTTPS_PROXY=https://my-proxy.example.com

通常,这些预定义变量不会在记录 docker 历史中,这是为了避免诸如 HTTP_PROXY 变量中的敏感信息的泄露。例如,用如下 Dockerfile 进行构建:

FROM ubuntu
RUN echo "Hello world"

--build-arg HTTP_PROXY=http://user:pass@proxy.lon.example.com
此时,HTTP_PROXY 变量值在 docker 历史记录中不可用,也不会进行缓存;如果更改代理服务器为: http://use:pass@proxy.sfo.example.com,则后续构建并不会出现缓存不命中的问题。如果不想这样,可以通过在 Dockerfile 中用 ARG 指令,显式声明预定义变量的同名变量来达到:

FROM ubuntu
ARG HTTP_PROXY
RUN echo "Hello World"

这个时候,执行该 Dockerfile,那么 HTTP_PROXY 就会记录在 docker 历史中,如果再更改其值,那么后续构建就会出现缓存不命中的问题。

13.5、对构建缓存的影响

ARG 指令声明的变量,虽然不能像 ENV 指令声明的变量那样保存新构建的镜像中,但确也是会对构建缓存造成影响。若 Dockerfile 定义了 ARG 变量,并且在构建过程中发生了值变更,那么 **缓存未命中 **的问题将发生在第一次使用该 ARG 变量时,而非定义。特别的,ARG 指令之后的所有 RUN 指令会隐式地使用 ARG 变量(作为环境变量),那么显然 RUN 指令也可能会出现缓存未命中的问题。看两个例子:

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>,那么,第二行指令不会发生缓存不命中的问题,而第三行则会出现缓存不命中的问题;这是因为,ARG CONT_IMG_VER 令 RUN 行被标识为与运行 CONT_IMG_VER= echo hello 相同,因此,如果 更改,就会发生缓存不命中。在同一命令行中,如果使用如下 Dockerfile 示例:

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

同样第三行指令会出现缓存不命中的问题,因为 ENV 指令声明的变量引用了 ARG 变量,而该变量又在命令行中进行了更改。再来看个例子:

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

显然,ENV 和 ARG 声明了同名变量,并且 ENV 指令还进行了值覆盖,那么,第三行指令不会导致缓存不命中问题,因为 CONT_IMG_VER 有了一个常量值,从而第四行的 RUN 指令使用的环境变量和值在构建期间不会改变。

14、ONBUILD 指令

ONBUILD 指令格式:ONBUILD <INSTRCUTION>
ONBUILD 指令的作用:讲一个触发指令/触发器添加到镜像中,以便在该镜像在后续作为另一个构建的基础时执行
触发器会在下游构建的上下文中执行,其效果就像是在下游的 Dockerfile 中的 FROM 指令之后立即插入一样。而触发器,可以有任何构建指令进行注册。当我们要构建一个作为其他镜像的基础镜像的镜像时,如应用程序构建环境或可以使用用户特定配置自定义的守护进程,那么 ONBUILD 指令都非常有用。
例如,所要构建的镜像为可重用的 Python 程序编译器,则需要将应用程序源代码添加到指定目录中,且可能需要在此之后调用构建脚本,此时显然不能只调用 ADD 和 RUN,因为还没有访问应用程序源代码的权限,而且每个应用程序源代码脚本都是不一样的;虽然可以使用一个简单的 Dockerfile 模板进行复制粘贴,放到每个应用程序源代码中,但是,这种方式的效率何其低下,又容易出差错,还难以更新,由于和特定应用程序的代码混合在一起。高效的做法是使用 ONBUILD 指令:

  1. 当遇到 ONBUILD 指令,docker 会向正在构建的镜像的元数据添加一个触发器,并且不会影响到当前的构建;
  2. 在当前构建阶段结束时,一系列的触发器会存储在镜像清单的 OnBuild 键下,可以用 docker inspect 进行查看;
  3. 而后续构建阶段中,可用 FROM 将新镜像作为新构建阶段的基础;而处理 FROM 指令的部分操作,就是寻找 ONBUILD 触发器,并按照注册的顺序进行先后执行,期间,任何一个触发器失败都会导致整个 FROM 指令的失败,只有当所有触发器无误地执行成功,FROM 指令才能正常执行并继续后面的构建。
  4. 触发器在执行后会从最终镜像中清除,换句话说,它们不会被“孙子”构建所继承。

ONBUILD 指令示例如下:

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

ONBUILD 指令不允许套娃使用,即 “ONBUILD ONBUILD” 是不允许的
ONBUILD 指令可能不会触发 FROM 或 MAINTAINER 指令

15、STOPSIGNAL 指令

该指令的格式:STOPSIGNAL signal
该指令的功能:设置容器退出的系统调用信号,即当该系统调用信号发送到容器中,且容器收到该信号时,容器将退出运行。该信号可以是 SIG<NAME> 格式的信号名称,也可以是与系统调用表中匹配的无符号整数,例如 9;如果没有定义信号,则默认是 SIGTERM
可以使用 docker run 或 docker create 的 --stop-signal 选项覆盖镜像的默认停止信号。

16、HEALTHCHECK 指令

该指令有如下两种格式:

  • HEALTHCHECK [OPTIONS] CMD command:通过在容器内运行命令的方式,检查容器的健康状况
  • HEALTHCHECK NONE:禁用从基础镜像继承的任何健康检查

HEALTHCHECK 指令告诉 Docker 守护进程如何测试容器,以检查它是否正常工作;这可以检查诸如 Web 服务器陷入无限循环并且无法处理新链接的情况,即使服务器仍然在运行。
当容器指定了健康检查时,除了正常状态,便还有健康状态。健康状态的初始值为 starting,每当健康检查通过时,健康状态就会更新为 healthy(无论之前的健康状态如何),而连续不能通过健康检查一定次数后,健康状态就会更为 unhealthy
HEALTHCHECK 指令可用选项有:

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

运行检查检查,将在容器启动后首先运行 interval 秒,然后在每次之前的检查完成后再次运行 interval 秒。如果,检查的单次运行时间超时超出 timeout 指定的秒数,则认为检查失败。start period 为需要时间引导的容器提供初始化时间,在此期间的探测失败将不计入最大重试次数,但是,如果在启动期间健康检查成功,则认为容器已启动,之后的所有连续失败将计入最大重试次数。
一个 Dockerfile 只能有一个 HEALTHCHECK 指令生效,如果同个 Dockerfile 中出现多个 HEALTHCHECK 指令,则默认最后一个生效。
CMD 关键字之后 command 可以是 shell 命令(例如 HEALTHCHECK CMD /bin/check-running)或 exec 数组。
命令的退出状态指示容器的健康状态:

  • 0:成功,容器良好运行,可以正常使用
  • 1:不健康,容器无法正常工作
  • 2:保留,不要使用此退出代码

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

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

为了便于debug 失败的细节,命令在 stdout 或 stderr 上写入的任何输出文本(UTF-8)都将存储在健康状态中,并可以用 docker inspect 进行查看,此类输出应保持简短(当前仅存储前 4096 个字节)。
当容器的健康状态发送变化时,会生成一个带有新状态的 health_status 事件。

17、SHELL 指令

该指令格式:SHELL ["executable", "parameters"]
SHELL 指令允许覆盖用于命令的 shell 形式的默认 shell。Linux 系统的默认 shell 是 ["/bin/sh", “-c”],而 Windows 系统的则是 [“cmd”,"/S","/C"]。SHELL 指令必须以 JSON 格式写入 Dockerfile。SHELL 指令在 Windows 系统上尤为有效,因为 Windows 有两种常用而又截然不同的原生 shell:cmd 和 powershell,以及包括 sh 在那的备用 shell。SHELL 指令可以出现多次,但每条 SHELL 指令都会覆盖先前的 SHELL 指令,并影响后续指令:

FROM microsoft/windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S", "/C"]
RUN echo hello

在 Dockerfile 中像 RUN、CMD 和 ENTRYPOINT 等具有 shell 格式的指令,当它们使用 shell 格式时,会首 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 形式的每条 RUN 指令都需要一个额外的 powershell -command 前缀,使得上述书写形式变得很低效。为了提高效率,一种方式是使用 RUN 指令的 exec 格式——JSON 形式:

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

虽然比起 shell 格式的 RUN 指令,这种写法提高了效率,不用调用额外的 cmd.exe,但,却需要双引号和转义符,使得参数冗长;所以,看看下面使用 SHELL 指令后带来的效率提升:

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

结果:
![image.png](https://www.icode9.com/i/ll/?i=img_convert/56e1594bbc41187df86dd30be1c1337e.png#clientId=u554c06dc-0dff-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u8d0bc25f&margin=[object Object]&name=image.png&originHeight=1320&originWidth=1680&originalType=binary&ratio=1&rotation=0&showTitle=false&size=214940&status=done&style=shadow&taskId=u0b12bd9f-832b-463d-8298-ed065acddf3&title=)
SHELL 指令也可用于修改 shell 的运行方式。 例如,在 Windows 上使用 SHELL cmd /S /C /V:ON|OFF,可以修改延迟的环境变量扩展语义。 如果需要备用 shell,如 zsh、csh、tcsh 等,则 SHELL 指令也可以在 Linux 上使用。




完整的 Dockerfile 示例

以 Zookeeper 的 Dockerfile 为例:

#
# Dockerfile for zookeeper-arm
#

FROM easypi/alpine-arm
MAINTAINER EasyPi Software Foundation

# Install required packages
RUN apk add --no-cache \
    bash \
    openjdk8-jre \
    su-exec

ENV ZOO_USER=zookeeper \
    ZOO_CONF_DIR=/conf \
    ZOO_DATA_DIR=/data \
    ZOO_DATA_LOG_DIR=/datalog \
    ZOO_PORT=2181 \
    ZOO_TICK_TIME=2000 \
    ZOO_INIT_LIMIT=5 \
    ZOO_SYNC_LIMIT=2 \
    ZOO_MAX_CLIENT_CNXNS=60

# Add a user and make dirs
RUN set -ex; \
    adduser -D "$ZOO_USER"; \
    mkdir -p "$ZOO_DATA_LOG_DIR" "$ZOO_DATA_DIR" "$ZOO_CONF_DIR"; \
    chown "$ZOO_USER:$ZOO_USER" "$ZOO_DATA_LOG_DIR" "$ZOO_DATA_DIR" "$ZOO_CONF_DIR"

ARG GPG_KEY=586EFEF859AF2DB190D84080BDB2011E173C31A2
ARG DISTRO_NAME=zookeeper-3.4.12

# Download Apache Zookeeper, verify its PGP signature, untar and clean up
RUN set -ex; \
    apk add --no-cache --virtual .build-deps \
        gnupg wget; \
    wget -q "http://www.apache.org/dist/zookeeper/$DISTRO_NAME/$DISTRO_NAME.tar.gz"; \
    wget -q "http://www.apache.org/dist/zookeeper/$DISTRO_NAME/$DISTRO_NAME.tar.gz.asc"; \
    export GNUPGHOME="$(mktemp -d)"; \
    gpg --keyserver ha.pool.sks-keyservers.net --recv-key "$GPG_KEY" || \
    gpg --keyserver pgp.mit.edu --recv-keys "$GPG_KEY" || \
    gpg --keyserver keyserver.pgp.com --recv-keys "$GPG_KEY"; \
    gpg --batch --verify "$DISTRO_NAME.tar.gz.asc" "$DISTRO_NAME.tar.gz"; \
    tar -xzf "$DISTRO_NAME.tar.gz"; \
    mv "$DISTRO_NAME/conf/"* "$ZOO_CONF_DIR"; \
    rm -rf "$GNUPGHOME" "$DISTRO_NAME.tar.gz" "$DISTRO_NAME.tar.gz.asc"; \
    apk del .build-deps

WORKDIR $DISTRO_NAME
VOLUME ["$ZOO_DATA_DIR", "$ZOO_DATA_LOG_DIR"]

EXPOSE $ZOO_PORT 2888 3888

ENV PATH=$PATH:/$DISTRO_NAME/bin \
    ZOOCFGDIR=$ZOO_CONF_DIR

COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["zkServer.sh", "start-foreground"]

redis 的

FROM easypi/alpine-arm:3.8

# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
RUN addgroup -S redis && adduser -S -G redis redis

# grab su-exec for easy step-down from root
RUN apk add --no-cache 'su-exec>=0.2'

ENV REDIS_VERSION 4.0.10
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-4.0.10.tar.gz
ENV REDIS_DOWNLOAD_SHA 1db67435a704f8d18aec9b9637b373c34aa233d65b6e174bdac4c1b161f38ca4

# for redis-sentinel see: http://redis.io/topics/sentinel
RUN set -ex; \
	\
	apk add --no-cache --virtual .build-deps \
		coreutils \
		gcc \
		jemalloc-dev \
		linux-headers \
		make \
		musl-dev \
	; \
	\
	wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
	echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
	mkdir -p /usr/src/redis; \
	tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
	rm redis.tar.gz; \
	\
# disable Redis protected mode [1] as it is unnecessary in context of Docker
# (ports are not automatically exposed when running inside Docker, but rather explicitly by specifying -p / -P)
# [1]: https://github.com/antirez/redis/commit/edd4d555df57dc84265fdfb4ef59a4678832f6da
	grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
	sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
	grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
# for future reference, we modify this directly in the source instead of just supplying a default configuration flag because apparently "if you specify any argument to redis-server, [it assumes] you are going to specify everything"
# see also https://github.com/docker-library/redis/issues/4#issuecomment-50780840
# (more exactly, this makes sure the default behavior of "save on SIGTERM" stays functional by default)
	\
	make -C /usr/src/redis -j "$(nproc)"; \
	make -C /usr/src/redis install; \
	\
	rm -r /usr/src/redis; \
	\
	runDeps="$( \
		scanelf --needed --nobanner --format '%n#p' --recursive /usr/local \
			| tr ',' '\n' \
			| sort -u \
			| awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \
	)"; \
	apk add --virtual .redis-rundeps $runDeps; \
	apk del .build-deps; \
	\
	redis-server --version

RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD ["redis-server"]
上一篇:Dockerfile 优化


下一篇:Docker从入门到精通(五)——Dockerfile