Dockerfile指令详解

1. FROM

FROM [--platform=<platform>] <image> [AS <name>]

Or

FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]

Or

FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]

Form指令,会开始一个新的build阶段,并且为接下来的指令设置基础镜像.因此,一个有效的Dockerfile必须以FROM作为指令的开头.任何有效的镜像都可以当做基础镜像,而且还可以很轻松的从公共仓库拉取公共镜像.

在Dockerfile中,ARG指令是唯一一个可以出现在FROM指令之前的指令.

在一个Dockerfile中,FROM指令可以多次出现,用来构建多个镜像,或者将一个build阶段产生的镜像当做另一次build的依赖.

在每个新的FROM指令之间,会将前面的内容进行提交,并将上一个阶段最后的镜像ID输出.
每个FROM指令,执行时会清除之前指令的执行信息.

为了方便记忆,还可以使用AS name为新构建的镜像起一个名字.
name可以,被用于接下来的FROMCOPY --from=指令引用.

tagdigest值也是可选的,如果不使用的话builder会自动为我们的新镜像分配一个latest作为默认tag.

--platform可以用于指定镜像的来源的平台,以防FROM指令引用了一个多平台的镜像,比如linux/amd64, linux/arm64, or windows/amd64等平台.

默认的,执行build命令的目标平台会被当做默认平台.

–platform 的值还可以使用全局参数,比如--platform=$BUILDPLATFORM.

1.1 ARG和FROM

FROM指令支持任何出现在第一个FROM指令之前,被ARG指令声明的变量.
因此CODE_VERSION对于两次FROM都有效.

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

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

在FROM指令之前使用ARG指令声明的变量,处于build阶段之外,因此,这个变量也可以被FROM后的任何指令所使用.

但在build阶段的指令中,如果想要使用在第一个FROM之前声明的ARG变量的值,需要再次使用ARG指令+变量名,有种在build阶段引入此变量的含义.
比如下面第三行,处于build阶段,因此如果不使用
ARG VERSION将其引入build阶段,则在
RUN echo $VERSION > image_version,该变量是unset的空值.

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

2. RUN

RUN指令有2种形式:

1)shell形式:
命令在shell中执行,默认的,如果是在Linux则相当于/bin/sh -c的方式执行命令,而如果是在Windows系统中,则相当于cmd /S /C

RUN <command>

shell形式的默认shell可以通过SEHLL命令来修改.
在shell形式中还可以使用\来当做续行符.比如下面的2个例子是等价的:

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

2)exec形式:

RUN ["executable", "param1", "param2"] (exec form)

RUN指令会在当前镜像的基础之上,创建一个新的镜像层来执行后面所传递给它的命令,并且提交执行结果,而这个被提交的镜像层会被用来当做Dockerfile中下一个指令的基础镜像.

分层的RUN指令及生成新层级的提交机制,构成了Docker的主要理念,它使得镜像的创建可以非常廉价,而且可以基于镜像历史中的任何一个点创建容器,就像管理source一样.比如Git,我们可以随时切换到任何分支进行操作.

exec 形式,可以避免一些问题:

  1. it possible to avoid shell string munging
    不知道如何能翻译的比较好.举例来说,如果一个脚本test.sh不能自己执行,必须要/bin/sh -c test.sh 的方式来执行,那么,如果使用RUN的shell形式,最后得到的命令相当于:
/bin/sh -c "/bin/sh -c 'test.sh'"

相当于使用了2次/bin/sh,而此时sh进程的id号不是1,因此基于此镜像启动的容器会没有办法接收SIGINAL.

  1. to RUN commands using a base image that does not contain the specified shell executable.
    如果镜像的环境中压根没有/bin/sh,或cmd /S /C,那么使用exec的形式也可以很好的解决.
    比如:只是随便写的,举个例子,csh语法完全不会,这样就可以以cshell来执行命令了.
RUN ["/bin/csh", "-c", "echo hello"]

Note: The exec form is parsed as a JSON array, which means that you must use double-quotes (“) around words not single-quotes (‘).
但是,需要特别注意的是,exec形式,是被当做JSON数组来解析的,所以所有的内容都要用双引号包含起来,重要的事情说三遍.
双引号 双引号 双引号

RUN指令的缓存不会被自动清除,也就是说在下一次build中也会使用缓存,可以使用 --no-cache参数来显示的禁止缓存.
ADD指令也可以是RUN指令的缓存失效.

