Jusene's Blog

利用Dockerfile构建Docker镜像

字数统计: 3.3k阅读时长: 12 min
2017/08/31 Share

为什么使用Dockerfile

常见的创建自己想要的镜像有两种:

  1. 一种是通过docker commit将修改好的镜像提交为本地的新镜像,我们都知道docker的存储是分层存储,只有最上层才是可读写的,而我们的普通操作往往会调用许多文件或者打开许多文件,导致这些文件发生改变,而改变了就需要写入这层存储,将大量的无关内容添加为新的镜像内容,这会造成镜像文件极其臃肿,可以通过docker diff来看下普通操作发生了多少的变化。而且通过这种方式提交上去的镜像出了提交本人无人知道做了什么操作,极其不容易管理。所以docker commit除了学习外或者入侵取证外都不建议直接在生产环境使用。
  2. 这是大家都建议的方法,通过Dockerfile来构建docker镜像,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们把每一层的修改、安装、构建、操作都写入一个脚本来运行,这个脚本来构建、定制镜像,那么docker commit的问题都可以得到解决,而这个脚本文件我们就称为Dockerfile。

Dockerfile 指令集合

Dockerfile指令选定基础镜像、安装必要程序、复制配置文件和数据文件、自动运行服务以及暴露的端口等。

FROM指令

用于制定镜像时所需要的基础镜像,基础镜像是必须指定的,因此一个Dockerfile中FROM是必备的指令,并且必须是第一条指令。Docker Hub上有许多高质量的官方镜像,我们可以拿来定制自己的专有镜像。

语法格式:

1
FROM <image>:<tag> 或者
2
FROM <image>@<digest>

除了基础镜像外,Docker还存在一个特殊的镜像,名为scratch。这个镜像是虚拟的概念,它表示一个空白的镜像。

1
FROM scratch

以scratch为基础的镜像,意味着不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在,这种方式不常见,但是如果在Linux下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的库都已经在可执行文件里了,因此可以使用scratch来使镜像更加小巧。

MAINTAINER指令

用于提供信息的指令,用于让作者提供本人的信息;不限制其出现的位置,但建议跟在FROM之后。

语法格式:

1
MAINTAINER <author's detail>

RUN指令

用于指定docker build过程中要运行的命令,RUN是定制镜像时最常用的指令之一。

语法格式:

1
RUN command
2
RUN ["<executed>","<params>","<param2>",...]

注意:Dockerfile中每一个指令都会建立一层,RUN也不例外,每一个RUN的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束commit这一层修改,构成新的镜像。

错误的写法:

1
FROM centos:latest
2
3
RUN yum install -y httpd
4
RUN yum clean all
5
RUN echo "Hello Docker!" > /var/www/html/index.html

这样我们已经建立了三层境界,这样的构造还是会造成镜像异常臃肿,所以这是完全没意义的,而且UnionFS是有最大层数限制的,AUFS曾经是最大不得超过42层,现在是不得超过127层。

正确的写法:

1
FROM centos:latest
2
3
RUN yum install -y httpd && \
4
    yum clean all && \
5
    echo "Hello Docker!" > /var/www/html/index.html

写Dockerfile必须提醒自己,这不是写shell,而是在定义每层如何构建。

COPY指令

COPY 指令将从构建上下文目录中文件或目录复制到新的一层的镜像内目标位置。

语法格式:

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

src :要复制的源文件或目录,支持通配符。
dest :目标路径,正在创建的镜像文件的文件系统路径;建议使用绝对路径,否则,则相对于WORKDIR而言

注意:

  • 使用COPY指令,源文件的各种元数据都会保留,比如读写执行的权限等。
  • src的路径必须是build上下文的路径。
  • 如果dest事先不存在,它将自动创建,包括其父目录
  • src如果是目录,递归复制会进行;如果有多个src,此时dest必须是目录,而且得以/结尾

ADD指令

类似COPY指令,但是在COPY基础上增加了一些功能。

语法格式:

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

