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

FROM

所谓定制镜像,那么就一定是以一个镜像为基础,在其上进行修改定制。就像我们之前运行了一个Nginx的容器,在其上面修改一样,基础容器是必需指定的。而FROM就是指定基础镜像,因此在DockerFile中,FROM是必备指定,并且必需是第一条指令!

除了指定现有的基础镜像以外,DockerFile还存在一个特殊的镜像srcatch,这个镜像是一个虚拟的概念,并不实际存在,它表示一个空白的镜像:

FROM scratch
...

如果你以scratch作为基础镜像,意味着你将不使用任何镜像为基础,接下来你所写的指令将作为第一层开始存在。不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见。如swarmcoreos/etcd。对Linux下静态编译的程序来说,并不需要其他操作提供其运行时支持,所需的一切库都在可执行文件里了,因此使用scratch作为基础,可以使镜像的体积更加小巧。

RUN

RUN指令是用来执行命令行命令的,由于命令行的强大功能,RUN指令是定制镜像时最常用的指令之一。其格式有两种:

  • shell格式:就像在命令行中输入的Shell脚本命令一样,比如之前的:
echo '<h1>Hello Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec格式:像是函数调用的格式,例如:
apt-get update
mkdir -p /usr/src/redis

DockerFile的每一个指令都会新构建一层,RUN命令也不例外。每一个RUN行为,都会新建立一层,然后在其上执行命令,执行完毕后,提交这一层的修改,构成新的镜像!

UnionFS是有最大层数限制的,比如AUFS,曾经是最大不能超过42层,现在是最大不能超过127层。所以,对于一些编译、软件的安装、更新等操作,无需分成好几层来操作,这样会使得镜像非常臃肿,拥有非常多的层,不仅仅增加了构建部署的时间,也很容易出错!!例如,上面的exec格式的命令可以写作一层:

RUN buildDeps=apt-get update && mkdir -p /usr/src/redis && apt-get purge -y --auto-remove $buildDeps

这里仅仅使用了一个RUN指令,所以只会新建一层!对于一些编译、安装以及软件的更新等操作,没有必要分为很多层来操作,只需要一层就可以了!在此,我们可以使用&&符号将多个命令分割开,使其先后执行。此时,一个RUN指令有可能会变得非常长,为了使DockerFile的可阅读性和代码更加美观,我们可以使用\进行换行操作。另外,我们还可以使用#进行行首的注释。

观察刚刚编写的RUN指令,我们会发现在指令的结尾处添加了清理工作的命令,删除了为了编译构建的软件,清理了所有下载、展开的文件,并且还清理apt缓存文件。我们之前说过,镜像是多层存储,每一层存储的东西不会在下一层删除,会一直跟随着镜像。因此在镜像构建时,一定要确保每一层只添加真正需要的东西,任何无关的东西都应该被清理掉。

COPY

COPY指令将从上下文目录中的指定路径下的文件或文件夹复制到新的一层的镜像内的指定路径之下,格式为:

COPY <源路径> ... <目标路径>

原路径可以是多个,甚至是通配符,其通配规则只需要满足GO语言的filepath.Math规则即可,如下:

COPY ./test1.py ./test2.py /test/
COPY ./t*.py /test/
COPY ./test?.py /test/

目标路径是容器内的绝对路径,也可以是工作目录下的相对路径,工作目录可以使用WORKDIR指令进行指定。目标路径不需要事先创建,Docker会自动创建所需的文件目录。使用COPY指令会将源路径的文件的所有元数据,比如读、写、指定全选、时间变更等。如果源路径时一个目录,那么会将整个目录复制到容器中,包括文件系统元数据。

ADD

ADD指令和COPY的格式和性质基本一致,只不过是在COPY的基础上增加了一些功能。例如ADD指定中,源路径可以是一个远程URL,Docker引擎会自动帮我们将远程URL的文件下载下来到目标路径下,例如:

ADD http://192.168.0.89:5000/test.py /test/

