3.使用Dockerfile定制镜像

docker

使用 Dockerfile 定制镜像

从之前的docker commit的学习里可以了解到,镜像定制实际就是定制每一层所添加的配置、文件。若将每一层的命令都写入一个脚本,用这个脚本来构建镜像,之前黑箱镜像的缺陷就都会解决,这个脚本就是Dockerfile。
Dockerfile是一个文本文件,其中包含一条一条指定,每一条指定构建一层,因此每一条指定就是指定该层应该如何创建。
以nginx镜像为例,使用Dockerfile来定制。
在一个空白目录中创建一个名为Dockerfile的文件。
Dockerfile主体内容分为四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

1
2
3
mkdir mynginx
cd mynginx
touch Dockerfile

在Docker文件中添加内容为:

1
2
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

这样使用Dockerfile定制镜像的脚本就完成了。

FROM指定镜像基础

定制镜像就是在一个镜像的基础上进行定制。基础镜像是必须指定的,而且必须是Dockerfile中的第一条指令。
Docker还存在一个特殊的镜像名为scratch,它表示一个空白镜像。

RUN执行命令

RUN指令是用来执行命令行命令的。每一个RUN行为都会新建一层在其上执行命令,执行结束后commit这一层的修改,构成新的镜像。RUN支持两种命令格式。

  • shell格式:RUN COMMAND,就像直接在命令行中输入命令一样
  • exec格式:RUN [可执行文件,参数1,参数2]

在构建镜像的Dockerfile中将有共同目的指定放在一个RUN下,例如安装redis的多个命令行命令,因为这样可以防止镜像臃肿、防止有多层镜像等问题。
在将多条指定当做一个RUN动作的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释XXXXXXXXXXX
RUN buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.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

在这个例子中可以看到有删除命令,这是一步很重要的操作,因为在这一层存储的东西不会在下一层删除,会一直跟着镜像,在构建时一定要确保每层只添加真正需要的东西,任何无关的东西都清掉

构建镜像

在Dockerfile所在目录执行docker build [选项] 上下文路径/URL/- ,在执行构建命令时可以加上-t 镜像名[:标签] 指定镜像名与标签。

1
docker build -t nginx:v3 .

镜像构建上下文(context)

在上面的例子中上下文路径就是Dockerfile所在的目录的路径,也就是在命令尾部的.,这是默认情况下会把上下文目录中的Dockerfile文件当做Dockerfile;当然可以指定Dockerfile,例如使用-f指定某个文件为Dockerfile,这个文件名可以不是Dockerfile,也可以不在上下文目录中。
当构建镜像的时候用户会指定构建镜像的上下文,docker build 就会把这个路径下的所有内容打包(所以不要将多余文件放到build context中),上传给docker引擎(服务端守护进程),展开后就是获得构建镜像所需的一切文件。构建上下文环境会被递归处理,所以,构建所指定的路径还包括了子目录,而URL还包括了其中指定的子模块。

docker file其他用法

从URL中构建镜像

1
docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1

指定构建目录为11.1,然后Docker会自己去git clone这个项目,进入到指定目录开始构建。
从URL构建

用给定的tar压缩包构建

1
docker build http://server/context.tar.gz

如果给定的URL是tar压缩包,Docker会下载这个压缩包并自动解压,以其作为上下文开始构建。

从标准输入中读取Dockerfile进行构建

1
2
3
docker build - < Dockerfile

cat Dockerfile | docker build -

如果是从标准输入传入的是文本文件,则将其视为Dockerfile,并开始构建。因为没有上下文,因此不能有本地文件COPY进镜像等操作。

从标准输入中读取上下文压缩包进行构建

1
docker build - < context.tar.gz

如果发现标准输入的文件格式是压缩包,会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

Dockerfile中其他指令详解

MAINTAINER

设置镜像的作者,可以是任意字符串。

COPY复制文件

COPY是将 build context 中的文件或目录复制到镜像内的目标路径位置。

1
2
3
4
COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
例:
COPY package.json /usr/src/app/

源路径可以是多个,甚至可以是通配符,。