注意:

  • 源路径可以是一个URL,这种情况下,Docker会下载文件到目标路径下,下载后文件的权限自动设置600,如果这种权限不对还得RUN修改权限。
  • 源路径是一个tar压缩文件,ADD指令将会自动解压缩这个压缩文件到目标路径中。
  • 其他的使用方法与COPY指令一致。

CMD指令

CMD指令与RUN命令相似,不同的是,CMD是docker run时启动容器的命令,指定的命令是容器启动时的默认要运行的程序,程序运行结束,容器就结束,不过CMD指令指定的程序可被docker run命令后面指定要运行的程序所覆盖。

语法格式:

1
CMD command
2
CMD ["<executed>","<param1>","<param2>",...]
3
CMD ["<parm1>","<param2>",...]                在指定了ENTRYPOINT指令后,用CMD指定的具体参数

注意:如果Dockerfile存在多个CMD指令,仅最后一个生效。

ENTRYPOINT指令

类似于CMD指令,但其不会被docker run的命令行参数指定的指令所覆盖,而且这些命令行参数会被当作参数送给ENTRYPOINT指令指定的程序。但是,如果运行docker run时使用–entrypoint选项,此选项的参数可当作要运行的程序覆盖ENTRYPOINT指令指定的程序。

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

ENV指令

定义环境变量,此变量可被docker-file文件的其他指令调用,调用格式$variable_name或${variable}。

语法格式:

1
ENV <key> <value>  一次定义一个变量
2
ENV <key>=<value>  一个可定义多个变量,如果<value>中有空白字符,要使用\字符进行转译或加引号

ENV定义的环境变量在镜像运行的整个过程中一直存在,因此,可以使用inspect命令查看,甚至也可以在docker run启动此镜像时,使用–env或-e选项来修改指定变量的值。

ARG指令

与ENV相似的效果,都是设置环境变量。所不同的是,ARG所设置的环境变量,在将来的容器运行时是不会存在这些环境变量的,但是也不要使用ARG保存密码之类的信息,因为docker histaory还是可以看到所有值的。

语法格式:

1
ARG <key> <value>
2
ARG <key>=<value>

ARG指定设置的变量可以在docker build中使用–build-arg key=value来覆盖。

VOLUME指令

容器的数据持久化,我们可以在用户没有运行挂载数据卷的情况下,创建一个默认的数据卷用来创建数据,不会向容器的存储层写入大量数据。

语法格式:

1
VOLUME mountpoint
2
VOLUME ["mountpoint","mountpoint",...]

注意:如果我们运行了挂载数据卷,这个数据卷将会覆盖这个匿名的数据卷,其匿名的数据卷的我们将不可见,这是为了防止删除容器会导致数据丢失的方式。

EXPOSE指令

申明容器运行提供服务的端口,这只是一个申明,我们还是需要在容器运行的时候指定-p或-P来开启端口映射,申明的好处是我们可以很清楚的看见容器启动了什么端口了提供服务。

语法格式:

1
EXPOSE <port>[/protocol] [<port>/<protocol>]

protocol为tcp或udp,tcp为默认的协议。

WORKDIR指令

使用WORKDIR指令可以指定工作目录,为所有的RUN/CMD/ENTRYPOINT/COPY/ADD指定工作目录。

语法格式:

1
WORKDIR <dirpath>

注意:WORKDIR可以出现多次,也可使用相对路径,此时表示相对于前一个WORKDIR指令指定的路径;WORKDIR还可以调用由ENV或ARG构建的环境变量的值。

还需要注意的是,对于Dockerfile新手来说RUN命令常常当作shell来写,在shell中连续两行的是在同一个执行环境,因此前一个命令修改的内存状态会影响后一个命令,而在Dockerfile中,两个RUN命令的执行环境根本不同,是两个完全不同的构建容器,所以会出现跟自己逻辑不对称的事情发生,在得以需要利用两个RUN命令的时候,确保WORKDIR是容器需要运行的工作目录中。

USER指令

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

语法格式:

1
USER <USERNAME>

注意:USERNAME必须是容器/etc/passwd中存在的用户,不然可能会出错。