3. CMD

CMD指令有3种形式:

1)exec形式:(也是推荐使用的形式)

CMD ["executable","param1","param2"]

2)参数形式:

CMD ["param1","param2"]

param1param2被当做ENTRYPOINT指令的默认参数.

3)shell形式:

CMD command param1 param2

在一个Dockerfile中,只有一个CMD指令能够生效,如果有多个CMD存在,那么只有最后一个CMD指令会生效.

CMD指令的目的是为一个运行中的容器提供一个默认的命令.它可以是一个可执行的命令比如脚本文件a.sh,又或者是不可执行的内容a.txt,但后者必须再指定一个ENTRYPOINT指令.而a.txt将会作为ENTRYPOINT指令的参数.

与RUN命令不同,CMD命令在容器运行阶段才会被执行,而不是镜像编译阶段.

4. LABEL

MAINTAINER指令的替代者,因为LABEL指令更加丰富.

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

LABEL指令,它以键值对的形式,向镜像添加元信息.
如果想要在LABEL的value中使用空格,那么需要使用双引号将内容包含起来:

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

An image can have more than one label. You can specify multiple labels on a single line. Prior to Docker 1.10, this decreased the size of the final image, but this is no longer the case. You may still choose to specify multiple labels in a single instruction, in one of the following two ways:

一个镜像可以有多个label,也可以在一行指定多个label,在1.10版本之前,这样可以精简最终编译成镜像的大小,但是现在不会了.

不过你仍然可以将多个label写在一行,下面两种方式都可以:

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

在基础镜像或者父层镜像中存在的label会被新镜像继承,如果同一个label被多次赋值,那么最后一次会覆盖之前的值.

可以使用下面的命令来查看镜像的label信息

sudo docker inspect CONTAINER_ID | CONTAINER_NAME

5. EXPOSE

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

EXPOSE 指令会通知Docker,容器在 运行时将会监听指定的端口,端口库可以指定TCP或者是UDP协议,默认的是TCP协议.

EXPOSE不是真正意义上的暴露端口.它的作用更像是处于build镜像的人和运行容器的人之间的一个文档,它记载了打算要暴露的端口.
真正暴露端口的一方是启动容器的一方,使用-p local_port:container_port选项将容器内的端口与宿主机的端口做一个映射,或者使用-P选项来暴露所有端口,并将它们暴露给更高阶的端口.

默认以TCP协议暴露端口,当然也可以指定UDP协议的方式,甚至可以同时以两种协议的方式

EXPOSE 80/tcp
EXPOSE 80/udp

6. ENV

ENV指令有如下2种方式

方式1:
ENV <key> <value>
方式2:
ENV <key>=<value> ...

ENV指令可以设置环境变量,的值赋值为,这个值可以在build阶段后续的指令中使用,也可以被替换为其他值.

方式1:
为一个变量赋一个value.在第一个空格,key与value之间的空格之后的所有字符串都被当做value,包括空白字符.
如果在value中的引号没有被转义符\转义,那么在解释器进行解释过程中会被移除.

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

方式2:

一次性可以设置多个变量,这种语法使用=连接key和value,而且可以使用\或者引号来包含空白字符.

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

使用ENV指令设置的变量在基于此镜像运行的容器中也会存在,可以通过下面的命令查看,

sudo docker inspect CONTAINER_ID | CONTAINER_NAME

也可以使用下面的命令来替换变量值

sudo docker run --env <key>=<value>

7. ADD

ADD 有两种形式:

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

[--chown=:]选项只限于Linux环境,在Windows无效.

ADD指令从将新的文件,目录或远程URL所指向的文件,添加到镜像文件系统中的.路径.

可以指定多个,但是如果如果他们都是文件或者目录,那么他们的路径将被视作是基于context的.

每个都可以使用通配符,例如:

ADD hom* /mydir/        # adds all files starting with "hom"
ADD hom?.txt /mydir/    # ? is replaced with any single character, e.g., "home.txt"

是目标容器内的一个绝对路径,或者是基于WORKDIR指令的相对路径.

ADD test relativeDir/          # adds "test" to `WORKDIR`/relativeDir/
ADD test /absoluteDir/         # adds "test" to /absoluteDir/

当添加含有特殊字符的文件或目录时,比如([ 和 ]),你需要遵照Go语言的规则,以免他们会被当做匹配模式,例如要添加的文件名是 arr[0].txt,要用下面的方式:

ADD arr[[]0].txt /mydir/    # copy a file named "arr[0].txt" to /mydir/

使用ADD指令,所有的新文件被创建时UID和GID都是0,除非使用--chown选项特别指定.
可以使用UID/GID,也可以使用username/groupname.

如果只提供了username或UID没有提供groupname和GID,那么会使用跟owner同样的UID值作为GID.
当提供的是username或者是groupname时,容器的根文件系统中的/etc/passwd/etc/group来获取响应的UID或GID.
如果目标的根文件系统没有/etc/passwd/etc/group,但是却在–chown时使用了username或groupname,那么build将会报错.
使用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/

是远程URL的文件,那么目标文件系统中此文件的权限为600.

注意事项:

1)当你通过STDIN传递一个Dockerfile进行build时候,没有context,因此,在这种情况下,ADD指令只能用URL来当做.

docker build - < somefile

也可以通过STDIN方式来传递一个压缩文件,Dockerfile在此压缩文件的根路径,这时,压缩文件内的目录可以被当做context

docker build - < archive.tar.gz

2)如果URL文件是需要授权的,那么需要使用

RUN wget

等等的工具将文件下载到容器内,因为ADD指令不支持授权.

3)目录本身不会被复制,ADD仅复制目录中的内容.

ADD 需要遵守下列的准则:

  1. 必须是在context环境中的,不能访问context之外的内容.
  2. 如果是一个URL,并且不以斜线/结尾,那么文件会被下载并拷贝到
  3. 如果是一个URL,并且以斜线/结尾,那么下载下来的文件名会从URL来选取,而文件也会被下载到/,举例说明:
ADD http://example.com/foobar /

会创建一个/foobar文件. 为了能被推断出一个合适的文件名,这种方式URL一定不能过于复杂,http://example.com会导致无法正常build.

  1. 如果是一个目录,那么整个目录中的内容,包括文件系统的元数据都会被拷贝.

  2. 如果是一个本地的tar包,被识别为压缩格式,诸如(identity, gzip, bzip2 or xz),那么将会被自动展开为一个目录.
    而如果是URL上的一个tar包,则不会被自动解压.
    另外要注意的是,识别的依据是文件内容,而不是文件名,如果有一个空文件恰巧是以.tar.gz结尾,那么它不会被视为压缩文件,也不会被解压缩.

  3. 如果目录并不存在,那么将会自动创建(递归创建)所有需要的目录.

8. COPY

COPY 有两种形式:

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

用于对付有空白字符的情况.

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

功能和语法类似ADD,也是从将文件拷贝到,与ADD不同的是COPY不能从URL添加文件,也不会解压本地压缩文件.

注意事项:
如果使用STDIN的方式来build的话,是没有context的,所以COPY无法使用.

COPY可以使用--from=选项,指向前面的build阶段中的镜像名称或index,来替换用户所发送来的context

9. ENTRYPOINT

类似CMD,ENTRYPOINT也有两种方式:
1)exec方式

ENTRYPOINT ["executable", "param1", "param2"]

CMD中的内容,会被当做参数传递给ENTRYPOINT,当做默认的参数(这里是-c).

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

而这个默认的参数-c,在docker run时,是可以被覆盖的.
下面的命令,-H将会替代-c,当做参数传递给exec形式的ENTRYPOINT,还可以通过–entrypoint选项来覆盖ENTRYPOINT指令.

# 将上面的Dockerfile构建成名为top的镜像
sudo docker build -t top .
sudo 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

使用下面的命令,可以检验一些执行结果,发现,CMD所提供的默认参数,确实被替换了.

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

此时,我们可以优雅地使用下面的命令来停止该容器.

sudo docker stop test

如果你需要为一个可执行文件写一个启动脚本,你可以使用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 "$@"

gosu命令的作用类似sudo,但是在容器中使用sudo会有安全隐患,所一般在docker中都是使用gosu来提升权限的.
而且不同的是,gosu命令执行的命令PID是1(exec也是),因此可以接收Unix的信号.

2)shell方式

ENTRYPOINT command param1 param2

You can specify a plain string for the ENTRYPOINT and it will execute in /bin/sh -c. This form will use shell processing to substitute shell environment variables, and will ignore any CMD or docker run command line arguments. To ensure that docker stop will signal any long running ENTRYPOINT executable correctly, you need to remember to start it with exec:

