Dockerfile定制镜像

Dockerfile简介

  • Dockerfile 是一个文本文件,其内包含了一条条的指令Instruction,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
  • 我们可以使用Dockerfile定制镜像,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。

Dockerfile制作

这里我们以制作nginx镜像为例子,首先我们创建一个自定义nginx目录,然后创建Dockerfile文件。

1
2
3
mkdir mynginx 
cd mynginx
touch Dockerfile

然后在Dockerfile进行编辑,替换原始的nginx页面内容

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

关于上面中的命令FROMRUN 后面我们会进行详细讲解。

编译Dockerfile

1
2
3
4
5
6
7
8
9
10
11
mgtv@ubuntu:~/mynginx$ sudo docker build -t nginx:V1 .
[sudo] password for mgtv:
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 62f816a209e6
Step 2/2 : RUN echo '<h1>hello,Dcoker!</h1>' > /usr/share/nginx/html/index.html
---> Running in 24c99f45946b
Removing intermediate container 24c99f45946b
---> 2010f87c2c59
Successfully built 2010f87c2c59
Successfully tagged nginx:V1

  • -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。
  • 最后的.号,其实是在指定镜像构建过程中的上下文环境的目录

查看制作好的ngnix镜像

1
2
3
4
mgtv@ubuntu:~/mynginx$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx V1 2010f87c2c59 2 minutes ago 109MB
ubuntu latest 4c108a37151f 3 weeks ago 64.2MB

运行新的镜像,打开地址:ip:8081即可看到最新修改之后的效果

1
sudo docker run --name ngnix_v1 -d -p 8081:80 nginx:V1

image

Dockerfile 命令

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行
了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而
FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并
且必须是第一条指令。

1
FROM nginx

Docker 还存在一个特殊的镜像,名为
scratch 。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

1
FROM scratch

MAINTAINER 设置镜像作者

我们设置了镜像的作者后,可以通过 docker inspect 命令查看镜像的信息,里面包含有作者信息。

命令格式如下:

1
MAINTAINER  <name>

例如我们创建Dockerfile内容如下:

1
2
FROM nginx
MAINTAINER sutune@qq.com

编译之后查看镜像信息

1
2
3
4
5
6
7
8
9
10
11
docker@default:~/mynginx$ docker build -t nginx:sutune . #编译镜像
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx
---> 98ebf73aba75
Step 2/2 : MAINTAINER sutune@qq.com
---> Running in 23ebc293468b
Removing intermediate container 23ebc293468b
---> 4aaf6df34e72
Successfully built 4aaf6df34e72
Successfully tagged nginx:sutune
docker@default:~/mynginx$ docker inspect nginx:sutune #查看镜像信息

返回内容会比较多,我们截取部分查看,从返回的内容中找到Author对应的值,我们可以看到设置的作者信息已经生效。

1
2
"DockerVersion": "18.06.1-ce",
"Author": "sutune@qq.com",

LABEL 镜像标签

用来标明dockerfile的标签 可以使用Label代替Maintainer 最终都是在docker image基本信息中可以查看,命令格式如下:

1
2
LABEL <key>=<value> <key>=<value> <key>=<value> ...
LABEL version="1.0" description="nginx服务定制" #示例

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

RUN

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

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

  • 如果需要执行多个命令可以使用&&将命令串联起来,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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/r
edis-3.2.5.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-component
s=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

命令的最后--auto-remove $buildDeps添加了清理工作的命令,删除了为了编译构建
所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。

CMD 容器启动命令

  • Docker不是虚拟机,容器就是进程。既然是进
    程,那么在启动容器的时候,需要指定所运行的程序及参数。 CMD指令就是用于
    指定默认的容器主进程的启动命令的。
  • 只可以出现一次,如果写了多个,只有最后一个生效。

命令格式

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

例如我们在上面的Dockerfile中增加下面一行

1
CMD echo "hello world"

然后进行编译后运行

1
2
3
4
5
6
#编译dockerfile
sudo docker build -t nginx:V2 .

#启动容器
sudo docker run -it a1d41110df22
hello world

但是如果在运行时加上新的命令,则Dockerfile中的CMD的命令将会被替代掉。

