Docker(六)DockerFile

我们知道镜像是一层一层来构建的,Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。简单的说,Dockerfile就是构建镜像的一个脚本。

对于编写好的DockerFile文件,使用docker build命令即可生成一个镜像(image)。例如:

1
docker build -f /root/docker_file/myDockerFile -t my_custom/centos .

该命令中的参数:

  • -f:file,指定DockerFile文件的路径
  • -t:taget,指定构建后的镜像名和版本(tag),不指定tag则默认是latest。
  • 结尾的.号:用来指定镜像构建过程中的上下文环境的目录为当前目录。

DockerFile是什么样的呢?在https://hub.docker.com/上我们可以找到centos7.3容器的DockerFile文件:

1
2
3
4
5
6
7
8
9
10
FROM scratch
MAINTAINER https://github.com/CentOS/sig-cloud-instance-images
ADD centos-7-docker.tar.xz /

LABEL name="CentOS Base Image" \
vendor="CentOS" \
license="GPLv2" \
build-date="20161214"

CMD ["/bin/bash"]

一.DockerFile的基础知识

DockerFile脚本的特点

  • 每条保留字指令都必须为大写字母且后面要跟随至少一个参数。
  • 指令按照从上到下,顺序执行。
  • #表示注释。
  • 每条指令都会创建一个新的镜像层,并对镜像进行提交。

Docker执行DockerFile的大致流程

  1. docker从基础镜像运行一个容器。
  2. 执行一条指令并对容器作出修改。
  3. 执行类似docker commit的操作提交一个新的镜像层。
  4. docker再基于刚提交的镜像运行一个新容器。
  5. 执行dockerfile中的下一条指令直到所有指令都执行完成。

DockerFile、Docker镜像和Docker容器三者之间的关系

  • Dockerfile定义了进程需要的一切东西。Dockerfile涉及的内容包括执行代码或者是文件、环境变量、依赖包、运行时环境、动态链接库、操作系统的发行版、服务进程和内核进程(当应用进程需要和系统服务和内核进程打交道,这时需要考虑如何设计namespace的权限控制)等等。
  • Docker镜像,编辑好一个DockerFile文件后,通过docker build可以生成一个Docker镜像。
  • Docker容器,通过docker run运行一个镜像便得到一个docker容器,容器是直接提供服务的。

二.DockerFile常用指令

FROM

指定当前新镜像是基于哪个镜像的。
Dockerfile中第一条指令必须是FROM指令。
语法:

1
2
3
FROM <image>
FROM <image>:<tag>
FROM <image>:<digest>

三种写法,其中<tag>和<digest>是可选项,如果没有选择,那么默认值为latest。如果不以任何镜像为基础,那么写法为:FROM scratch。 对比Java中每个类都是Object类的子类来理解,scratch可以说就是所有镜像的父镜像。

MAINTAINER

指定镜像维护者的信息。

ARG

语法格式:

1
ARG <name>[=<default value>]

设置变量命令。ARG指令用来定义一个变量,在docker build创建镜像的时候,使用--build-arg <varname>=<value>来指定参数。
如果用户在build镜像时指定了一个参数没有定义在Dockerfile中,那么将有一个Warning。提示如下:

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

我们可以定义一个或多个参数,如下:

1
2
3
4
FROM busybox
ARG user1
ARG buildno
...

也可以给参数一个默认值:

1
2
3
4
FROM busybox
ARG user1=someuser
ARG buildno=1
...

如果我们给了ARG定义的参数默认值,那么当build镜像时没有指定参数值,将会使用这个默认值。

RUN

指定容器构建时需要运行的命令。通常用于安装应用和软件包。
有两种格式

1
2
RUN <command>		#格式一:shell格式
RUN ["<executable>", "<param1>", "<param2>"] #格式二:exec格式

第一种后边直接跟shell命令:
在linux操作系统上默认 /bin/sh -c
在windows操作系统上默认 cmd /S /C
例如:apt-get install python3
第二种是类似于函数调用。可将<executable>理解成为可执行文件,后面就是两个参数。
例如:["apt-get", "install", "python3"]

两种写法比对:

1
2
3
4
5
6
7
#第一种
RUN yum install -y curl \
echo $HOME \
/bin/sh -c "echo hello"

#第二种
RUN ["/bin/bash", "-c", "echo hello"]

注意:多行命令不要写多个RUN,原因是Dockerfile中每一个指令都会建立一层。多少个RUN就构建了多少层镜像,会造成镜像的臃肿、多层,不仅仅增加了构件部署的时间,还容易出错。RUN书写时的换行符是\

EXPOSE

容器运行时暴露给外部的端口。
这个端口供容器外部连接使用。格式为:

1
EXPOSE <port1> [<port2> <port3>...]

在启动容器时通过加-P参数(注意是大写),Docker宿主机会自动分配一个端口转发到指定的端口;使用-p,则可以具体指定宿主机哪个本地端口映射到容器的这个端口。
例如:我在elasticsearch镜像的Dockerfile中指定了暴露出9200和9300端口,我可以在Dockerfile中写:

于是,容器的这两个端口就暴露出来了,启动容器的时候如果参数是docker run –P elasticsearch命令,则会自动分配宿主机上的两个端口给容器暴露的这两个端口。

外界访问的接口就是32770和32771。
如果加上端口映射,以docker run –p 9200:9200 –p 9300:9300 elasticsearch命令启动,则通过宿主机机的9200端口和9300端口就可以访问该elasticsearch容器了。

ENV

用于指定构建镜像过程中的环境变量。这些环境变量,后续可以被RUNWORKDIR等指令使用;容器运行起来之后,也可以在容器中获取这些环境变量。
语法有两种

1
2
1. ENV <key> <value>
2. ENV <key>=<value> ...

两者的区别就是第一种是一次设置一个,第二种是一次设置多个。
例如:

1
2
3
ENV word hello
ENV download_path=/usr/test js_path=/app_data/static/js
RUN echo $word

WORKDIR

指定在创建容器后,终端默认登陆的进来工作目录。
格式: WORKDIR ${path}
RUNCMDENTRYPOINTCOPYADD生效。如果不存在则会创建,也可以设置多次。
如:

1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

pwd执行的结果是/a/b/c

WORKDIR也可以解析环境变量(ENV)。
如:

1
2
3
4
ENV DIRPATH /app_data
ENV DIRNAME static
WORKDIR $DIRPATH/$DIRNAME
RUN pwd

pwd的执行结果是/app_data/static

ADD

语法格式:

1
2
ADD <src>... <dest>   #格式一
ADD ["<src>",... "<dest>"] #格式二

这是一个复制命令,将复制指定宿主机目录中(src)的文件到容器中的指定目录(dest)。

<src>可以是一个绝对路径,也可以是一个URL或一个tar文件,如果是一个url则ADD命令类似于wget,如果是tar文件会自动解压为目录。

<dest>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径。
说明:对于从远程URL获取资源的情况,由于ADD指令不支持认证,如果从远程获取资源需要认证,则只能使用RUN wgetRUN curl替代。

如以下写法都是可以的:

1
2
3
ADD test relativeDir/ 
ADD test /relativeDir
ADD http://example.com/foobar /download

尽量不要把<scr>写成一个文件夹,如果<src>是一个文件夹了,会复制整个目录的内容,包括文件系统元数据。

COPY

语法格式:

1
2
COPY <src>... <dest> 		#格式一
COPY ["<src>",... "<dest>"] #格式二

类似ADD,也是一个复制命令。
ADD的区别,COPY的<src>只能是本地文件,其他用法一致。

VOLUME

语法为:

1
VOLUME ["<path>"]

该指令用来设置容器数据卷,实现挂载功能,可以将宿主机上或者其他容器中的目录挂在到这个容器指定的目录。
这个指令一般用来持久化存储数据。因为容器使用的是AUFS,这种文件系统不能持久化数据,当容器关闭后,所有的更改都会丢失,使用这个指令可以将容器中的数据持久化到宿主机的目录上。

说明:
[“<path>“]可以是一个JsonArray,也可以是多个值。所以如下几种写法都是正确的:

1
2
3
VOLUME ["/var/log/"]
VOLUME /var/log
VOLUME /var/log /var/db

CMD

该指令是指定一个容器启动时要运行的命令。
语法有三种写法:

1
2
3
CMD <command> <param1> <param2>		#使用shell方式执行
CMD ["<executable>","<param1>","<param2>"] #使用exec执行,这是推荐的方式
CMD ["<param1>","<param2>"] #提供给ENTERYPOINT的默认参数

第一种比较好理解,就时shell这种执行方式和写法。例如:

1
2
3
CMD echo $MYPATH
CMD echo "build success..."
CMD /bin/bash

第二种和第三种其实都是可执行文件加上参数的形式。不同之处是第一种同时指定了可执行文件和参数,而第二种是指定了ENTRYPOINT指令需要的参数。
第二种的写法举例:

1
CMD ["/bin/echo", "this is a echo test"]

运行容器 docker run -it [image] 将输出:

this is a echo test
类似于开机启动项的感觉。

注意:如果Dockerfile中指定了多条CMD命令,只有最后一条会被执行。如果用户启动时候加了运行的命令,则会覆盖掉CMD指定的指令。比如 docker run -it [image] /bin/bashCMD 会被忽略掉,命令 bash 将被执行,而不会执行打印命令:

root@10a32dc7d3d3:/#

第三种的写法可以参考NTERYPOINT指令说明。

ENTRYPOINT

类似CMD,指定一个容器启动时要运行的命令。
也是有shell和exec两种格式:

1
2
ENTRYPOINT <command> <param1> <param2>
ENTRYPOINT ["<executable>", "<param1>", "<param2>"]