我们使用docker build进行构建镜像,然后使用docker run创建并启动容器,会发现在根目录下的test文件夹下有了test.py文件。如果源路径是本地的一个tar压缩文件时,ADD指定在复制到目录路径下会自动将其进行解压,如下:

ADD docker2.tar /test/

压缩格式为gzipbzip2以及xz的情况下,ADD指令都会将其解压缩!

非常值得注意的是,目标路径为一个URL时,会将其自动下载到目标路径下,但是其权限被自动设置成了600,如果这并不是你想要的权限,那么你还需要额外增加一层RUN命令进行更改,另外,如果下载的是一个压缩包,同样你还需要额外增加一层RUN命令进行解压缩。所以,在这种情况下,你还不如指定只用一层RUN,使用curl或者wget工具进行下载,并更改权限,然后进行解压缩,最后清理无用文件!

当你的源路径为压缩文件并且不想让Docker引擎将其自动解压缩,这个时候就不可以使用ADD命令,你可以使用COPY命令进行完成!

其实ADD命令并不实用,并不推荐使用!!!

CMD

CMD指令与RUN指令相似,也具有两种格式:

  • shell格式CMD <命令>
  • exec格式CMD [“可执行文件”, “参数1”, “参数2”, …]

之前介绍容器的时候就说过,Docker不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,就需要指定运行的程序及参数。CMD就是指定默认的容器主进程的启动命令的。
在运行时可以设置CMD指令来代替镜像设置中的命令,例如Ubuntu默认的CMD/bin/bash,当我们使用命令docker run -it ubuntu创建并启动一个容器会直接进入bash。我们也可以在运行时指定运行别的命令,比如docker run -it ubuntu cat /etc/os-release,这就用cat /etc/os-release命令代替了默认的/bin/bash命令,输出了系统版本信息。比如,我想在启动容器的时候,在控制台中输出Hello Docker!,我们可以在Dockerfile中这样写,如下:

FROM ubuntu
CMD echo "Hello Docker!"

接下来,我们构建一个镜像ubuntu:v1.0,接下来,我们以此镜像为基础创建并启动一个容器,如下:

docker run -it ubuntu:v1.0

这样,就会在控制台中输出Hello Docker!的信息。

值得注意的是,如果使用shell格式,那么实际的命令会被包装成为sh -c的参数的形式进行执行。上面的CMD指令,在实际执行中会变成:

CMD ["sh", "-c", "echo", "Hello Docker!"]

因为这种特性,一些命令在加上sh -c之后,有可能会发生意想不到的错误,因此在Dockerfile中使用RUN指令时,更加推荐使用exec格式!最后需要牢记,使用docker run命令指定要执行的命令可以覆盖RUN指令,如果我们的docker run中指定了我们将要执行的命令,并且在Dockerfile中也指定了CMD命令,那么最终只会执行docker run命令中指定的命令。比如有这样一个Dockerfile:

FROM ubuntu
CMD ["echo", "Hello Docker!"]

我们将其构建成成镜像ubuntu:v1.1,下面,我们以此镜像为基础创建并启动一个容器,如下:

docker run -it ubuntu:v1.1 cat /etc/os-release

那么容器只会执行cat /etc/os-release命令,也就是说在控制台只会输出系统版本信息,并不会输出Hello Docker!信息

ENTRYPOINT

ENTRYPOINT指令和CMD指令目的一样,都是指定容器运行程序及参数,并且与CMD一样拥有两种格式的写法:

  • shell格式ENTRYPOINT <命令>
  • exec格式ENTRYPOINT [“可执行文件”, “参数1”, “参数2”, …]

CMD指令一样,ENTRYPOINT也更加推荐使用exec格式,ENTRYPOINTdocker run命令中同样也可以进行指定,只不过比CMD指令来的繁琐一些,需要指定--entrypoint参数。同样,在docker run命令中指定了--entrypoint参数的话,会覆盖Dockerfile中ENTRYPOINT上的指令。