1
2
sudo docker run -it a1d41110df22  /bin/bash
root@cc469b79c2eb:/#

ENTRYPOINT 入口点

  • ENTRYPOINT的格式和RUN指令格式一样,分为exec格式和shell格
    式,
  • ENTRYPOINT 的 Exec格式用于设置容器启动时要执行的命令及其参数,同时可通过CMD命令或者命令行参数提供额外的参数。ENTRYPOINT 中的参数始终会被使用,这是与CMD命令不同的一点。

我们在上面的例子中增加ENREYPOINT的打印语句,Dockerfile修改如下

1
2
3
4
FROM nginx
RUN echo '<h1>hello,Dcoker!</h1>' > /usr/share/nginx/html/index.html
ENTRYPOINT ["/bin/echo","entrypoint test text"] #增加ENTRYPOINT命令
CMD echo "hello world"

编译执行之后结果如下:

1
2
mgtv@ubuntu:~/mynginx$ sudo docker run -it b99cdc234746 attach_command  
entrypoint test text attach_command

我们可以发现,与CMD命令不同之处在于ENTRYPOINT 中的参数始终会被使用,而 CMD 则被替换没有执行。如果 Docker 镜像的用途是运行应用程序或服务,比如运行一个 MySQL,应该优先使用 Exec 格式的 ENTRYPOINT 指令。

RUN,CMD和ENTRYPOINT都能够用于执行命令,下面是三者的主要用途

  1. RUN命令执行命令并创建新的镜像层,通常用于安装软件包
  2. CMD命令设置容器启动后默认执行的命令及其参数,但CMD设置的命令能够被docker run命令后面的命令行参数替换
  3. ENTRYPOINT配置容器启动时的执行命令(不会被忽略,一定会被执行,即使运行 docker run时指定了其他命令)

COPY 复制文件

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像
内的 <目标路径> 位置。
格式:

1
2
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]

使用案例

1
2
3
4
5
#拷贝单个文件
COPY package.json /usr/src/app/

#拷贝多个文件
COPY hom* /mydir/

ADD 更高级的复制文件

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

  • <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip , bzip2 以及
    xz 的情况下, ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。
  • <源路径>为URL时 ,这种情况下,Docker 引擎会试图去下载这个
    链接的文件放到 <目标路径> 去

ENV 设置环境变量

顾名思义,这个命令就是设置环境变量,格式如下:

1
2
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

我们将Dockerfile增加环境变量命令:

1
2
FROM nginx
ENV version 1.0 #增加环境变量

编译之后以命令交互方式运行,输入命令env即可查看到我们定义的环境变量version=1.0

1
2
3
4
5
6
7
8
9
10
11
12
mgtv@ubuntu:~/mynginx$ sudo docker run -it 81f139934358 /bin/bash 
root@0adc00c8afeb:/# env
HOSTNAME=0adc00c8afeb
version=1.0
NJS_VERSION=1.15.6.0.2.5-1~stretch
NGINX_VERSION=1.15.6-1~stretch
PWD=/
HOME=/root
TERM=xterm
SHLVL=1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env

通过ENV定义的环境变量,可以在dockerfile被后面的所有指令中使用

1
2
3
FROM nginx
ENV version 1.0 #增加环境变量
CMD ehco $version #打印设置的环境变量

上面的$version就是对环境变量的引用。编译运行之后即可看到打印的环境变量。

1
2
mgtv@ubuntu:~/mynginx$ sudo docker run -it 44c98dafbcb0 
1.0

ARG 构建参数

  • ARG指令定义了一个变量,能让用户可以在构建期间使用docker build命令和其参数--build-arg 对这个变量赋值。
  • ARG和ENV的效果一样,都是设置环境变量。所不同的是ARG所设置的
    构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。
    定义格式如下:
1
ARG <name>[=<default value>]

我们在Dockerfile中定义如下:

1
2
3
4
FROM nginx
ENV version 1.0
ARG user #定义变量user
RUN echo $user #编译时打印变量值

