CMD指令
CMD 指定默认的容器主进程启动命令
格式:CMD <command>
(shell格式)或 CMD [“executable”,”param1”,”param2”]
(exec格式,推荐格式)或 CMD[”param1”,”param2”]
。(为ENTRYPOINT指令提供参数)
CMD指令提供容器启动时运行的默认命令,例如ubuntu镜像默认的CMD是/bin/bash
,因此我们可以直接使用 docker run -it ubuntu
进入bash。
同时也可以使用docker run -it ubuntu cat /etc/os-release
,执行该命令后会输出系统版本信息。因为当在执行docker run命令时,如果显示地指定了容器的启动命令,那么会将Dockerfile中CMD设置的默认启动命令覆盖,也就是说:cat /etc/os-release
命令会替代成为容器的启动命令,所以输出了系统版本信息。
在指令格式上,一般推荐使用exec格式,因为使用 shell格式时,实际的命令会被包装为 sh -c
的参数的形式进行执行。比如:CMD echo $HOME
,在实际执行中,会将其变更为:CMD [ "sh", "-c", "echo $HOME" ]
。
ENTRYPOINT指令
ENTRYPOINT 指定默认的容器主进程启动命令
格式:ENTRYPOINT <command>
(shell格式)或ENTRYPOINT [“executable”,”param1”,”param2”]
。(exec格式,推荐格式)
ENTRYPOINT和CMD一样,都可以指定容器默认的启动命令,但是它又和CMD有所不同。上面我们说过,用户在执行docker run
命令创建并启动容器时,如果指定了启动命令,那么“该启动命令”会覆盖CMD指令设置的默认启动命令,但是ENTRYPOINT设置的启动命令该不能被覆盖。
细心的同学可能发现了CMD命令可以为ENTRYPOINT指令提供参数。实际上,如果使用Dockerfile构建镜像时,既使用了ENTRYPOINT指令,又指定了CMD指令,那么CMD指令的含义就发生了改变, CMD 的内容将作为参数传给 ENTRYPOINT指令,换句话说实际执行时,变成了<ENTRYPOINT> <CMD>
。同时,如果执行docker run基于该镜像创建并启动容器,并设置了启动命令时,docker run设置的“启动命令”依然会覆盖CMD的内容,但也仅仅是作为ENTRYPOINT指令的参数。
实例
假设需要一个得知使用者当前公网IP的镜像,可以使用下面的Dockerfile构建一个镜像。
FROM centos
RUN yum install curl
CMD ["curl","-s","http://ip.cn"]
执行docker build -t myip
.来构建一个名为myip的镜像,镜像构建完成后,如果想要查询当前公网的IP,执行docker run myip
,如下所示:
[root@localhost tempdir]# docker run myip
当前 IP:XXXXXXXX 来自:XXXXXXXXXXXX
嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是curl
,那么如果我们希望显示HTTP头信息,就需要加上-i
参数。那么我们可以直接加 -i
参数给 docker run myip
么?
[root@localhost tempdir]# docker run myip -i
container_linux.go:247: starting container process caused "exec: \"-i\": executable file not found in $PATH"
docker: Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "exec: \"-i\": executable file not found in $PATH"
我们可以看到执行明后后输出了 executable file not found
的错误信息,也就是“可执行文件找不到”。之前我们说过,跟在镜像名后面的是command,运行时会替换 CMD的默认值。因此这里的-i
替换了原来的CMD,而不是添加在原来的 curl -s http://ip.cn
后面。而 -i
根本不是命令,所以自然找不到。
那么如果我们希望加入-i
这参数,我们就必须重新完整的输入这个命令:docker run myip curl -s http://ip.cn –i
。这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在改写Dockerfile,使用ENTRYPOINT设置启动命令:
FROM centos
RUN yum install curl
ENTRYPOINT ["curl","-s","http://ip.cn"]
这次我们再来尝试直接使用 docker run myip
以及 docker run myip -i
:可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后,docker run
命令ENTRYPOINT不会被覆盖。它会作为参数传给ENTRYPOINT,从而达到了我们预期的结果。
[root@localhost tempdir]# docker run myip
当前 IP:XXXXXXXXXXXX 来自:XXXXXXXXXXXXXX
[root@localhost tempdir]# docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.11.9
Date: Wed, 09 Aug 2017 08:32:24 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
当前 IP:XXXXXXXXXXXX 来自:XXXXXXXXXXXXXXX
WORKDIR指令
- WORKDIR为其他指令设置工作目录;
格式:WORKDIR <工作目录路径>
;
WORKDIR指令为Dockerfile中的任何RUN,CMD,ENTRYPOINT,COPY和ADD指令设置工作目录(或称当前目录)。(也就是说以后各层的当前目录就被改为WORKDIR指定的目录)如果WORKDIR对应的目录不存在,将会自动被创建。
ENV指令
- ENV设置环境变量;
格式:ENV <key> <value>
或ENV <key>=<value>
;
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,还是运行时的应用,都可以直接使用这里定义的环境变量。如下所示,下面是为tomcat设置环境变量并让tomcat自启动的一个Dockerfile片段。ENV设置的环境变量CATALINA_HOME
能够被后续定义指令使用。
ENV CATALINA_HOME /var/tmp/apache-tomcat-8.0.45
ENV PATH $PATH: $CATALINA_HOME/bin
ENTRYPOINT $CATALINA_HOME /bin/startup.sh && /bin/bash
ARG指令
- ARG构建参数;
格式:ARG <参数名>[=<默认值>]
;
ARG与ENV有些类似,它们都可以被后面的其它指令直接使用,但是它并不是环境变量,这意味着将来容器运行时是不会存在ARG变量的。
Dockerfile中的ARG指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令docker build
中用 --build-arg <参数名>=<值>
来覆盖。
什么时候用ARG,什么时候用ENV?
如果想保存为环境变量,就用ENV
;如果只想在Dockerfile中临时使用,就用ARG
。
EXPOSE指令
- EXPOSE暴露端口;
格式:EXPOSE <端口1> [<端口2>...]
EXPOSE指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。如果想要公开容器的端口,必须在docker run是指定-p参数去公开端口或者指定-P参数公开所有被EXPOSE的端口。具体可以参照Docker官方文档 。
在Dockerfile中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P
时,会自动随机映射EXPOSE 的端口。
实例:使用Dockerfile,创建一个拥有java和tomcat运行环境的镜像。
Dockerfile的内容如下,首先使用FROM指定基础镜像为ubuntu:latest
镜像。然后使用WORKDIR设置当前的工作目录为/var/tmp。接下来使用RUN命令将jre.tar.gz下载到工作目录,并解压文件,然后删除jre.tar.gz;然后用类似的方式处理tomcat。
接下来使用ENV配置java与tomcat的环境变量,由于tomcat服务会默认监听8080端口,所以使用EXPOSE
暴露端口号。最后使用ENTRYPOINT
设置启动命令,使tomcat服务随容器启动而启动。
FROM ubuntu
WORKDIR /var/tmp
RUN apt-get update && \
apt-get install -y wget && \
wget --no-check-certificate --no-cookies --header "Cookie: o\fraclelicense=accept-securebackup-cookie" http://download.o\fracle.com/otn-pub/java/jdk/8u144-b01/090f390dda5b47b9b721c7dfaa008135/jre-8u144-linux-x64.tar.gz && \
tar -xzf jre-8u144-linux-x64.tar.gz && \
rm jre-8u144-linux-x64.tar.gz
RUN wget "http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.0.45/bin/apache-tomcat-8.0.45.tar.gz" && \
tar -xzf apache-tomcat-8.0.45.tar.gz && \
rm apache-tomcat-8.0.45.tar.gz
ENV JAVA_HOME /var/tmp/jre1.8.0_144
ENV CATALINA_HOME /var/tmp/apache-tomcat-8.0.45
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/bin
EXPOSE 8080
ENTRYPOINT /var/tmp/apache-tomcat-8.0.45/bin/startup.sh && /bin/bash
ONBUILD指令
- ONBUILD添加一个将来执行的触发器(trigger);
格式:ONBUILD <其它指令>
;
ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如RUN, COPY等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
ONBUILD指令的具体执行步骤
- 在构建过程中,ONBUILD指令会添加到触发器指令镜像元数据中,这些触发器指令并不会在当前构建过程中执行。
- 在构建过程后,触发器指令会被存储在镜像详情中,其主键是OnBuild,可以使用
docker inspect
命令查看。 - 在之后该镜像可能作为其他Dockerfile中FROM指令的参数。在构建过程中,FROM指令会查找ONBUILD触发器指令,并且会以它们注册的顺序执行。若有触发器指令执行失败,则FROM指令被中止,并返回失败;若所有触发器指令执行成功,则FROM指令完成并继续执行下面的指令。在镜像构建完成后,触发器指令会被清除,不会被子孙镜像继承。
ONBUILD指令的实例
- 首先编写一个Dockerfile文件,内容如下所示:
FROM busybox
ONBUILD RUN touch 1.txt
- 利用上面的Dockerfile文件构建一个新镜像:
docker build -t image1 .
。执行docker run image1 cat 1.txt
,提示:cat: can't open '1.txt': No such file or directory
。可以知道基于image1镜像构建的容器中不存在1.txt文件。我们通过inspect image1
,在里面可以找到。
"OnBuild":[
"RUN touch 1.txt"
],
- 编写一个新的Dockerfile文件,内容如下所示:
FROM image1
RUN echo 'hello'
- 利用第三步创建的的Dockerfile文件构建一个新镜像:
docker build -t image2 .
。如下所示:在执行完FROM指令后,首先执行的是触发器,也就是# Executing 1 build trigger...
,该指令创建了一个1.txt文件。然后才执行RUN echo 'hello'。执行docker run image2 cat 1.txt
,执行成功!!
[root@localhost dir1]# docker build -t image2 .
Sending build context to Docker daemon 2.048 kB
Step 1/2 : FROM image1
# Executing 1 build trigger...
Step 1/1 : RUN touch 1.txt
---> Running in 5c9b99ef7801
---> 4c5b719176b8
Removing intermediate container 5c9b99ef7801
Step 2/2 : RUN echo 'hello'
---> Running in 8a2598e90e1f
hello
---> f5642bb4a975
Removing intermediate container 8a2598e90e1f
Successfully built f5642bb4a975
[root@localhost dir1]# docker run image2 cat 1.txt`
[root@localhost dir1]#
VOLUME指令
- VOLUME定义匿名卷;
格式:VOLUME ["<路径1>", "<路径2>"...]或VOLUME <路径>
;
之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于数据卷(volume)中,后面的章节我们会进一步介绍Docker数据卷的概念。
参考文献:《Docker容器与容器云p179》。