当指定了ENTRYPOINT指令时,CMD指令里的命令性质将会发生改变!CMD指令中的内容将会以参数形式传递给ENTRYPOINT指令中的命令,如下:

FROM ubuntu
ENTRYPOINT ["rm", "docker2"]
CMD ["-rf"]

其实,它真正执行的命令将会是:

rm docker2 -rf

从例子中可以看出,ENTRYPOINT指令和CMD指令非常的相似,也很容易将其搞混,就比如上面的例子,就可以完全使用一条CMD指令CMD ["rm", "docker2", "-rf"]来完成。这两个指令到底有什么区别,为什么要同时保留这两条指令呢?

我们可以使用ENTRYPOINT指令和CMD指令相结合,使得在创建并启动时要执行的命令更加灵活!有如下Dockerfile:

FROM ubuntu
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["curl", "-s", "http://ip.cn"]

此时,我们将其构建成镜像ubuntu:v1.2,下面我们创建并启动容器:

docker run -it ubuntu:v1.2

将会在控制台输出我们相应的公网IP信息!此时,如果我们还需要获取HTTP头信息时,我们可以这样:

docker run -it ubuntu:v1.2 -i

此时,将会在控制台中将公网IP信息以及HTTP头信息全部输出!我们知道,docker run命令中紧跟在镜像后面的是CMD指令命令,运行时会替换默认的CMD指令。因为我们在Dockerfile中指定了ENTRYPOINT指令,根据ENTRYPOINT指令的特性知道,当指定了ENTRYPOINT指令,CMD指令的内容将会以参数的形式传递给ENTRYPOINT,所以在容器中最终执行的命令是curl -s -i http://ip.cn-i参数被传递到ENTRYPOINT中,所以最终在控制台中会输出HTTP头信息!!!

ENV

ENV指令用于设置环境变量,格式有两种:

  • ENV
  • ENV = = …

这个指令非常简单,就是用于设置环境变量而已,无论是接下来的指令,还是在容器中运行的程序,都可以使用这里定义的环境变量。例如:

FROM ubuntu:16.04
ENV MODE=test
RUN apt-get update && apt-get install -y curl && curl http://192.168.0.89:5000/$MODE && rm -rf /var/lib/apt/lists/*

如果你要设置多个环境变量,为了美观,你可以使用\来进行换行。多个环境变量的隔开,使用空格进行隔开的,如果某个环境变量的值是由一组英文单词构成,那么你可以将其使用""进行圈起来。如下:

FROM ubuntu:16.04
RUN MODE=test DESCRITPION="ios 12" \
    TITLE="iphone"

接下来,将这个Dockerfile构建成镜像,然后以此镜像为基础创建并启动一个容器,在容器中,我们调用这个环境变量,仍然是有用的!!!

值得注意的是,如果你想通过CMD或者ENTRYPOINT指令的exec格式来打印环境,就像下面这样:

CMD ["echo", $MODE]
CMD ["echo", "$MODE"]

这样都是不能正确输出环境变量的值的,你可以改成exec格式来执行shell命令,如下:

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

如此,就能正确输出环境变量的值了!

ARG

构建参数ARGENV指令一样,都是设置环境变量。与之不同的是,ARG设置的环境变量只是在镜像构建时所设置的,在将来容器运行时是不会存在这些环境变量的。但是不要因此就用ARG来保存密码之类的信息,因为通过docker history还是能够看得到的。ARG指令与ENV指令的使用类似,如下:

FROM ubuntu:16.04
ARG app="python-pip"
RUN apt-get update && apt-get install -y $app && rm -rf /var/lib/apt/lists/*

ARG构建参数可以通过docker run命令中的--build-arg参数来进行覆盖

VOLUME

VOLUME指令用于构建镜像时定义匿名卷,其格式有两种:

  • VOLUME <路径>
  • VOLUME [“<路径1>”, “<路径2>”, …]

之前我们说过,容器存储层应该保持无状态化,容器运行时应尽量保持容器内不发生任何写入操作,对于需要保存动态数据的应用,其数据文件应该将其保存在数据卷中(VOLUME)

定义一个匿名卷:

FROM ubuntu:16.04
VOLUME /data

定义多个匿名卷:

FROM ubuntu:16.04
VOLUME ["/data", "/command"]

这里的/data/command目录在容器运行时会自动挂载为匿名卷,任何向/data/command目录中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化!容器匿名卷目录指定可以通过docker run命令中指定-v参数来进行覆盖

EXPOSE

EXPOSE指令是声明运行时容器服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在Dockerfile中这样声明有两个好处:一个是帮助镜像使用者更好的理解这个镜像服务的守护端口,另一个作用则是在运行时使用随机端口映射时,也就是docker run -p命令时,会自动随机映射EXPOSE端口。

要将EXPOSE和在运行时使用-p <宿主>:<容器端口>区分开来,-p是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而EXPOSE仅仅是声明端口使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR

使用WORKDIR指令来制定工作目录(或者称为当前目录),以后各层操作的当前目录就是为指定的目录,如果该目录不存在,WORKDIR会自动帮你创建目录,如下:

FROM ubuntu:16.04
WORKDIR /data/test
RUN mkdir docker && echo "test" > demo.txt

当我们使用docker build构建此镜像,并使用docker run命令进行创建和启动容器之后,会发现目录被自动切换到了/data/test,并且在当前目录下有一个文件夹docker,在docker下有一个文件domo.txt并且有相应的内容。我们还可以为特定的指令指定不同的工作目录,如下:

FROM ubuntu:16.04
WORKDIR /data/test
RUN mkdir docker
WORKDIR /data/test/docker
RUN echo "test" > demo.txt

这样,Dockerfile中两次RUN指令的操作都在不同的目录下进行,最终容器会切换到最后一次WORKDIR指令下的目录。

WORKDIR指令可以通过docker run命令中的-w参数来进行覆盖

USER

USER指令用于将会用以什么样的用户去运行,例如:

FROM ubuntu:16.04
USER docker

基于该镜像启动的容器会以docker用户的身份来运行,我们可以指定用户名或者UID,组名或者GID,或者两者的结合,如下:

FROM ubuntu:16.04
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group

USER指令可以在docker run命令中的-u参数进行覆盖

HEALTHCHECK

HEALTHECHECK指令是告诉Docker该如何判断容器的状态是否正常,这是1.12引入的新指令,其格式有两种:

  • HEALTHCHECK [options] CMD <命令>:检查容器健康状态的命令
  • HEALTHCHECK NONE:如果基础镜像有健康检查指令,这一行将会屏蔽掉其健康检查指令

HEALTHECHECK支持下列选项:

  • –interval=<间隔>:两次检查的时间间隔,默认为30s
  • –timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查将会判定为失败,默认为30s
  • –retries=<次数>:当连续失败指定次数之后,则将容器状态视为unhealthy,默认为3次

在没有HEALTHCHECK指令之前,Docker引擎只可以通过容器内主进程是否退出来判断容器状态是否异常。很多情况下这没有问题,但是如果程序进入了死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法继续提供服务了。在1.12之前,Docker引擎不会检测到容器的这种状态,从而不会重新调度,导致可能容器已经无法提供服务了却仍然还在接收用户的请求。

假设我们有个镜像是最简单的Web服务,我们希望增加健康检查来判断Web服务是否在正常工作,我们可以用curl来帮助判断,其DockerfileHEALTHCHECK可以这么写:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s CMD curl -fs http://localhost/ || exit 1

接下来,我们将该Dockerfile编译构建成一个镜像,并以此镜像为基础创建并启动一个容器。此时,我们使用docker container ls命令来查看容器的状态,如下:

root@ubuntu:~/docker# docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                   PORTS                NAMES
036b91eea00d        nginx:v1.2          "nginx -g 'daemon of…"   7 seconds ago       Up 6 seconds (healthy)   0.0.0.0:80->80/tcp   web

我们再STATUS这一列中可以看到,状态未healthy。如果我们快速的多次执行docker container ls的话,会发现STATUS状态是由health: starting最后变为healthy,当然如果容器未在正常工作,最后的状态将会变为unhealthy

这里,我们设置了每5s检查一次,如果检查时间超过3s没有响应就视为失败。||符号左边的命令执行结果为假,右边的命令才会执行!

为了帮助排除故障,健康检查命令的输出会被存储于健康状态里,我们可以使用docker inspect命令来进行查看:

root@ubuntu:~/docker# docker inspect --format '{{json .State.Health}}' web | python3 -m json.tool
{
    "Status": "healthy",
    "FailingStreak": 0,
    "Log": [
        {
            "Start": "2018-07-17T21:15:05.900643297+08:00",
            "End": "2018-07-17T21:15:05.968989028+08:00",
            "ExitCode": 0,
            "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n"
        }
    ]
}

CMDNETRYPOINT一样,HEALTHCHECK指令只可以出现一次,如果有多个HEALTHCHECK指令,那么只有最后一个才会生效!!!

ONBUILD

ONBUILD是一个特殊的指令,它后面跟着的是其他指令,比如COPYRUN等,而这些命令在当前镜像被构建时,并不会被执行。只有以当前镜像为基础镜像去构建下一级镜像时,才会被执行。格式为:ONBUILD <其他指令>

Dockerfile中的其他指令都是为了构建当前镜像准备的,只有ONBUILD指令是为了帮助别人定制而准备的。例如:

from ubuntu:16.04
WORKDIR /data
ONBUILD RUN mkdir test

此时,我们以此Dockerfile进行构建镜像ubuntu:test,并以此镜像为基础创建并启动一个容器,进入容器后,容器会自动切换到WORKDIR指令下的目录,此时我们使用ls命令会发现在工作目录下,并未创建test文件夹,如下:

root@ubuntu:~/docker# docker run -it ubuntu:test
root@3a8f912fd23b:/data# ls
root@3a8f912fd23b:/data#

此时,我们再创建一个Dockerfile,只需一个FROM指令即可,使其继承刚刚我们构建的ubuntu:test镜像,如下:

FROM ubuntu:test

我们再以此Dockerfile构建镜像ubuntu:test_onbuild,并以此镜像为基础创建并启动一个容器,进入容器后,容器会自动切换到WORKDIR指令下的目录,此时我们使用ls命令会发现在工作目录下,已经创建好了一个名为test的文件夹,如下:

root@ubuntu:~/docker# docker run -it ubuntu:test_onbuild
root@5394e605b6ea:/data# ls
test

LABEL

LABEL指令可以为镜像指定标签,其格式为:LABEL <key1>=<value1> <key2>=<value2> ...

LABEL后面是键值对,多个键值对以空格进行隔开,如果value中包含空格,请使用""将value进行圈起来,如下:

FROM ubuntu:16.04
LABEL name=test
LABEL description="a container is used to test"

我们知道,DockerFile的每一个指令都会新构建一层,所以,上面的LABEL我们可以写成一条指令,用空格进行隔开,如下:

FROM ubuntu:16.04
LABEL name=test description="a container is used to test"

为了美观,我们还可以使用\符号进行换行操作。

要查看镜像的标签,我们可以使用docker inspect命令,如下:

root@ubuntu:~# docker inspect --format '{{json .Config.Labels}}' test | python3 -m json.tool 
{
    "description": "a container is used to test",
    "name": "test"
}

其中“test”为容器名称!

值得注意的是,这里的标签并非是我们一开始将镜像名称中的<仓库>:<标签>,这两者是不一样的!这里标签,类似于签条,注解之类的意思

MAINTAINER

MAINTAINER指令用于指定生成镜像的作者名称,其格式为:MAINTAINER <name>

MAINTAINER指令已经被弃用,可以使用LABEL指令进行替代,如下:

LABEL maintainer='Stephen Chow'

MAINTAINER指令在一些老的Dockerfile中仍然可以看到,所以还是需要了解一下的!

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