1
2
COPY hom* /mydir/
COPY hom?.txt /mydir/

目标路径可以是容器的绝对路径,也可以是工作目录的相对路径(工作目录用WORKDIR指定),目标路径不需要事先创建。
在使用COPY时源文件的各种元数据都会被保留,比如读、写、执行权限,时间等。在使用该指令的时候还可以加上--chown=<user>:<group>来改变文件的用户、用户组。

ADD更高级的复制文件

ADD指令与COPY的格式、性质基本一致,一样可以改变文件的用户、用户组,但在COPY上增加了一些特定。例如:

  • 源文件路径可以是URL。Docker会自动下载文件放到目标路径,文件权限自动设置为600,修改权限需使用RUN指令修改;下载文件为压缩包时,需要解压缩也一样需要额外的一层RUN指令进行加压缩。但是这样的操作与之前介绍的在构建镜像时只将确定放入镜像的文件放入镜像、同目标操作放在一层等注意事项相驳,所以不如直接在RUN指令中使用wget、curl等工具下载、处理权限、解压缩、清理无用文件更加合理。
  • 源文件为一个压缩包时,ADD指令将会自动解压这个文件到目标文件中;需要解压时比较实用,不需要解压时还是使用COPY。

Docker官方文档中指明尽量使用COPY,因为COPY更加明确,并且ADD指令会令镜像构建缓存失效,从而使镜像构建变得缓慢。所以在自动解压的时候使用ADD,其他情况下使用COPY

CMD容器命令

Post not found: 1.初识Docker中Docker容器(Container)介绍到容器就是进程,那么在启动容器的时候需要指定运行的程序和参数,CMD指令就是来指定容器主进程的启动命令

1
2
3
shell 格式:CMD <命令>
exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

在指令格式上,一般使用exec格式,该格式在解析时会被解释成JSON格式,因此一定要用双引号,而不是单引号。
如果是shell格式实际命令就会被包装为sh -c 格式,如:

1
2
3
CMD echo $HOME
变更为
CMD [ "sh", "-c", "echo $HOME" ]

如果写了多个,只有最后一个生效,CMD 可以被 docker run 之后的参数替换。。
【注意】
容器前台执行与后台执行问题
容器内没有后台服务的概念。
当使用CMD执行后台服务的相关命令时,例如:CMD service nginx start,发现容器容器执行后就立即退出了。这是因为对于容器来说启动程序就是容器应用程序,容器就是为了主进程存在的,主进程退出了,容器也就失去了存在的意义,存而退出了。详细的情况就是CMD service nginx start会被理解为CMD [ "sh", "-c", "service nginx start"],既然主进程是sh,那么当这个service nginx start命令结束了,sh也就结束了,sh主进程退出了,容器自然就退出了。
正确的做法:

1
2
直接执行Nginx可执行文件,并要求前台执行
CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT入口点

ENTRYPOINT目的和CMD指令一样,都是在指定容器中启动程序和参数。ENTRYPOINT在运行时也可以代替,需用–entrypoint指定。当指定了ENTRYPOINT后,CMD就发生了改变,不再是直接运行其命令,而是将CMD的内容作为参数传给ENTRYPOINT指令,实际执行时,将变为<ENTRYPOINT> "<CMD>"。如果写了多个,只有最后一个生效

让镜像变成像命令一样使用

当我们需要查询当前网络IP,那么可以使用CMD实现

1
2
FROM ubuntu:18.04
CMD [ "curl", "https://ip.cn" ] #该网址是查询IP的

使用docker build -t myip .来构建镜像,然后docker run myip 查询当前网络IP
但是当我们需为curl添加一个选项,只能通过docker run myip curl -s https://ip.cn -i这样的方式,这显然不是一种好的方式,可以使用ENTRYPOINT解决这个问题

1
2
FROM ubuntu:18.04
ENTRYPOINT [ "curl", "https://ip.cn" ]

这样就可以docker run myip -i来对启动程序添加选项

应用运行前的准备工作

