【转】Dockerfile
阅读原文时间:2023年07月11日阅读:1

docker build可以基于Dockerfile和context打包出一个镜像,其中context是一系列在PATHURL中指定的位置中的文件(context是递归的,包含子目录下的文件,build时会将context中的全部内容传递给docker daemon)。其中PATH是指本地文件系统,URL则是git仓库的地址。比如在当前目录下创建镜像:docker build .,此时context就是当前目录.。build的运行是由docker daemon操作的,并不是CLI(Command-Lin Interface)。通常build时强烈建议创建一个空目录,然后将Dockerfile文件放入,最后只将build需要用到的文件放到该目录下。注:千万不要用root或者/作为PATH,docker daemon会将整个目录下的文件读取。为了尽可能提高Docker的build性能,和git一样可以在上述目录中添加.dockerignore文件排除一些不要的文件,比如正常打包时都会排除掉Dockerfile:

1

./Dockerfile*

 默认build时会基于context的根目录下Dockerfile文件打包(如果不存在会报错),当然可以通过-f DockerfilePath的方式指定任意位置的Dockerfile位置,但后面的context必须在Dockerfile所在位置的目录或者父目录(指定为其他目录会报错,比如指定为~/k8s/),比如:

1
2

# ~/docker/test1/Dockerfil指定Dockerfile位置,~/docker/指定context一定是Dockerfile所在目录或父目录
docker build -f ~/docker/test1/Dockerfile ~/docker/

 此外还可以指定如果镜像构建成功存放的仓库和标签(即repository和tag),如构建时打上阿里云的仓库标签:docker build -t registry.cn-shanghai.aliyuncs.com/hhu/redis:4.0-alpine3.9 .。注:这里可以不断追加-t 标签的方式打上多个仓库仓库标签,比如:docker build -t registry.cn-shanghai.aliyuncs.com/hhu/redis:4.0-alpine3.9 -t test1/redis:4.0-alpine3.9 -t test2/redis:4.0-alpine3.9 .,上述栗子一下就会出现3个标签的镜像,但它们的ID是相同的,仅仅是标签不同。

 和大部分的应用类似,docker daemon会在运行Dockerfile中的指令时会先验证文件的可行性,比如语法错误就会返回error。Dockerfile中的每条指令都是独立运行的(比如RUN cd /tmp并不会影响下一条指定的执行),并不会创建新的镜像。不论何时,docker都会尽可能重复应用缓存的中间镜像以便加快build的过程,所以有时在build时可能会在控制台看到Using cache的字样。可以使用的缓存镜像只有在本地具备父链才能使用(即缓存的镜像先前被创建过或者整个镜像链都使用docker load加载过)。如果在build时希望使用某个特定的镜像缓存可以使用--cache-from选项,注:使用--cache-from时不需要具备本地父链,可能会从其他的注册中心拉下来。

 Dockerfile的语法为INSTRUCTION arguments,不区分大小写,但为了区分指令和参数,约定指令全部大写、参数尽量小写。docker会按序执行Dockerfile中的指令,Dockerfile文件必须以FROM指令开头(除指令解释器外),它指定了当前构建镜像的基础镜像。通常Dockerfile中也允许注释,注释行以#开头,但指令解释器除外,比如。

2.1 指令解释器