然后我们编译镜像时传入对应的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mgtv@ubuntu:~/mynginx$ sudo docker build --build-arg user=sutune  -t nginx:arg .
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM nginx
---> 62f816a209e6
Step 2/4 : ENV version 1.0
---> Running in d9dc0ee04b10
Removing intermediate container d9dc0ee04b10
---> e14840e61523
Step 3/4 : ARG user
---> Running in 9cdb854fd341
Removing intermediate container 9cdb854fd341
---> b77129c69319
Step 4/4 : RUN echo $user
---> Running in b92e0d7fb876
sutune
Removing intermediate container b92e0d7fb876
---> 1cd755773a00
Successfully built 1cd755773a00
Successfully tagged nginx:arg

从上面的执行结果可以看出,在Step 4/4时打印了我们传入的参数值sutune
Docker有一组预定义ARG变量,您可以ARG在Dockerfile中没有相应的指令的情况下使用它们。

  • HTTP_PROXY
  • http_proxy
  • HTTPS_PROXY
  • https_proxy
  • FTP_PROXY
  • ftp_proxy
  • NO_PROXY
  • no_proxy

VOLUME 匿名卷

  • Docker可以将主机上的某个目录与容器的某个目录(称为挂载点、或者叫卷)关联起来,容器上的挂载点下的内容就是主机的这个目录下的内容,这类似linux系统下mount的机制。
  • 这样的话,我们修改主机上该目录的内容时,不需要同步容器,对容器来说是立即生效的。 挂载点可以让多个容器共享。
  • 命令格式如下:
1
2
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

例如我们在Dockerfile中进行如下配置:

1
2
FROM nginx
VOLUME /data1

上面我们使用VOLUME命令创建了一个目录data1,这个目录会在容器中创建,并自动关联到主机的目录中。编译镜像之后,我们运行容器,在容器中可以看到容器目录中创建了一个data1的目录。

1
2
3
mgtv@ubuntu:~/mynginx$ sudo docker run -it 469f5887efc5 /bin/bash
root@413e3d61e34b:/# ls
bin boot data1 dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

那么这个卷对应关联的主机是哪个目录呢,我们可以使用docker inspect命令来查看。

1
2
3
4
5
6
mgtv@ubuntu:~/mynginx$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
413e3d61e34b 469f5887efc5 "/bin/bash" 4 minutes ago Exited (127) 8 seconds ago mystifying_nobel


},

首先查找到对应容器的的id,然后根据id查看容器详细信息,返回的内容会比较多,我们省略部分信息,找到Mounts对应的内容我们可以看到Source对应的值 就是在主机中与我们创建的卷目录/data1关联的目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sudo docker inspect 413e3d61e34b 

...