如果需要短暂切换到某一用户执行命令,不要使用su或sudo,这些都需要麻烦的配置,而且tty缺失的环境下容易出错,建议使用gosu。

1
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64"

HEALTHCHECK指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,在没有HEALTHCHECK指令之前,Docker引擎只能通过容器内的主进程是否退出判断容器是否状态异常。很多情况下这是可行的,但是如果程序进入死锁或者死循环的情况下,程序并不会退出,但是容器已经无法使用了。Docker 1.12后,Docker提供了HEALTHCHECK指令,当一个镜像指定HEALTHCHECK指令后,初始状态为starting,在HEALTHCHECK指令检查成果后变为healthy,如果连续一定次数失败,则变为unhealthy。

HEALTHCHECK 支持下列选项:

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

和 CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。在 HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

example:

1
FROM nginx
2
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
3
HEALTHCHECK --interval=5s --timeout=3s \
4
  CMD curl -fs http://localhost/ || exit 1

ONBUILD指令

定义触发器,当前dockerfile构建出的镜像被用作基础镜像去构建其他镜像时,ONBUILD指令指定的操作才会被执行。

语法格式:

1
ONBUILD <INSTRUCTION>

注意:ONBUILD不能自我嵌套,不会运行FROM,MAINTAINER

example:

构建基础镜像:

1
FROM centos:lates
2
MAINTAINER Jusene
3
4
RUN yum install -y httpd && \
5
    yum clean all && \
6
7
WORKDIR /var/www/html
8
ONBUILD COPY index.html index.html
9
10
CMD ["httpd","-f","/etc/httpd/conf/httpd.conf","-DFOREGROUND"]
11
12
构建出基础镜像httpd:v1
13
~]# docker build -f Dockerfile . -t httpd:v1

我们在调用基础镜像的时候,需要在docker build的上下问中放入index.html,我们就可以根据httpd:v1的基础镜像构建出我们想要的镜像。

1
FROM httpd:v1
2
3
构建出最终的镜像
4
~]# docker build -f Dockerfile-1 . -t httpd:v2

其他生成的方法

Dockerfile是最标准的生成镜像的方法,由于各种特殊需求和历史原因,还存在一些特别的方法来生成镜像。

export & import

  • export导出容器的文件系统到一个tar包
    docker export [OPTIONS] CONTAINER
  • import从一个tar包中倒入成一个镜像
    docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]
1
~]# docker export centos7 > centos7.tar
2
~]# docker import centos7.tar httpd:v0
3
~]# docker history httpd:v0
4
IMAGE               CREATED             CREATED BY          SIZE                COMMENT
5
09e7250f1e0e        46 seconds ago                          225.2 MB            Imported from -

save & load

  • save保存一个或多个镜像到一个tar包
    docker save [OPTIONS] IMAGE [IMAGE…]
  • load从一个tar包活着标准输入加载一个镜像
    docker load [OPTIONS]
1
~]# docker save httpd:v0 | gzip > httpdv0.tar.gz
2
~]# docker load -i httpdv0.tar.gz
3
Loaded image: httpd:v0
4
5
镜像远程迁移
6
docker save <镜像名> | bzip2 | pv | ssh <用户名>@<主机名> 'cat | docker load'
CATALOG
  1. 1. 为什么使用Dockerfile
  2. 2. Dockerfile 指令集合
    1. 2.1. FROM指令
    2. 2.2. MAINTAINER指令
    3. 2.3. RUN指令
    4. 2.4. COPY指令
    5. 2.5. ADD指令
    6. 2.6. CMD指令
    7. 2.7. ENTRYPOINT指令
    8. 2.8. ENV指令
    9. 2.9. ARG指令
    10. 2.10. VOLUME指令
    11. 2.11. EXPOSE指令
    12. 2.12. WORKDIR指令
    13. 2.13. USER指令
    14. 2.14. HEALTHCHECK指令
    15. 2.15. ONBUILD指令
  3. 3. 其他生成的方法
    1. 3.1. export & import
    2. 3.2. save & load