你可以为ENTRYPOINT指定一个单纯字符串,他将会被执行于/bin/sh -c中.
但是这么做,会让它忽略CMD指令或docker run 命令行中提供的参数,因此,为了确保docker stop可以让容器内正确的接收并处理信号,我们要记得在ENTRYPOINT指令中使用exec.

FROM ubuntu
ENTRYPOINT exec top -b

当你运行上面build后的镜像,你可以看到,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

当我们停止容器时,也会很干净的退出.

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

但是如果你忘记了使用exec

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的PID并不是1.

$ 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

这时,如果你使用docker stop去停止容器,容器不会干净的退出,因为它会在超时后,强制发送SIGKILL信号来退出.

$ /usr/bin/time docker stop test

看realtime的时间超过了10秒.

test
real	0m 10.19s
user	0m 0.04s
sys	0m 0.03s

9.1 CMD 和 ENTRYPOINT的交互

CMD 和 ENTRYPOINT指令都可以用来定义一个在容器启动成功以后,会自动运行的命令.下面是关于二者之间协作的一些规则:

  1. Dockerfile 应该指明至少一个CMD 或 ENTRYPOINT 指令.(一般来讲是这样的,毕竟如果每次启动容器后都必须要进去手动执行一些命令和脚本,那实在是太不知能了.)
  2. ENTRYPOINT 当一个容器被用作可执行容器时(成功启动容器后,它可以自己运行一些命令或脚本),应当使用ENTRYPOINT .
  3. CMD 应该被用于,定义并为ENTRYPOINT 命令提供默认的参数,或在容器内执行某些特殊的命令.
  4. CMD 命令会被启动容器时所指定的参数或命令覆盖掉.

The table below shows what command is executed for different ENTRYPOINT / CMD combinations:
下面的表格展示了,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是被定义在基础镜像中的,那么在Dockerfile中使用ENTRYPOINT 会重置CMD为空值,因此如果想要CMD有作用,必须被定义在当前镜像中.

10. VOLUME

VOLUME 指令用来根据指定的name来在容器内创建一个挂载点,并将它标记为是与外部的宿主机或容器中的卷具有挂载关系的.

它的值可以是JSON数组, VOLUME ["/var/log/"],或者是一个有多个参数的字符串,例如:

VOLUME /var/log
or 
VOLUME /var/log /var/db
VOLUME ["/data"]

更多有关通过Docker客户端进的挂载命令、信息和样例请参见:

https://docs.docker.com/storage/volumes/

关于在Dockerfile指定volume,需要记住下面的几点:

  1. 基于Windoes的容器:容器内的目标volume必须满足下列条件之一:
    1)不存在或者是空的目录
    2)C盘以外的盘符

  2. 在Dockerfile中改变volume:
    如果在volume已经被声明之后,任何一个build阶段,想要对它做出的改变都会被无视.

  3. JSON格式:
    必须使用双引号.

  4. 宿主机的目录是在容器运行时被声明的:
    宿主机的目录是依赖于主机的,这么做是为了维持镜像的可移植性,因为在容器内指明的一个目录不能确保在所有的宿主机都能有效.
    也因此,宿主机的映射目录是不可以在Dockerfile中指定的. VOLUME 指令也没有能指定宿主机目录的参数.
    所以,你必须在创建或者启动容器的时候指定宿主机上与容器内进行映射的目录.

11. USER

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

USER 指令用于设定:
运行镜像(基础镜像)和Dockerfile中接下来的RUN,CMD,和ENTRYPOINT 指令所使用的用户(和组)的信息.
Warning:
当用户没有组时,会被使用root组运行.

在Window中,如果用户不是内置用户的话,则必须首先被创建.
在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 /path/to/workdir

WORKDIR 指令为Dockerfile中,在其后面的RUN, CMD, ENTRYPOINT, COPY 以及ADD等指令设置工作目录.
如果WORKDIR 所指定的目录不存在,即使在随后没有任何指令了,它也会被创建.

在一个Dockerfile中WORKDIR 指令可以多次出现,如果被提供了一个相对路径,那么它将是基于它前一个WORKDIR指令所设置的目录.例如下面这样

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

pwd命令输出的结果会是 /a/b/c

WORKDIR 指令可以解析,在其之前被声明的环境变量,并且,只有在Dockerfile中被明确声明了的环境变量可以使用.例如:

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

pwd命令的输出结果将会是/path/$DIRNAME

