使用Docker构建应用 | 第二篇:Dockerfile详解

标签: Docker  

1. Dockerfile文件格式

Dockerfile文件格式如下:

# Comment
INSTRUCTION arguments
# 注释
指令 参数

Dockerfile文件中指令不区分大小写,但为了更易区分,约定使用大写形式。

Docker 会依次执行Dockerfile中的指令,文件中的第一条指令必须是FROMFROM指令用于指定一个基础镜像。

#开头的行,Docker会认为是注释。但#出现在指令参数中时,则不是注释。如:

# Comment
RUN echo 'we are running some # of cool things'

2. Dockerfile中的指令

2.1 FROM

FROM指令用于指定其后构建新镜像所使用的基础镜像。FROM指令必是Dockerfile文件中的首条命令,启动构建流程后,Docker将会基于该镜像构建新镜像,FROM后的命令也会基于这个基础镜像。

FROM语法格式为:

FROM <image>

FROM <image>:<tag>

FROM <image>:<digest>

通过FROM指定的镜像,可以是任何有效的基础镜像。FROM有以下限制:

  • FROM必须是Dockerfile中第一条非注释命令
  • 在一个Dockerfile文件中创建多个镜像时,FROM可以多次出现。只需在每个新命令FROM之前,记录提交上次的镜像ID。
  • tagdigest是可选的,如果不使用这两个值时,会使用latest版本的基础镜像

2.2 RUN

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

  • shell 格式:RUN <command>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["executable", "param1", "param2"],这更像是函数调用中的格式。

RUN可以执行任何命令,然后在当前镜像上创建一个新层并提交。提交后的结果镜像将会用在Dockerfile文件的下一步。

通过RUN执行多条命令时,可以通过\换行执行

FROM debian:jessie

RUN buildDeps='gcc libc6-dev make' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

2.3 CMD