"Mounts": [
{
"Type": "volume",
"Name": "cc9c111840be511464bd165387b4f28b25d56561fa7c95d4a4d28d9faa441b7c",
"Source": "/var/lib/docker/volumes/cc9c111840be511464bd165387b4f28b25d56561fa7c95d4a4d28d9faa441b7c/_data",
"Destination": "/data1",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],


...

EXPOSE 端口暴露

  • EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不
    会因为这个声明应用就会开启这个端口的服务。
  • 在 Dockerfile 中写入这样的声明有
    两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映
    射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P
    时,会自动随机映射 EXPOSE 的端口。docker run -p(小写)是手动映射。
  • 要将EXPOSE和在运行时使用-p <宿主端口>:<容器端口>区分开来。 -p
    映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访
    问,而EXPOSE仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行
    端口映射。

我们在Dockerfile中定义如下:

1
2
3
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
EXPOSE 80 #设置端口暴露

然后进行编译,编译之后运行容器时我们设置参数-P 进行随机映射端口

1
2
3
4
5
6
mgtv@ubuntu:~/mynginx$ sudo docker run  --name nginx_expose  -d -P nginx:expose 
d42e313658f8bc806fd633ff41ecfefd12d1a07a11cc67d4382a3e1aa962a3f5

mgtv@ubuntu:~/mynginx$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d42e313658f8 nginx:expose "nginx -g 'daemon of…" 10 seconds ago Up 9 seconds 0.0.0.0:32768->80/tcp nginx_expose

从上面的运行结果我们可以看到随机映射的端口为32768,我们可以输入宿主机的ip+32768即可访问。

WORKDIR 指定目录

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

1
WORKDIR <工作目录路径>

我们将Dockerfile编写如下:

1
2
3
FROM nginx
WORKDIR /sutune
RUN mkdir nginx

上面的命令含义是先创建一个sutune的根目录,然后继续在这个根目录下创建一个文件夹nginx编译执行之后我们运行镜像

1
2
3
4
5
6
7
8
docker@default:~/mynginx$ docker run -it nginx:latest /bin/bash
root@41df29b87213:/sutune# pwd
/sutune
root@41df29b87213:/sutune# ls
nginx
root@41df29b87213:/sutune# cd nginx
root@41df29b87213:/sutune/nginx# pwd
/sutune/nginx

从上面的运行结果我们可以看到我们在容器内部创建了对应的文件路径/sutune/nginx
设置目录之后后续的命令都会在此目录基础之上运行操作。

USER 指定用户

USER指令用于指定容器执行程序的用户身份,默认是root用户。在docker run 中可以通过 -u 选项来覆盖USER指令的设置。切换的用户必
须是事先建立好的,否则无法切换。

1
docker run -i -t -u sutune nginx:latest /bin/bash

我们在Dockerfile创建如下命令,首先创建用户user然后使用USER命令进行账户切换

1
2
3
FROM nginx
RUN groupadd -r sutune && useradd -r -g sutune sutune #创建用户
USER sutune #切换用户

编译镜像之后运行,我们可以发现默认的root用户被替换成了我们指定的用户sutune

1
2
docker@default:~/mynginx$ docker run -it 4b2503a3abdb /bin/bash
sutune@d982cb72946d:/$

ONBUILD 镜像触发器

  • ONBUILD指令可以为镜像添加触发器。其参数是任意一个Dockerfile 指令。当我们在一个Dockerfile文件中加上ONBUILD指令,该指令对利用该Dockerfile构建镜像(比如为A镜像)不会产生实质性影响。
  • 但是当我们编写一个新的Dockerfile文件来基于A镜像构建一个镜像(比如为B镜像)时,这时构造A镜像的Dockerfile文件中的ONBUILD指令就生效了,在构建B镜像的过程中,首先会执行ONBUILD指令指定的指令,然后才会执行其它指令。
  • 利用ONBUILD指令,实际上就是相当于创建一个模板镜像,后续可以根据该模板镜像创建特定的子镜像,需要在子镜像构建过程中执行的一些通用操作就可以在模板镜像对应的dockerfile文件中用ONBUILD指令指定。 从而减少dockerfile文件的重复内容编写。

注意:如果是再利用B镜像构造新的镜像时,那个ONBUILD指令就无效了,也就是说只能再构建子镜像中执行,对孙子镜像构建无效。其实想想是合理的,因为在构建子镜像中已经执行了,如果孙子镜像构建还要执行,相当于重复执行,这就有问题了。

ONBUILD 命令格式

1
ONBUILD <其它指令>

首先我们创建一个Dockerfile,我们的目的是创建一个文件夹mydir

1
2
FROM nginx
ONBUILD RUN mkdir mydir

然后进行编译再运行容器,我们会发现在当前镜像运行的容器中没有创建对应的文件夹。

1
2
3
4
docker@default:~/mynginx$ docker build -t nginx_mydir . 
docker@default:~/mynginx$ docker run -it nginx_mydir /bin/bash
root@e7dd926cba73:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

我们继续基于刚刚创建的镜像nginx_mydir创建新的镜像nginx_v1

1
FROM nginx_mydir

编译运行镜像,我们可以看到此时创建了对应的文件夹mydir

1
2
3
4
5
6
7
8
9
10
docker@default:~/onbuild$ docker build -t nginx_v1 .

docker@default:~/onbuild$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx_v1 latest c52a02c179b2 11 seconds ago 109MB
nginx_mydir latest 5c02392cb5e2 10 minutes ago 109MB

docker@default:~/onbuild$ docker run -it nginx_v1 /bin/bash
root@166b0a1473fe:/# ls
bin boot dev etc home lib lib64 media mnt mydir opt proc root run sbin srv sys tmp usr var

HEALTHCHECK 健康检查

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

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

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

HEALTHCHECK 支持下列选项:

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

小结

最后我们以一张图来小结各个命令的作用加深理解。

image

参考资料