启动容器就是启动主进程,但有些时候,在启动主进程前,需要一些准备工作。
比如mysql数据库,在mysql服务器运行之前需要做一些数据库配置、初始化的工作。而且希望避免使用root用户去启动服务,从而提高安全性,但在启动服务之前还需要以root身份做一些准备工作,最后切换到用户身份启动服务。或者是除了服务外,其它命令使用root身份执行,方便调试。这种情况就可以写一个脚本,然后放入ENTRYPINT中执行,这个脚本会将CMD传过来的参数作为命令,在脚本最后执行。

1
2
3
4
5
6
7
8
9
官方redis镜像
FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

这个脚本为redis创建了redis用户,在最后指定了ENTRYPOINT为docker-entrypoint.sh脚本

1
2
3
4
5
6
7
8
9
#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
chown -R redis .
exec su-exec redis "$0" "$@"
fi

exec "$@"

这个脚本就是根据CMD的内容来判断,是否切换到reids用户身份启动服务器,否则使用root身份。

ENV设置环境变量

这个指令就是设置环境变量而已,无论后面是什么指令都可以使用这里定义的环境变量

1
ENV VERSION 1.0

ARG构建函数

与ENV一样都是设置环境变量的,但是ARG设置的环境变量将来在容器中是不会存在的,是构建环境的环境变量,但也不能使用ARG保存密码之类的信息,因为在docker history中还是可以看到所有值的。

VOLUME定义匿名卷

1
2
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

容器运行时应该尽量保持容器的存储层不发生操作,对与数据库类的需要保存动态数据的应用,其数据库文件应该保存在卷中(volume)。为了防止运行是用户忘记将动态文件保存所保存的目录挂载为卷,在Dockerfile中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时用户不指定挂载,其应用可以正常运行,不会像存储层写入大量数据。

1
VOLUME /data

这里/data目录就会在运行是自动挂载为匿名卷,任何向/data中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然运行时可以覆盖这个挂载配置docker run -d -v mydata:/data xxxx,这样就把mydata(宿主机)这个命名卷挂载到了 /data (容器内)这个位置。

EXPOSE声明端口

1
EXPOSE <端口1> [<端口2>...]

EXPOSE指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。
EXPOSE是定义容器打算使用什么端口,不会自动在宿主进行端口映射;而容器运行时使用 **-p <宿主端口>:<容器端口>**,是映射宿主端口和容器端口,也就是将容器对应的端口开放给外界访问。

WORKDIR指定工作目录

1
WORKDIR <工作目录路径>

使用WORKDIR指令可以来指定工作目录,以后各层的当前目录级就改为指定的目录,如果目录不存在WORKDIR就会自动创建。
在介绍RUN指令时说过每一个RUN都是一层,所以RUN cd /app这类的指令可能会导致错误,因为这个命令只对当前RUN层有效。

USER指定当前用户

1
USER <用户名>[:<用户组>]

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。但是与WORKDIR不同的是USER指定的用户必须是已经存在的
当希望使用一个用户身份运行一个脚本时不要使用susudo,建议使用gosu

HEALTHCHECK健康检查

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

【选项】

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

在没有 HEALTHCHECK 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。但是当容器无法退出时,容器无法提供服务。所以使用HEALTHCHECK可以真实的反应容器的实际状态。如果写了多个,只有最后一个生效
当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy。
HEALTHCHECK后面命令的返回值决定了健康检查的结果:0→成功、1→失败、2→保留,不使用这个值

1
2
HEALTHCHECK --interval=5s --timeout=3s CMD curl -fs http://localhost/ || exit 1
因为当CMD后的命令失败时会返回一个非0的正整数,所以需要给指定失败时的返回值

可以使用docker container ls查看健康状态。
为了帮助排除故障,健康检查命令的输出(包括stdout、stderr)都会被存储在健康状态里,可以使用docker inspect进行查看。

1
2
# json.tool是一个将json格式数据格式化的工具
docker inspect --format '{{json .State.Health}}' web | python -m json.tool

ONBUILD 为他人做嫁衣裳

格式: ONBUILD <其它指令> 。

1
2
3
4
5
6
7
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的三行将不会被执行,只有将当前镜像作为基础镜像去构建下一个镜像的时候才会被执行。