CMD相比较,相同点:

  1. 运行时机相同,都是容器启动时运行。
  2. 如果写多条,只有最后一条会生效。
    不同点:
  3. ENTRYPOINT不会被启动容器时命令行中的command覆盖,而CMD则会被覆盖。命令行中的command参数会追加到NTRYPOINT指令上,这样就可以用来提供额外的参数。
  4. 如果在Dockerfile种同时写了ENTRYPOINTCMD,那么CMD指定的内容将会作为ENTRYPOINT的参数。

如下是一个示例:
在DockerFile中有如下指令:

1
ENTRYPOINT ["/bin/echo", "Hello"]

当容器通过 docker run -it [image] 启动时,输出为:

1
Hello

而如果通过 docker run -it [image] Java 启动,则输出为:

1
Hello Java

将Dockerfile修改为:

1
2
ENTRYPOINT ["/bin/echo", "Hello"]  
CMD ["world"]

当容器通过 docker run -it [image] 启动时,输出为:

1
Hello world

而如果通过 docker run -it [image] Java 启动,输出依旧为:

1
Hello Java

如果将Dockerfile修改为:

1
2
CMD ["/bin/echo", "world"]
ENTRYPOINT ["/bin/echo", "Hello"]

当容器通过 docker run -it [image] 启动时,输出为:

1
Hello /bin/echo world

如果将Dockerfile修改为:

1
2
CMD echo world
ENTRYPOINT ["/bin/echo", "Hello"]

当容器通过 docker run -it [image] 启动时,输出为:

1
Hello /bin/sh -c echo world

ONBUILD

该命令用于指定当所创建的镜像作为其他新建镜像的基础镜像时所执行的指令。
语法:

1
ONBUILD [INSTRUCTION]

这个命令只对当前镜像的子镜像生效。比如当前镜像为A,在Dockerfile种添加:

1
2
ONBUILD ADD . /app
ONBUILD RUN python app.py

这两个命令不会在A镜像构建或启动的时候执行。此时有一个镜像B是基于A镜像构建的,则B在构建镜像的时候会自动执行这两条命令,等价于在B镜像的Dockerfile中增加了两条指令:

1
2
3
FROM A
ADD ./app
RUN python app.py

关于指令的补充说明

COPY和ADD比较

COPY指令和ADD指令都可以将主机上的资源复制或加入到容器镜像中,都是在构建镜像的过程中完成的。
不同点:

  1. ADD的<src>参数可以是一个URL或者是一个可识别的压缩格式(tar, gzip, bzip2, etc)的本地文件。而COPY的<src>参数只能是本地文件。
  2. COPYADD 的另一个区别时候COPY可以用在 multistage 场景下。在 multistage 的用法中,可以使用 COPY 命令把前一阶段构建的产物拷贝到另一个镜像中。

相同点:

  1. 都能将本地文件夹或者本地文件复制到容器镜像中。
  2. <dest>参数指定的文件名或者目录名不存在,会自动创建。
  3. 对于<src>是文件还是目录,<dest>是否以‘/’结尾,最终拷贝的效果可以参考Linux的命令。例如
    • 如果<src>参数是个文件(比如:/tmp/app_data/a.txt),且<dest>是以/结尾(比如:/path1/path2/), 则docker会把目标路径当作一个目录,会把源文件拷贝到该目录下(容器内最终是:/path1/path2/a.txt)。
    • 如果<src>参数是个文件(比如:/tmp/app_data/a.txt),且<dest>参数不是以/结尾比如:/path1/abc),则docker会把文件复制为<dest>参数指定的文件(容器内最终是:/path1/abc)。
    • 如果<src>参数是一个目录(比如:/tmp/app_data/js),<dest>参数不管是否以/结尾(比如:/path1/path2/),则docker会把<src>目录下的文件复制到<dest>下(最终:/path1/path2/目录下有/tmp/app_data/js目录下的所有文件。js这个目录名不会复制)。

小总结:COPY是Docker 1.0发布时候新加入的指令,COPY是ADD的一种简化版本,目的在于满足大多数人“复制文件到容器”的需求。
Docker 团队的建议是在大多数情况下使用COPY。拷贝文件的原则:使用COPY(除非你明确你需要ADD

RUN和CMD区别

RUN是构件容器时就运行的命令以及提交运行结果。
CMD是容器启动时执行的命令,在构建时并不运行,构建时仅仅是在镜像文件中指定了这个命令具体是什么。

CMD和ENTRYPOINT使用场景的区别

如果想为容器设置默认的启动命令,可使用 CMD 指令。用户可在 docker run 命令行中替换此默认命令。
如果 Docker 镜像的用途是运行应用程序或服务,比如运行一个 MySQL,应该优先使用 Exec 格式的 ENTRYPOINT 指令。CMD 可为 ENTRYPOINT 提供额外的默认参数,同时可利用 docker run 命令行替换默认参数。

------ 本文完 ------