指令解释器是可选的,它会影响下面指令的处理方式,但不会在增加层layer,也不会作为构建的步骤展示出来。docker一旦处理了注释、空行或生成器指令,就不会再看是它否分析器指令了,就算下面还有指令解释器,docker也会将其视为注释(都是以#开头),所以Dockerfile中只要有指令解释器,请务必尽可能的靠前声明(约定解释器为小写),应该Dockerfile的第一行。指令解释器不支持行继续的行为(Linux中表现为\),同时指令解释器后应该空一行。总结一下,应该遵循如下的规则:

  1. #开头,指令解释器小写,如果有位于Dockerfile的第一行;
  2. 不支持行继续操作行为,即\
  3. 解释器后应留一个空白行;

下面是一些反例:

1
2
3
4
5
6
7
8
9
10
11
12
13

# 反例1:使用了行继续 \
# direc \
tive=value

# 反例2:只会识别第一个,value2不应该出现会被当成注释
# directive=value1
# directive=value2

FROM ImageName

# 反例3:指令解释器非首行
FROM ImageName
# directive=value

注:指令解释器允许使用空格符,比如下面的几种方式都是一样有效的:

1
2
3
4

#directive=value
# directive =value

directive= value

# directive = value

 Dockerfile支持的指令解释器有:syntaxescape。其中syntax仅支持下一代构建工具BuildKit,这里暂不进行捣鼓,直接看escape,它定义的是Dockerfile中的转义字符,有2种定义方式:

1
2
3
4
5

# escape=\

# 或者

# escape=`

如果不指定默认转义字符为\。转义字符既用于转义一行中的字符,也用于转义换行符。这使得Dockerfile指令可以跨越多行,无论转义分析器指令是否包含在Dockerfile中,转义都不会在RUN命令中执行,除非在行尾执行。在windows上,\ 是目录之间的分隔符,将转义字符设置为 ` 是非常有用的,它和 PowerShell 一致,比如在windows的powerShell中:

1
2
3

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

没有指定指令解释器,默认为\,那此时第二行中COPY testfile.txt c:\\就出现了转义字符(c:\\中的第一个\),实际意思就是COPY testfile.txt c:\,那剩下的那个\就变成了行继续的操作,所以第二行和第三行是一行实际为:COPY testfile.txt c:\RUN dir c:第三行的转义字符在行末属于可执行范围。在powershell中,为了避免这个问题可以使用 ` 作为转义字符就不会出现上述的问题:

1
2
3
4
5

# escape=`

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

上述Dockerfile拆解成了3个指令(除去指令解释器,之前是2条指令)。

2.2 环境替换

 docker中可以使用ENV为特定的指令声明环境变量,转义字符也被处理为将类似变量的语法包含到字面上的语句中。环境变量在Dockerfile中使用$variable_name${variable_name}标注,上述2标注方式是一样的,第2种{xx}的方式通常用于解决变量名没有空格的问题(不是很理解),比如${foo}_bar,此外这种大括号的方式还支持一些标准的bash修饰符,比如(word只是随意取的值):

  • ${variable:-word}表示如果variable设置了,那它的值就是设置的值;如果variable没有设置,那word就是它的值(有点三目运算的意思)。
  • ${variable:+word}表示如果variable设置了,那word就是它的值;如果variable没有设置,那它的值就是空字符串。

注:变量上也是可以使用转义字符的,比如使用的转义字符是默认的\,那在变量中\$foo\${foo},那就表示它就是个普通的字符串$foo${foo},而不是对应foo的值。

 环境变量支持如下的指令:ADDCOPYENVEXPOSEFROMLABELSTOPSIGNALUSERVOLUMEWORKDIRONBUILD(1.4版本之后该指令只有在和上述其他指令结合使用时才能使用环境变量)。下面是一个示例(解析的指令在#后已经标出):

1
2
3
4
5
6
7
8
9

FROM busybox
# 声明环境变量foo,表示的值为/bar
ENV foo /bar
# 等同于 WORKDIR /bar
WORKDIR ${foo}
# 等同于 ADD . /bar
ADD . $foo
# 等同于 COPY $foo /quux
COPY \$foo /quux

 环境变量的替换将在整个指令中对每个变量使用相同的值,比如下面的示例:

1
2
3

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

上述的定义过程中,def的值为hello,而不是byeghi的值为bye,因为它不是将abc设置为bye那条指令的一部分。可能有点绕人,但只要记住一点就行了,在使用变量时,变量的值永远都是前面最靠近使用该变量的指令的定义(但不包含本身),如第2条指令使用def=$abc,向前寻找变量abc,排除自身,发现最靠近的abc定义在第一条指令中abc=hello,所以def=hello;同样第3条指令使用ghi=$abc,向前寻找变量abc,发现第2条指令最靠近它,虽然第1条指令也有abc的声明,但没有第2条指令靠近,所以取第2条指令之中abc的声明bye,所以ghi=bye

2.3 .DOCKERIGNORE文件

 和.gitignore类似,在CLI(Command-Lin Interface)将context发送给docker daemon时,它会关注一下context根目录下的.dockerignore文件,如果存在,CLI将修改context以排除与该文件中匹配的文件和目录。这样可以避免将一些不必要的大文件和敏感发送给docker daemon并通过ADDCOPY将这些文件加入到镜像中。同样的.dockerignore文件中也是允许注释的,行首用#标注即可,比如:

1
2
3
4

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

上述的# comment是一个注释,CLI将不会管它;*/temp*表示CLI将排除context根目录中的任何一级子目录中名称以temp开头的文件或目录;*/*/temp*表示CLI将从context根目录下任何两级子目录中排除以temp开头的文件或目录(如/somedir/subdir/temporary.txt);temp?表示CLI将排除context根目录中名为temp的单字符扩展名的文件和目录(比如/tempa)。此外它还支持**通配符(匹配0或多个目录,注意是用于匹配目录的!!),如**/*.go就是匹配context目录(包括它所有的子孙目录)下所有以.go结尾的文件。

 如果行首是!可以用于排除例外的情况,示例如下:

1
2

*.md
!README.md

上述就是排除所有以.md结尾的文件,但README.md除外不用排除。

!的位置会影响.dockerignore文件中与特定文件匹配的属性决定它是包含的还是排除的最后一行,下面是2个示例(有点懵懂):

1
2
3
4
5
6
7
8
9

# 示例1:除了README-secret.md之外,排除上下文中含任何其他以.md结尾的文件。
*.md
!README*.md
README-secret.md

# 示例2:所有README文件都包含在内,中间的 README-secret.md 没有效果,因为 !readme*.md 与 readme-secret.md 匹配却在最后
*.md
README-secret.md
!README*.md

 在.dockerignore中甚至可以将Dockerfile.dockerignore排除掉,但它们仍然会被发送到daemon因为docker需要这些进行job,但ADDCOPY指令不会将它们复制到镜像中。

2.4 具体指令(只包含18.09版本最新指令)

2.4.1 FROM

FROM指令初始化一个新的构建阶段,它为后续的指令设置了一个基本镜像,因此它必须是在所有指令的最前面定义的(除指令解释器),这个基本镜像可以是任何一个合法的镜像。语法如下:

1
2
3
4
5
6
7
8

# 方式1
FROM [AS ]

# 方式2
FROM [:] [AS ]

# 方式3
FROM [@] [AS ]

FROM可以在一个Dockerfile文件中出现多次,创建多个镜像或使用一个生成阶段作为另一个的依赖。只需在每条新的FROM指令之前记录提交所输出的最后一个镜像的ID,每个FROM指令会清除之前其他指令创建的状态。name是可选的,可以通过在FROM指令中添加AS name给一个新的构建阶段起一个名字,这个名字可以在后来的FROM指令和COPY --from=<name|index>指令中引用对应的镜像。同样tagdigest也是可选的,如果不指定,默认为latest

2.4.2 ARG

ARG是唯一一个可以在FROM指令之前的,FROM指令是支持在第一个FROM之前由ARG指令声明的变量,比如:

1
2
3
4
5
6
7
8
9

# 声明变量 CODE_VERSION,值为latest
ARG CODE_VERSION=latest
# 在FROM指令中使用由ARG声明的变量
FROM base:${CODE_VERSION}
CMD /code/run-app

# 在FROM指令中使用由ARG声明的变量
FROM extras:${CODE_VERSION}
CMD /code/run-extras

FROM指令之前ARG变量的声明是属于构建阶段之外的,因此不能在FROM指令之后的其他指令中使用。如果要使用第一个From之前声明的ARG值,需要在构建阶段内使用没有指定值的ARG指令,比如:

1
2
3
4
5

ARG VERSION=latest
FROM busybox:$VERSION
# 声明一个没有默认值的变量 ARG 指令
ARG VERSION
RUN echo $VERSION > image_version

2.4.3 RUN

RUN指令将会在当前镜像的顶部的一个新层layer中执行命令并提交结果,提交镜像的结果将用于Dockerfile文件中的下一步骤。分层RUN并生成新的新的结果符合Docker的核心思想(即提交的成本很低且可以从镜像历史的任何一点创建容器,很像源代码管理)。语法如下:

1
2
3
4
5

# 形式1:shell的形式,命令运行在shell中,Linux中默认运行 /bin/sh -c,win中默认运行 cmd /S /C
RUN

# 形式2:exec的形式
RUN ["executable", "param1", "param2"]

exec形式可以避免对shell字符串进行munging,可以在不包含指定可执行shell的基本镜像中运行RUN指令。shell形式默认的shell可以使用SHELL命令更改,比如要使用除/bin/sh之外的shell,就需要使用传入所需shell的exec形式:

1

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

在shell形式中可以使用\以继续运下一行的 RUN 指令(即使用\作为行继续操作符,可以跨行运行),比如:

1
2

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

上述使用的是行继续操作符,实际是一个指令,等同于:RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'。exec形式会被编译成JSON形式,所以需要使用双引号"而不是单引号',此外在exec形式中应该避免\,如RUN ["c:\windows\system32\tasklist.exe"]会出错,正确的写法应该为RUN ["c:\\windows\\system32\\tasklist.exe"]

 和shell形式不同,exec形式不会调用shell,所以平常在shell正常处理的在exec形式中可能无法处理,比如:RUN [ "echo", "$HOME" ]不会替换变量$HOME,如果要用shell处理,要么直接用shell形式RUN <command>,要么直接执行shell,如:RUN [ "sh", "-c", "echo $HOME" ]RUN指令的缓存在下一个构建阶段不会失效,比如RUN apt-get dist-upgrade -y将会在下一个阶段重复使用,当然也可以使用--no-cache参数来让RUN指令失效,比如:docker build --no-cacheRUN指令的缓存也可以通过ADD指令失效。

2.4.4 CMD

 一个Dockerfile文件中仅可以只有一个CMD指令,如果有多个CMD,仅有最有一个CMD奏效,它的主要目的是为正在执行的容器提供默认值,这些默认值可以包括可执行文件,也可以省略可执行文件(但此时必须指定一个ENTRYPOINT指令),主要语法如下:

1
2
3
4
5
6
7
8

# 形式1:exec形式,这是首选形式
CMD ["executable","param1","param2"]

# 作为ENTRYPOINT默认参数
CMD ["param1","param2"]

# shell形式
CMD command param1 param2

如果CMD用于为ENTRYPOINT指令提供默认参数(即上述第2种形式),那CMDENTRYPOINT指令都应该用JSON格式指定(双引号")。当在shell或exec形式,CMD指令设置在运行镜像时要执行的命令,如果使用shell形式的CMD<command>应该是/bin/sh -c

1
2

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

如果不想在shell中运行<command>,那就必须使用JSON数组的形式并使用可执行文件的全路径。数组形式是CMD的首选形式,任何额外的参数必须在数组中做独立表达,如:

1
2

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

如果想每次容器都执行相同的可执行文件,应该考虑使用ENTRYPOINTCMD的配合使用。如果指定docker run的参数,那么它们将覆盖在CMD中指定的默认值。

注:RUNCMD可能有些像,主要区别是:RUN是运行一个命令然后提交结果,而CMD在构建镜像的过程中不会干任何事,但指定镜像的预期命令。

2.4.5 LABEL

LABEL指令用于向镜像添加元数据,它是一个键值对,如果key值或value值中包含空格,需要使用双引号和反斜杠,一个镜像可以有多个标签,下面是示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 有空格用引号包裹
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 multi.label1="value1" multi.label2="value2" other="value3"

# 一个LABEL中设置多个标签
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"

FROM指令中指定的基本镜像中的标签会被当前构建的镜像继承过来,这些标签如果在当前构建的镜像指定相同的key值但不同的value值时是会被覆盖的,使用docker inspect xxx可以查看镜像xxx的标签,下面是openjdk部分标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

docker inspect 3675b9f543c5

# 结果
[
{
"Id": "sha256:3675b9f543c5db0d8d554f8e103a4cb98db26be5e5c88019cbe49dcd4fea4685",
"RepoTags": [
"openjdk:8-jdk-alpine"
],
"RepoDigests": [
"openjdk@sha256:2a52fedf1d4ab53323e16a032cadca89aac47024a8228dea7f862dbccf169e1e"
],
"Parent": "",
"Comment": "",
"Created": "2019-04-10T01:52:39.548813341Z",
"Container": "2d38510bb479e015e7990ac60a6af8855e1c9f7bf6b6a66bde2c8625355eccd9",
"ContainerConfig": {

},

"Metadata": {
"LastTagTime": "0001-01-01T00:00:00Z"
}
}
]

2.4.6 EXPOSE

EXPOSE指令表示了docker容器运行时监听的特定网络端口,可以指定监听TCP(默认)或UDP,在实际运行容器docker run时可以使用-p标识去发布并映射到一个或多个端口。比如默认监听TCP,现在改成UDP:

1

EXPOSE 80/udp

也可以同时监听TCP和UDP的80端口:

1
2

EXPOSE 80/tcp
EXPOSE 80/udp

如果在docker run时指定-p,那端口将为TCP和UDP各暴露一次。-p在主机上使用了一个短暂的高阶主机端口,因此TCP和UDP的端口将不相同(不懂)。当然就算在Dockerfile中配置过端口,在docker run时可以通过-p来覆盖这里定义的,如:

1

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

docker network可以在容器之间创建网络进行交流,不需要暴露或发布特定端口,因为连接到网络的容器可以通过任何端口相互通信。

2.4.7 ENV

ENV指令用于设置环境变量,将环境变量设置<key>设置为<value>,这里设置的环境变量将会存在于所有后续指令的环境变量中,可以在命令行进行替换,可以使用docker inspect xxx查看环境变量的值。ENV有如下形式:

1
2
3
4
5
6
7
8
9
10
11
12

# 形式1:设置单个变量值,在第一个空格后的字符串就是value(可能包含空格)
ENV
# 形式1示例,定义了3个环境变量
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy

# 形式2:允许同时设置多个环境变量,可以使用双引号 和行继续操作符 \
ENV =
# 形式2示例,定义了2个环境变量
ENV myName="John Doe" myDog=Rex\ The\ Dog \
myCat=fluffy

使用ENV设置的环境变量将从生成镜像到镜像容器运行保持不变,使用docker inspect可以看到这些环境变量,在运行容器时可以使用docker run --env <key>=<value>来改变环境变量。如果为单个指令设置值的话,尽量使用RUN <key>=<value> <command>

2.4.8 ADD

ADD指令可以复制新的文件、目录或者远程URL中的<src>,将它们添加到镜像的文件系统中(具体是镜像的<dest>目录),具体语法有2种:

1
2
3
4
5

# 形式1:将context中 src 复制到镜像中的 dest 目录
ADD [--chown=:]

# 形式2:和形式1一样,就多了双引号,主要用于src和dest路径中存在空格的情况
ADD [--chown=:] ["",… ""]

<src>是相对于context的相对路径,可以有多个,而且它可以包含通配符,比如:

1
2
3
4

# 将所有以hom开头的为文件复制进去
ADD hom* /mydir/
# ? 匹配单个字符,如"home.txt"
ADD hom?.txt /mydir/

<dest>是镜像中的一个路径,可以是绝对路径,也可以是相对于WORKDIR的一个相对目录(以/开头就是绝对路径),如下:

1
2
3
4
5

# 将 test 复制到 `WORKDIR`/relativeDir/ 的相对路径
ADD test relativeDir/

# 将 test 复制到绝对路径 /absoluteDir/
ADD test /absoluteDir/

当复制包含特殊字符名字(比如[])的文件或目录时,需要做一些处理防止被误认为通配符,比如复制arr[0].txt文件:

1
2

# 将 arr[0].txt文件复制到镜像中的 /mydir/ 目录
ADD arr[[]0].txt /mydir/

注:其中--chown是仅支持建立再Linux容器上的Dockerfile,win上无效。所有新文件或目录都是使用UID和GID为0开头(Linux中UID和GID即表示UserId和GroupId,0是超级用户,500起则是普通用户,表示的一种权限)被创建,除非使用--chown选项指定用户、用户组(或用UID和GID,2种方式可以混用),如果指定了用户却不指定用户组(或指定了UID却不指定GID),那将会使用和UID一样的GID。下面是示例:

1
2
3
4

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

ADD遵循下面的规则:

  • <src>必须在context目录中,不能出现context之外的目录,CLI只将context发送给daemon,docker是无法读取context之外目录的文件、目录的;
  • <src>如果是一个URL:
    • 如果<dest>不以斜杠/结尾,文件将会从URL下载并被复制到<dest>
    • 如果<dest>以斜杠/结尾,那将从URL推断文件名并将文件下载到<dest>/<filename>,如ADD http://example.com/foobar /将会创建/foobar文件;
  • 如果<src>是一个目录,那整个目录的内容(只是它内部的内容但不包含目录本身)将会被复制,包括文件系统的元信息;
  • 如果<src>是本地context目录下的压缩包,那它将会被解压成一个目录(前提是可识别的压缩格式:gzip, bzip2 or xz),但仅限于context是本地目录的情况,如果<src>是URL资源就不会自动解压了。
  • 如果<src>有多个,那<dest>必须只能是一个目录(即必须/结尾);
  • 如果<dest>不以/结尾,那将会被认为是一个文件(不是目录),那<src>的内容将会被写入到该文件中;
  • 如果<dest>不存在,那将会创建(丢失的目录也会随之被创建);
2.4.9 COPY

COPY和上述的ADD指令功能一样,但COPY<src>不能是URL,也不能对压缩包进行解压,语法类似:

1
2

COPY [--chown=:]
COPY [--chown=:] ["",… ""]

2.4.10 ENTRYPOINT

ENTRYPOINT允许配置容器,语法为:

1
2
3
4
5

# 形式1:exec形式,优先
ENTRYPOINT ["executable", "param1", "param2"]

# 形式2:shell形式
ENTRYPOINT command param1 param2

比如以默认内容启动nginx,监听80端口:docker run -i -t --rm -p 80:80 nginx。运行docker run xxx时指定命令行参数将会追加到exec形式的ENTRYPOINT指令中,且覆盖使用CMD指令指定的所有元素,允许将这些参数传给entry point,-d(即docker run <image> -d)即可将将这些参数传给entry point,甚至可以使用docker run --entrypoint选项覆盖Dockerfile中的ENTRYPOINT指令。shell形式禁止使用任何CMDrun命令行参数,缺点是ENTRYPOINT将作为/bin/sh-c的子命令启动,不会传递信号,即可执行文件经不会成为容器的PID 1且不会收到 Unix 信号,因此可执行文件将收不到docker stop <container>发的SIGTERM。Dockerfile中最后一条ENTRYPOINT指令才有效。

【exec形式的ENTRYPOINT

 可以使用exec形式的ENTRYPOINT设置相当稳定的默认命令和参数,然后使用任意一种形式的CMD设置可能改变的额外默认值,如:

1
2
3

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

在运行该镜像容器时,top是唯一的进程。在命令行中可以使用--entrypoint选项来覆盖Dockerfile中ENTRYPOINT指令。

注:

  1. exec形式必须使用双引号""(最终是被编译成JSON);
  2. exec形式不会对shell命令中的变量做替换(如$HOME不会被替换成用户根目录,比如/root),如有需要必须指定一个shell目录,如ENTRYPOINT [ "sh", "-c", "echo $HOME" ]

【shell形式的ENTRYPOINT

 shell形式可以直接指定字符串,默认就会在/bin/sh -c中执行,理所当然的可以进行变量替换。

【总结】

CMDENTRYPOINT指令定义了在运行容器时什么命令会执行,规则如下:

  1. Dockerfile应当指定至少一个CMDENTRYPOINT指令;
  2. 在将容器用作可执行文件时应当定义ENTRYPOINT
  3. CMD指令应当作为ENTRYPOINT指令指定默认参数的一种方式,或者在容器中执行特殊命令;
  4. CMD指令可以在运行容器时指定被重写;
  5. 如果CMD在基础镜像上定义了,那在当前镜像中如果设置ENTRYPOINT将会重置上述的CMD的值(变为空值),此时必须在当前镜像中定义CMD的值;
2.4.11 VOLUME

VOLUME指令用于创建指定名字的挂载点,并将其标记为保存来自本地主机或其他容器的外部装入的卷。形式有JSON和纯字符串的:

1
2
3
4
5
6
7

# 形式1(JSON)
VOLUME ["/var/log/"]

# 形式2
VOLUME /var/log
# 可以指定多个
VOLUME /var/log /var/db

比如:

1
2
3
4

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

将会创建一个新的挂载点/myvol/greeting并将greeting文件复制到上述新建的存储卷。使用VOLUME有如下的注意点:

  1. 基于win平台存储卷的容器:容器内部存储卷的目的地必须为空目录或者不存在的目录(此时docker会自动创建),此外指定目录不能为C盘;
  2. Dockerfile中更改存储卷:如果任何生成步骤在声明卷后然后又更改卷内的数据,则这些更改将被丢弃(即存储卷的使用周期只有当前容器);
  3. 由于JSON,注意双引号;
  4. 在主机目录在容器运行时声明:主机目录(即挂载点)是天然依赖主机的,为了保持映像的可移植性,因为不能保证给定的主机目录在所有主机上都可用,因此不能在Dockerfile中挂载主机目录,VOLUME指令不支持指定host-dir参数,在创建或者运行容器时必须指定挂载点;
2.4.12 USER

USER指令用于在运行镜像中的RUNCMDENTRYPOINT指令时设置用户名(或UID)以及用户组(可选,或者GID),主要形式如下:

1
2
3
4
5

# 形式1
USER [:]

# 形式2
USER [:]

注:

  1. 当用户没有用户组时,镜像将会运行在root用户组下;
  2. win中,如果用户不是内置的账户,那必须先进行创建,可以使用net user来创建,下面是示例:

1
2
3
4
5

FROM microsoft/windowsservercore
# 在容器中先创建win用户
RUN net user /add patrick
# Set it for subsequent commands
USER patrick

2.4.13 WORKDIR

WORKDIR指令用于设置RUNCMDENTRYPOINTCOPYADD指令的工作目录,如果WORKDIR指定的目录不存在,就会被创建(即使后续指令没有使用该目录),该目录可以在Dockerfile中使用多次,基本语法为:

1
2

WORKDIR /path/to/workdir
`

如果提供了相对路径,那它是先前WORKDIR指定目录的相对路径,下面是示例:

1
2
3
4
5
6
7

# 示例
# 绝对路径
WORKDIR /a
# 相对路径
WORKDIR b
WORKDIR c
RUN pwd

最终的路径为/a/b/c。此外WORKDIR指令可以解析之前使用ENV设置的环境变量,如:

1
2
3
4

ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
# 输出应为 /path/$DIRNAME
RUN pwd

2.4.14 构建时参数ARG

ARG指令用于用户在创建镜像时定义的一些变量,语法:

1

ARG [=]

其中默认值是可选的,如果在构建时不指定构建参数将会使用默认值。可以在docker build时使用--build-arg <varname>=<value>指定这些变量的具体值,如果用户指定了Dockerfile中未定义的构建参数,构建时将会出现下面的日志:

1

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

可以定义多个ARG指令,比如:

1
2
3
4

# 注:默认值是可省的
FROM busybox
ARG user1
ARG buildno

注:不建议将密码这类的信息作为构建参数,因为任何用户都可以使用docker history <images>查看到构建参数。

【作用域】

ARG变量定义从dockerfile中定义它的地方开始生效,而不是从参数在命令行或其他地方的使用开始生效,比如:

1
2
3
4

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

构建时使用docker build --build-arg user=what_user .,实际在Dockerfile中的第2行中定义了USER,并将变量user的值假设为some_user,第4行的USER指令定义了变量user是什么(将会从命令行中传过来)。在用ARG指令定义变量之前,任何变量的使用都是空字符串(那是,没定义就使用不报错就不错了)。ARG指令的作用域只在定义ARG的阶段生效,如果需要在多个阶段使用arg,每个阶段必须都包含ARG指令,如:

1
2
3
4
5
6
7

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

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

【使用】

可以使用ARGENV指令指定RUN指令中指定的变量,使用ENV指令定义的环境变量会覆盖掉ARG指令中同名的变量,比如:

1
2
3
4

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 .最终的CONT_IMG_VER变量输出为v1.0.0

【预定义的ARG】

Docker中有一些预定义好的参数ARG,这些参数可以直接在build时指定,而无需在Dockerfile中先进行定义,预定义的参数有:

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

使用时直接通过--build-arg <varname>=<value>传值进去即可。默认预定义的参数是无法使用docker history看到的,如果要覆盖默认行为(即预定义参数无法通过docker history看到),可以在Dockerfile中定义一个同名参数覆盖即可,如:

1
2
3

FROM ubuntu
ARG HTTP_PROXY
RUN echo "Hello World"

【构建缓存的影响】

ARG变量不会像ENV变量一样将其持久化到镜像中,但它也以类似的方式影响着构建缓存。如果Dockerfile中定义了和之前构建定义的不同ARG变量值,在第一阶段就会出现“cache miss”,尤其是在所有的RUN指令都是用了之前ARG定义的变量就会造成上述的缓存未命中。所有预定义的ARG变量并不会被缓存除非在Dockerfile中覆盖过(换而言之,所有Dokcerfile中使用ARG定义的变量都会被缓存)。下面是示例:

1
2
3
4
5
6
7
8
9

# Dockerfile1
FROM ubuntu
ARG CONT_IMG_VER
RUN echo $CONT_IMG_VER

# Dockerfile2
FROM ubuntu
ARG CONT_IMG_VER
RUN echo hello

在构建时指定--build-arg CONT_IMG_VER=<value>,在上述2个Dockerfile的构建过程中,第3行会出现 cache miss 的提示(但我操作过程中进行的很顺利啊,并没有出现这个提示啊( ̄ ‘i  ̄;),我是谁,我在哪……)。

2.4.15 ONBUILD

 当一个镜像A被用于另一个镜像B的基本镜像时(即FROM),ONBUILD指令用于向一个镜像中添加一个 trigger,以便可以在一段时间后执行。触发器将在下游(即当前构建的镜像B)生成的context中执行,只要它是在下游Dockerfile中FROM指令之后立即插入的。流程如下:

  1. 当看到ONBUILD时,构建器会向当前正在构建的镜像的 metadata 中添加 trigger,该指令不会影响当前构建;
  2. 在构建过程结束时,一系列的 trigger 将会存储到镜像的manifest中的OnBuild字段(一个数组对象)中,可以使用docker inspect查看这个字段;
  3. 然后这个镜像可能用作其他镜像构建的基础镜像,作为FROM指令的一部分,下游的构建器将会寻找ONBUILD trigger并按它们注册的顺序执行。如果其中某一个trigger执行失败了,FROM指令将被中止,从而导致构建失败;
  4. 触发器在执行后从最终镜像中清除,它们不是由“孙子”构建继承;

下面是示例:

1
2
3
4

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

注:

  • ONBUILD指令禁止嵌套使用,即类似于ONBUILD ONBUILD
  • ONBUILD指令可能不会触发FROMMAINTAINER指令(即尽量不要在ONBUILD指令中注册FROMMAINTAINER指令);
2.4.16 STOPSIGNAL

STOPSIGNAL指令用于设置系统发送到容器中、目的用于退出的命令,这个信号可以是内核的 syscall 表中有效的无符号数字(比如9),或格式为SIGNAME的信号名字(比如SIGKILL)。基本语法如下:

1

STOPSIGNAL signal

2.4.17 HEALTHCHECK

HEALTHCHECK指令用于告诉Docker如何检测容器是否处于健康(工作)状态,有点类似于服务检测,这个功能可以检测到一些情况,如Web服务器卡在无限循环中,无法处理新连接,即使服务器进程仍在运行。当一个容器指定了一个健康检测,那该容器除了它本身的正常状态外,还会额外有一个“健康状态”health status(字段为health_status),健康状态初始值为starting,只有当健康检查(可以称作容器体检)通过了,它才会变成healthy,在连续次数(这个次数可以指定)的体检结果都不通过那 health status 将变成unhealthyHEALTHCHECK的基本语法为:

1
2
3
4
5

# 形式1:在容器内运行指定命令进行容器体检
HEALTHCHECK [OPTIONS] CMD command

# 形式2:禁用从基映像继承的任何运行状况检查
HEALTHCHECK NONE

上述形式1中的OPTIONS可用选项有:

  • --interval=DURATION:表示容器体检的时间间隔,通常在容器首次进行体检(此时可以理解为delay)和体检失败后再次体检时用到,默认是30s;

  • --timeout=DURATION:表示容器体检的超时时间,一次体检时长超过此时间则认为本次体检失败,置为默认为30s;

  • --start-period=DURATION:表示容器的启动时长,在这个时间段内体检失败的结果不作数(即不算作retries),但是如果在这段时间内,容器体检成功了,从此刻(即使此刻还在start-period内)开始所有之后的检查就开始生效,retries开始统计,就认为容器已经启动成功,默认是0s;

  • --retries=N:表示容器体检重试的最大次数,即第一次体检失败并不意味着容器不健康,只有失败时尝试指定次数都是失败,该容器此时的health status才会置为unhealthy,默认是3次;

     形式1中CMD后可以是shell命令,如HEALTHCHECK CMD /bin/check-running,也可以是exec数组(和之前类似),这些命令的执行完成后的状态就可以推测出容器的health status,下面是多数命令的常用退出状态码:

  • 0:success - 容器体检健康,可以正常使用;

  • 1:unhealthy - 容器体检不健康,不能进行正常工作;

  • 2:reserved - 不要使用此退出代码;

比如:

1
2

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

从前至后,参数分别为:检测间隔5分钟,检测超时时间为3s,检测行为指令curl -f http://localhost/ || exit 1。为了调式失败的探测failing probes,使用stdout和stderr命令输出的内容(只能存储最近的4096byte)都将会存储到健康状态中,这些输出都可以使用docker inspect命令查看到。

2.4.18 SHELL

SHELL指令允许在shell形式中使用的默认shell可以被重写,Linux中默认的Shell为["/bin/sh", "-c"],win中默认的shell为["cmd", "/S", "/C"]。Dockerfile中的SHELL指令必须使用JSON格式。SHELL指令在win上是非常有用的,因为win上有两种常用的shell:cmdpowershell,包含可选的shell:sh

SHELL指令可以出现多次,每个SHELL指令都重写之前的SHELL指令,并影响所有的后续指令,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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中的RUNCMDENTRYPOINT指令中使用shell形式会被SHELL指令影响。下面的示例是win上的通用模式,它可以使用SHELL指令简化,如:

1

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

docker最终调用的命令为:

1

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

但这样效率较低,因为存在一个不必要的命令处理器正在被调用(即shell),而且RUN指令以shell形式给出需要一个额外的powershell -command前缀。为了提高效率,有2种方式:

  1. 直接使用RUN指令的JSON形式:RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""]
  2. 使用SHELL指令,配合使用shell形式,对于win用户是更加是更加自然的语法,特别是结合使用escape指定编译指令时,如:

1
2
3
4
5
6
7

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

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

附:

下面是整章Dockerfile最终的综合示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 示例1
# 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

# 示例2
# 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"]

# 示例3
# 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

转自:https://blog.wgl.wiki/Dockerfile/