Docker入门实践之dokerfile编写(2)

前面我介绍到dockerfile的经常使用的指令,本篇将继续讲到dockerfile相关指令。html

1. CMD 容器启动命令

CMD 指令的格式和 RUN 类似,也是两种格式:node

shell 格式:CMD <命令>
exec 格式:CMD ["可执行文件", "参数1", "参数2"...]

参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。python

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,须要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。在运行时能够指定新的命令来替代镜像设置中的这个默认命令,好比,ubuntu 镜像默认的 CMD 是 /bin/bash,若是咱们直接 docker run -it ubuntu 的话,会直接进入 bash。咱们也能够在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。linux

在指令格式上,通常推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,所以必定要使用双引号 ",而不要使用单引号。若是使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。好比:nginx

CMD echo $HOME
在实际执行中,会将其变动为:
CMD [ "sh", "-c", "echo $HOME" ]

这就是为何咱们可使用环境变量的缘由,由于这些环境变量会被 shell 进行解析处理。提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。web

Docker 不是虚拟机,容器中的应用都应该之前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。一些常会错误地将 CMD 写为:docker

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 可执行文件,而且要求之前台形式运行。好比:shell

CMD ["nginx", "-g", "daemon off;"]
2. EXPOSE 声明对外映射端口

格式为: EXPOSE <port> [<port>.....] 列如:npm

EXPOSE 22 80  8443

上述示例,申明docker应用服务须要暴露的端口,同时能够在容器启动的时能够经过-P使容器主机随机分配一个端口给指定的端口,使用-p能够具体指定那个端口映射。json

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

3. ENTRYPOINT 入口点

ENTRYPOINT 的格式和 RUN 指令格式同样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 同样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也能够替代,不过比 CMD 要略显繁琐,须要经过 docker run 的参数 --entrypoint 来指定。当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,再也不是直接的运行其命令,而是将 CMD 的内容做为参数传给 ENTRYPOINT 指令,即:

<ENTRYPOINT> "<CMD>"

有了 CMD 后,为啥还须要ENTRYPOINT,它们之间有什么区别呢?以下示例所示:假设须要一个得知本身当前公网 IP 的镜像,那么能够先用 CMD 来实现:

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

使用 docker build -t myip . 来构建镜像的话,若是须要查询当前公网 IP,只须要执行:

$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通

若是命令须要添加参数,好比从上面的 CMD 中能够看到实质的命令是 curl,那么若是咱们但愿显示 HTTP 头信息,就须要加上 -i 参数。那么能够直接加 -i 参数给 docker run myip 么?

$ docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

能够看到可执行文件找不到的报错,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 就能够解决这个问题。如今用 ENTRYPOINT 来实现这个镜像:

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

尝试直接使用 docker run myip -i:

$ docker run myip
当前 IP:61.148.226.66 来自:北京市 联通
$ docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive

当前 IP:61.148.226.66 来自:北京市 联通

能够看到,此次成功了。这是由于当存在 ENTRYPOINT 后,CMD 的内容将会做为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,所以会做为参数传给 curl,从而达到了预期的结果。

4. HEALTHCHECK 健康检查

格式:

HEALTHCHECK [选项] CMD <命令>:设置检查容器健康情况的命令
HEALTHCHECK NONE:若是基础镜像有健康检查指令,使用这行能够屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。在没有 HEALTHCHECK 指令前,Docker 引擎只能够经过容器内主进程是否退出来判断容器是否状态异常。不少状况下这没问题,可是若是程序进入死锁状态,或者死循环状态,应用进程并不退出,可是该容器已经没法提供服务了。在 1.12 之前,Docker 不会检测到容器的这种状态,从而不会从新调度,致使可能会有部分容器已经没法提供服务了却还在接受用户请求。而自 1.12 以后,Docker 提供了健康检查指令,经过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,若是连续必定次数失败,则会变为 unhealthy。HEALTHCHECK 支持下列选项:

--interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
--timeout=<时长>:健康检查命令运行超时时间,若是超过这个时间,本次健康检查就被视为失败,默认 30 秒;
--retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
和 CMD, ENTRYPOINT 同样,HEALTHCHECK 只能够出现一次,若是写了多个,只有最后一个生效。

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

假设咱们有个镜像是个最简单的 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

这里咱们设置了每 5 秒检查一次(这里为了试验因此间隔很是短,实际应该相对较长),若是健康检查命令超过 3 秒没响应就视为失败,而且使用 curl -fs http://localhost/ || exit 1 做为健康检查命令。使用 docker build 来构建这个镜像:

$ docker build -t myweb:v1 .

构建好了后,咱们启动一个容器:

$ docker run -d --name web -p 80:80 myweb:v1

当运行该镜像后,能够经过 docker container ls 看到最初的状态为 (health: starting):

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web

在等待几秒钟后,再次 docker container ls,就会看到健康状态变化为了 (healthy):

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   18 seconds ago      Up 16 seconds (healthy)   80/tcp, 443/tcp     web

若是健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)。为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,能够用 docker inspect 来查看。

$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool
{
    "FailingStreak": 0,
    "Log": [
        {
            "End": "2016-11-25T14:35:37.940957051Z",
            "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",
            "Start": "2016-11-25T14:35:37.780192565Z"
        }
    ],
    "Status": "healthy"
}
5. ONBUILD 为他人作嫁衣裳

格式:ONBUILD <其它指令>。

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,好比 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。Dockerfile 中的其它指令都是为了定制当前镜像而准备的,惟有 ONBUILD 是为了帮助别人定制本身而准备的。假设咱们要制做 Node.js 所写的应用的镜像。咱们都知道 Node.js 使用 npm 进行包管理,全部依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,须要先进行 npm install 才能够得到全部须要的依赖。而后就能够经过 npm start 来启动应用。所以,通常来讲会这样写 Dockerfile:

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

把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就能够直接拿来启动容器运行。可是若是咱们还有第二个 Node.js 项目也差很少呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那若是有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让咱们继续看这样的场景维护的问题。

若是第一个 Node.js 项目在开发过程当中,发现这个 Dockerfile 里存在问题,好比敲错字了、或者须要安装额外的包,而后开发人员修复了这个 Dockerfile,再次构建,问题解决。第一个项目没问题了,可是第二个项目呢?虽然最初 Dockerfile 是复制、粘贴自第一个项目的,可是并不会由于第一个项目修复了他们的 Dockerfile,而第二个项目的 Dockerfile 就会被自动修复。那么咱们可不能够作一个基础镜像,而后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,从新构建后就继承了基础镜像的更新?好吧,能够,让咱们看看这样的结果。那么上面的这个 Dockerfile 就会变为:

FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]

这里咱们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 my-node 的话,各个项目内的本身的 Dockerfile 就变为:

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

基础镜像变化后,各个项目都用这个 Dockerfile 从新构建镜像,会继承基础镜像的更新,那么,问题解决了么?没有。准确说,只解决了一半。若是这个 Dockerfile 里面有些东西须要调整呢?好比 npm install 都须要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,由于涉及到了当前项目的 ./package.json,难道又要一个个修改么?因此说,这样制做基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,然后面三条指令的变化则彻底没办法处理。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,可是此次将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。而后各个项目的 Dockerfile 就变成了简单地:

FROM my-node

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

6. ARG 构建参数

格式:ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果同样,都是设置环境变量。所不一样的是,ARG 所设置的构建环境的环境变量,在未来容器运行时是不会存在这些环境变量的。可是不要所以就使用 ARG 保存密码之类的信息,由于 docker history 仍是能够看到全部值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值能够在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。在 1.13 以前的版本,要求 --build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 --build-arg 指定的参数,必须在 Dockerfile 中使用了。若是对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,再也不报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用一样的构建流程构建不一样的 Dockerfile 的时候比较有帮助,避免构建命令必须根据每一个 Dockerfile 的内容修改。

相关文章
相关标签/搜索