13. ARG

ARG <name>[=<default value>]

ARG 指令定义了一个允许用户在build命令构建镜像时使用 --build-arg =参数,来提供给builder的变量.如果用户使用了 --build-arg =参数,但该变量并未在Dockerfile中被定义,那么会输出警告.

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

在一个Dockerfile中 ARG 指令可以多次出现.

FROM busybox
ARG user1
ARG buildno
...

Warning:
不推荐大家在使用--build-arg =参数时,传递隐私文件,比如github的秘钥,用户证书等等.
因为任何用户都可以使用docker history命令来查看这些信息.

ARG的默认值:
ARG 也可以赋一个默认值给变量.

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

如果ARG指令赋予了一个变量默认值,那么在build阶段没有显示的使用--build-arg =参数来传递值时,会使用默认值.
ARG的作用域:

ARG变量的生效时期是在它被声明时,而不是在被命令中使用时.

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

使用下面的命令传入user参数,开始构建镜像

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

第2行的USER指令中,user变量还未被声明,所以它被分配的值是some_user.
而由于在第3行已经声明了user变量,所以第4行,会得到what_user,因此可以得出,在变量被声明之前,任何对它的使用都是空.

ARG 指令的作用域止于它所被声明的build阶段的结束.如果是在一个有多个build阶段的Dockerfile中,那么美一个阶段都必须要一个ARG指令才可以使用它所声明的变量.

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

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

ARG指令的使用:

ARG或者ENV 指令都可以用来声明,可用于RUN指令的变量.
而用ENV指令声明的环境变量会覆盖ARG指令所声明的同名变量,如下:

1 FROM ubuntu
2 ARG CONT_IMG_VER
3 ENV CONT_IMG_VER v1.0.0
4 RUN echo $CONT_IMG_VER

如果使用下面的命令来build镜像

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

在这个例子中,RUN 会使用v1.0.0而不是ARG所传递的v2.0.1,这个行为有点类似shell脚本,当我们在执行shell脚本时,本地变量总是会覆盖掉环境变量所设置的相同内容.

Using the example above but a different ENV specification you can create more useful interactions between ARG and ENV instructions:

还是上面的例子,这次我们让通过使用不同的方式来声明ENV,用ARG和ENV指令结合来做一些更有意义的事.

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
$ docker build .

与ARG指令不同,ENV的值在整个build阶段都是存在的.因此这种方式可以在没有给出–docker-arg时候起到赋予变量默认值的作用.

预先声明好的ARGs:

Docker为我们提前声明了一些,可以让我们直接在Dockerfile使用的ARG变量

HTTP_PROXY
http_proxy
HTTPS_PROXY
https_proxy
FTP_PROXY
ftp_proxy
NO_PROXY
no_proxy

只需要在build镜像时,传入对应的参数名和值就可以了

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

默认的,这些Docker为我们提前声明好的变量不会将信息输出到docker history.这也降低了以外泄露敏感信息的风险.

比如使用 --build-arg HTTP_PROXY=http://user:[email protected]来build下面的Dockerfile.

FROM ubuntu
RUN echo "Hello World"

在这个例子中,HTTP_PROXY信息是不会出现在docker historycache中的.
如果这时你想要改变你的代理服务器地址为http://user:[email protected],那么接下来的build也不会导致缓存未命中.(因为HTTP_PROXY不会被缓存,而缓存中的内容都没变)

而如果是在Dockerfile中声明了HTTP_PROXY,那么它的信息就会被输出到docker history,而且如果这时想要改变代理服务器地址为http://user:[email protected],那么缓存将失效.(因为缓存中的一部分信息HTTP_PROXY发生了变化,所谓导致缓存无法命中.)

FROM ubuntu
ARG HTTP_PROXY
RUN echo "Hello World"

14. ONBUILD

ONBUILD [INSTRUCTION]

ONBUILD 指令给镜像添加一个触发器,这个触发器不会立即执行,而是在这个镜像被另一次build当做基础镜像时才会执行.

任何build指令都可以被注册为触发器.

当你正在构建一个今后会用来当做基础镜像的镜像时,ONBUILD非常有帮助,例如,它可以使一个应用程序的构建环境变量或者daemon根据不同用户的要求而进行配置.

比如你的镜像是一个可以复用的的Python程序的builder,它需要你将应用程序的源码添加到指定目录,然后
它可能还需要一个build脚本.