CMD用于指定在容器启动时所要执行的命令。CMD有以下三种格式:

  • shell 格式:CMD <command>
  • exec 格式:CMD ["executable", "param1", "param2"...]
  • 参数列表格式:CMD ["param1", "param2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

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

提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。

一些初学者将 CMD 写为:

CMD service nginx start

然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

而使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD [ "sh", "-c", "service nginx start"],因此主进程实际上是 sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

CMD ["nginx", "-g", "daemon off;"]

2.4 ENTRYPOINT

ENTRYPOINT用于给容器配置一个可执行程序。也就是说,每次使用镜像创建容器时,通过ENTRYPOINT指定的程序都会被设置为默认程序。ENTRYPOINT有以下两种形式:

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

ENTRYPOINTCMD非常类似,不同的是通过docker run执行的命令不会覆盖ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给ENTRYPOINTDockerfile中只允许有一个ENTRYPOINT命令,多指定时会覆盖前面的设置,而只执行最后的ENTRYPOINT指令。

docker run运行容器时指定的参数都会被传递给ENTRYPOINT,且会覆盖CMD命令指定的参数。如,执行docker run <image> -d时,-d参数将被传递给入口点。

也可以通过docker run --entrypoint重写ENTRYPOINT入口点。

如:可以像下面这样指定一个容器执行程序:

ENTRYPOINT ["/usr/bin/nginx"]

完整构建代码:

# Version: 0.0.3
FROM ubuntu:16.04
MAINTAINER 何民三 "cn.liuht@gmail.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hello World, 我是个容器' \ 
   > /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/nginx"]
EXPOSE 80

使用docker build构建镜像,并将镜像指定为itbilu/test

$ sudo docker build -t="itbilu/test" .

构建完成后,使用itbilu/test启动一个容器:

$ sudo docker run -i -t  itbilu/test -g "daemon off;"

在运行容器时,我们使用了-g "daemon off;",这个参数将会被传递给ENTRYPOINT,最终在容器中执行的命令为/usr/sbin/nginx -g "daemon off;"

2.5 LABEL

LABEL用于为镜像添加元数据,元数以键值对的形式指定:

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

使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。

如,通过LABEL指定一些元数据:

LABEL version="1.0" description="这是一个Web网站" by="liangbo"

或者

LABEL version="1.0" \ 
	  description="这是一个Web网站" \
	  by="liangbo"

注意;Dockerfile中还有个MAINTAINER命令,该命令用于指定镜像作者。但MAINTAINER并不推荐使用,更推荐使用LABEL来指定镜像作者。如:

LABEL maintainer="liangbo"

2.6 EXPOSE

EXPOSE用于指定容器在运行时监听的端口:

EXPOSE <port> [<port>...]

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P时,会自动随机映射 EXPOSE 的端口。

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

2.7 ENV

格式有两种:

ENV <key> <value>
ENV <key>=<value> ...

下列指令可以支持环境变量展开: RUNADDCOPYENVEXPOSELABELUSERWORKDIRVOLUMESTOPSIGNALONBUILD等。

可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。

2.8 COPY

COPY同样用于复制构建环境中的文件或目录到镜像中。其有以下两种使用方式:

COPY <src>... <dest>
COPY ["<src>",... "<dest>"]

COPY指令非常类似于ADD,不同点在于COPY只会复制构建目录下的文件,不能使用URL也不会进行解压操作。

2.9 ADD

ADD用于复制构建环境中的文件或目录到镜像中。其有以下两种使用方式:

ADD <src>... <dest>
ADD ["<src>",... "<dest>"]

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。

在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

另外需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

因此在 COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD

2.10 VOLUME

VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷:

VOLUME ["/data"]

一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

  • 卷可以容器间共享和重用
  • 容器并不一定要和其它容器共享卷
  • 修改卷后会立即生效
  • 对卷的修改不会对镜像产生影响
  • 卷会一直存在,直到没有任何容器在使用它

VOLUME让我们可以将源代码、数据或其它内容添加到镜像中,而又不并提交到镜像中,并使我们可以多个容器间共享这些内容。

如,通过VOLUME创建一个挂载点:

ENV PATH /home/tmp/
VOLUME [$PATH]

构建的镜像,并指定镜像名为volume/test。构建镜像后,使用新构建的运行一个容器。运行容器时,需-v参将能本地目录绑定到容器的卷(挂载点)上,以使容器可以访问宿主机的数据。

$ sudo docker run -i -t -v ~/code/tmp:/home/tmp/  volume/test 
root@31b0fac536c4:/# cd /home/tmp/
root@31b0fac536c4:/home/tmp# ls
README.md  app.js  bin  config.js  controller  db  demo  document  lib  minify.js  node_modules  package.json  public  routes  test  views

如上所示,我们已经可以容器的/home/tmp/目录下访问到宿主机~/code/tmp目录下的数据了。

2.11 USER

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

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

2.12 WORKDIR

WORKDIR用于在容器内设置一个工作目录:

WORKDIR /path/to/workdir

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

通过WORKDIR设置工作目录后,Dockerfile中其后的命令RUNCMDENTRYPOINTADDCOPY等命令都会在该目录下执行。

在使用docker run运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

2.13 ARG

ARG用于指定传递给构建运行时的变量:

ARG <name>[=<default value>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

2.14 ONBUILD

ONBUILD用于设置镜像触发器:

ONBUILD [INSTRUCTION]

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

例如:

让我们用 ONBUILD 写一下基础镜像的 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

项目内的自己的 Dockerfile 就变为:

FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

FROM my-node这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。

2.15 HEALTHCHECK

格式:

  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
  • HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy

HEALTHCHECK 支持下列选项:

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

CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

参考

Dockerfile reference Docker镜像构建文件Dockerfile及相关命令介绍

「真诚赞赏,手留余香」

请我喝杯咖啡?

使用微信扫描二维码完成支付

相关文章