Docker-DockerFile指令详解
我们已经介绍了 FROM , RUN ,还提及了 COPY , ADD ,其实 Dockerfile 功能很强大,它提供了十多个指令。下面我们继续讲解其他的指令FROM所谓定制镜像,那么就一定是以一个镜像为基础,在其上进行修改定制。就像我们之前运行了一个Nginx的容器,在其上面修改一样,基础容器是必需指定的。而FROM就是指定基础镜像,因此在DockerFile中,FROM是必备...
我们已经介绍了 FROM , RUN ,还提及了 COPY , ADD ,其实 Dockerfile 功能很强大,它提
供了十多个指令。下面我们继续讲解其他的指令
FROM
所谓定制镜像,那么就一定是以一个镜像为基础,在其上进行修改定制。就像我们之前运行了一个Nginx的容器,在其上面修改一样,基础容器是必需指定的。而FROM
就是指定基础镜像,因此在DockerFile中,FROM
是必备指定,并且必需是第一条指令!
除了指定现有的基础镜像以外,DockerFile还存在一个特殊的镜像srcatch
,这个镜像是一个虚拟的概念,并不实际存在,它表示一个空白的镜像:
FROM scratch
...
如果你以scratch
作为基础镜像,意味着你将不使用任何镜像为基础,接下来你所写的指令将作为第一层开始存在。不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见。如swarm
、coreos/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/
压缩格式为gzip
、bzip2
以及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
格式,ENTRYPOINT
在docker 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
构建参数ARG
和ENV
指令一样,都是设置环境变量。与之不同的是,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
来帮助判断,其Dockerfile
的HEALTHCHECK
可以这么写:
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"
}
]
}
和CMD
、NETRYPOINT
一样,HEALTHCHECK
指令只可以出现一次,如果有多个HEALTHCHECK
指令,那么只有最后一个才会生效!!!
ONBUILD
ONBUILD
是一个特殊的指令,它后面跟着的是其他指令,比如COPY
、RUN
等,而这些命令在当前镜像被构建时,并不会被执行。只有以当前镜像为基础镜像去构建下一级镜像时,才会被执行。格式为: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中仍然可以看到,所以还是需要了解一下的!
更多推荐
所有评论(0)