你不能仅仅通过简单的ADD和RUN命令来实现,因为你还没有要用来build的应用程序源码,而且将来的每个应用程序都可能会不同.

当然,你可以仅仅提供给开发者一份Dockerfile样本,但是这种做法即不是很高效,又很容易出错,而且还很难更新,因为它是针对某个应用程序而特定的.

针对上面的情况,可以使用ONBUILD来提前注册一个可以在再下一次build阶段运行的指令.

下面介绍一下它是如何工作的:

  1. 当builder遇到ONBUILD指令时,它会向镜像的元数据中添加一个触发器,而这个触发器的指令不会对当前的build产生影响.

  2. 在build的结尾,所有的触发器都会以OnBuildkey被储存在镜像的清单里.可以使用docker inspect指令查看.

  3. 这个镜像可以被FROM指令当做基础镜像使用.在FROM指令被执行时,触发器就会按照它们注册时候的顺序被执行.如果有任何一个触发器执行失败,那么FROM指令就会被停止,宣告build失败.如果所有触发器都成功了,那么FROM指令才算成功,之后的内容会被正常继续.

  4. 触发器不会被继承,所以镜像A中的触发器,尽在被用作B的基础镜像时有效,而在B镜像build成功后,触发器被清除,B中并不会有触发器存留,就是说如果B在被当做其他镜像的基础镜像时,没有触发器.

例如,你可以向下面这样使用ONBUILD:

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

Warning:
在ONBUILD 指令中使用 ONBUILD是不被允许的.

ONBUILD 指令也不可以当做FROM 或MAINTAINER 指令的触发器.

15. STOPSIGNAL

STOPSIGNAL signal

这个信号可以是一个内核系统调用表中为被分配的数字,比如9,或者是一个以SIGNAME格式命名的信号名称,比如SIGKILL.

16. HEALTHCHECK

HEALTHCHECK 指令有2中形式:

  1. 通过在容器内运行一个命令来检查容器健康
HEALTHCHECK [OPTIONS] CMD command 
  1. 禁用所有由基础镜像继承的健康检查
HEALTHCHECK NONE

HEALTHCHECK 指令告诉Docker,在它仍旧处于运行状态时,如何进行检测.
它可以检测例如:一个运行状态的web服务器是否卡在无限循环中无法处理新连接.

当一个容器被指定了healthcheck,它会在它的正常状态职位有一个健康状态.
这个状态最开始是starting,一旦healthcheck通过,它就会变为healthy状态.而在一定数量的连续失败后,它变为unhealthy状态.

在CMD指令前可以出现的healthcheck选项有:

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

在一个Dockerfile中,值能有一个有效的HEALTHCHECK,如果你添加了多个,那么只有最后一个HEALTHCHECK会生效.

命令的退出状态码表明了容器的健康状态:

0: success - the container is healthy and ready for use
1: unhealthy - the container is not working correctly
2: reserved - do not use this exit code

比如,如果要每隔5分钟检查一次web服务器是否可以在3秒钟内提供网站的主页信息:

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

17. SHELL

SHELL ["executable", "parameters"]

SHELL 指令可以用来替换使用shell形式所执行的命令的默认shell.在Linux上的默认shell为["/bin/sh", "-c"],而在Windows上是["cmd", "/S", "/C"].SHELL指令在Dockerfile中必须写成JSON形式.

SHELL 指令在Windows特别有用,因为在Window是系统中,有两种截然不同的shell类型,cmdpowershell,当然还包括sh.

在一个Dockerfile中,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

当RUN,CMD和ENTRYPOINT指令以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"

这种执行方式是很低效的,原因有2个:

  1. 有一个毫无必要的cmd.exe进程被调用.(在cmd中调用powershell,比直接调powershell多了一个cmd进程)
  2. 每次都多了一个powershell -command的前缀,写起来很啰嗦.

有两种方式可以是它变得更加高效:

方式1:使用RUN指令的JSON方式

JSON格式虽然它需要一些冗长的双引号啊,转义符什么的,并不会调用不必要的cmd.exe命令.

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

方式2:SHELL指令:

另一种方式就是使用SHELL指令了,使用shell格式来执行更加自然的Windows语法,尤其是当你使用
escape parser directive时.

通过使用 # escape=`,将转义符由默认的\变为`,这样在Windows中,路径的书写就要轻松很多.

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

你可能感兴趣的:(docker,docker)