Dockerfile基本用法

626 阅读7分钟

1.概述      

了解dockerfile的都知道,docker能够自动的从dockerfile文件里面读取指令进行镜像的建立。当我们把dockerfile写好以后通过docker build命令就可以开始进行镜像的建立了,建立好的这个镜像就可以在任何使用docker进行发布了。由于现在都使用docker化部署,对于应用开发人员最重要的工作之一就是通过编写dockerfile来建立自己的部署镜像。

2.基本用法

docker build命令建立镜像是通过dockerfile和一个指定关联环境。这个关联环境可能来之一个本地的目录或者一个指定的url。url是一个git库。一个关联环境是递归使用的,如果是本地指定的一个目录,那么就是递归的包含所有子目录。如果是url指定的一个git库,那么也包含git库的子模块。下面是一个简单的从当前目录进行镜像建立的命令:

$ docker build .
Sending build context to Docker daemon 6.51 MB ...

(注意那个”.”,就是代表当前目录)。这个命令是通过docker daemon运行的,而不是docker cli。Build的第一件事情就是将所有关联环境(递归)发送到daemon。在通常情况下都是建立一个空的目录,然后把dockerfile文件放在这个目录,在把需要的其他文件拷贝到这个目录,这样保证了建立镜像的干净。

为了提高build的性能,我们在build的时候可以通过建立一个文件名为.dockerignore来排除不需要的文件和目录(在关联环境的目录)。

通常情况下Dockerfile的文件名叫Dockerfile,放在关联环境的根目录下即可,但是也可以通过-f选项指定文件系统任意位置的文件作为dockerfile。

$ docker build -f /path/to/a/Dockerfile .

还可以通过指定-t选项来指定一个tag给镜像,如下:

$ docker build -t shykes/myapp .

也可以通过连续多个-t选项指定多个tag给镜像,如下:

$ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest .

docker daemon从dockerfile里面一条一条的获取指令,然后执行。在最终输出你的镜像id之前,如果需要就会把每一条指令执行结果提交给新的镜像。docker daemon会在最后自动清理所有发送给它的关联环境。每一条指令都是独立运行的,并且引发一个新镜像被创建。所以前面的任何一条指令都不会对后面的指令产生影响。

3.指令格式

dockerfile中每一条指令的格式如下:

# Comment
INSTRUCTION arguments

指令(INSTRUCTION)是不区分大小写的,不过通常的惯例指令都是大写,这样便于和后面的参数进行区分。Docker运行指令是按照dockerfile文件里面的顺序进行的,第一条指令必须是FROM,通过FROM指定一个你构建新镜像的最基础的镜像,就是你必须要基于一个基础镜像来构建你的新镜像。Docker会把以#开头的一行作为注释,除非是一条有效的解析器指令。#出现在一行中任何其他位置都会被当做是参数对待。

4.解析器指令

解析器指令是可选的,会影响dockerfile文件后面指令的处理方式。解析器指令不会增加新的层。也不会出现在构建步骤中。解析器指令的格式被写作类似注释的方式,如 # directive=value 。所以在上面解析指令格式的时候,强调了如果是一条有效的解析器指令不会当做是注释,只是和注释格式相同。一个解析器指令只能被使用一次。一旦出现注释、空行或者构建指令被处理过,docker就不会再寻找解析器指令了,也就是说解析器指令必须要出现在dockerfile的最前面。

前面说了FROM是第一条构建指令,但是FROM之前还可以有注释,注释之前还可以有空行,但是解析器指令必须要出现在这些所有的前面。如在shell脚本,第一行也是类似的解析器指令,指示使用什么版本的shell进行执行,语法也很相似,都是类似注释的方式。解析器指令也是不区分大小写的,但是为了方便区分建议使用小写字母。

为了和后面的构建指令区分,通常也在解析器指令后面添加一个空行。同样解析器指令也不支持续行符。为了帮助理解上面的规则,举一些例子来说明哪些是无效的解析器指令格式:

# direc \
tive=value

上面的解析器指令无效是因为不支持续行符。

# directive=value1
# directive=value2
FROM ImageName

上面的解析器指令无效是由于出现两次。

FROM ImageName
# directive=value

上面这个会被当成注释,因为出现在构建指令之后。

# unknowndirective=value
# knowndirective=value

上面这个第一条是不知道的解析器指令,后面虽然是知道的解析器指令,但是因为前面的被当成注释了,所以后面的也会被当成注释。

目前只支持一条解析器指令,如下:

  • escape

escape指令格式如下:

# escape=\ (backslash) or  # escape=` (backtick)

escape指令设置一个字符在dockerfile文件中作为转义字符使用。如果不设置,默认的转义字符是\。转义字符在一行中被作为转义字符,在一行的末尾用作续行符。转义字符很好理解,当有特殊的字符需要出现在参数里面就需要进行转义,作为续行符是帮助把一条构建指令可以写成多好,方便查看。如果没有这个功能,docker会把每一行当成一个构建指令,那么很长的指令就只能写一行,肯定会不方便查看。类似我们很多编程代码里面也有,例如c语言中的宏定义。

5.环境变量替换

环境变量能够在适当的构建指令中被当做变量使用(环境变量在dockerfile中本身可以使用env指令定义)。环境变量的应用可以使用或者variablename或者{variable_name}。两种形式是等效的,第二种情况主要用于那种需要和字符串连接的情况(没有空格区分)。例如${foo}_bar。第二种形式还可以实现下面的功能:

${variable:-word}:指示当variable变量存在是就使用变量的值,如果不存在就使用word作为值;
${variable:+word}:指示当variable变量存在时使用word作为结果值,如果不存在就使用空字符串。

上面的word可以是任意的字符串,甚至本身又是一个环境变量。另外,通过转义字符可以让或者variablename或者{variable_name}都当成字面量而不是替换环境变量的值,相当于使用\转义了。

环境变量的使用举例如下(#后面的注释是解释):

FROM busybox
ENV foo /bar
WORKDIR ${foo}    # WORKDIR /bar
ADD. $foo         # ADD . /bar
COPY \$foo /quux  # COPY $foo /quux

环境变量支持在dockerfile中支持如下构建指令使用:

  • ADD
  • COPY
  • ENV
  • EXPOSE
  • LABEL
  • USER
  • WORKDIR
  • VOLUME
  • STOPSIGNAL
  • ONBUILD (结合上面所列出的指令,1.4版本之前不支持)

后面详细解说这些指令使用情况。如果env指令在一行有多个环境变量定义,那么本行使用的环境变量都是本行以前的,本行定义的不会影响本行后面的结果。例如:

ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc

上面三条指令以后,def的值是hello,而不是bye,ghi的值是bye。

6. .dockerignore文件

docker cli(客户端)发送关联上下文环境给docker daemon之前,会在关联上下文环境的根目录寻找 .dockerignore文件。如果存在会修改发送给docker daemon的上下文环境所匹配的文件和目录,把这些排除在发送之外。这个可以有效的避免发送不需要的文件和目录,同时也不需要修改本地的关联环境目录。如果只需要其中某一个文件,可以使用ADD和COPY指令。匹配规则是使用go语言的匹配语法。

如果.dockerfile的第一列是#同样被当做注释处理,不会作为和docker客户端的排除规则。一个例子如下:

# comment
/temp
*/*/temp
temp?

上面第一行被忽略,第二行排除根目录以及所有子目录下所有以temp开头的文件,第三行排除根目录下的二级子目录以及所有二级子目录的子目录所有以temp开头文件;第四行排除所有根目录下以temp开头的文件。这里所说的是构建镜像关联环境的根目录,通常就是构建指令执行的目录。

7. 其他构建指令解析

  1. FROM
FROM <image> 
或 FROM <image>:<tag> 
或 FROM <image>@<digest> 

在Dockerfile中第一条非注释INSTRUCTION非解析器指令一定是FROM,它决定了以哪一个镜像作为基准,首选本地是否存在,如果不存在则会从公共仓库下载(当然也可以使用私有仓库的格式)。

  1. RUN
RUN <commnad> 或 
RUN ["executable", "param1", "param2"]

RUN指令会在当前镜像的顶层执行任何命令,并commit成新的(中间)镜像,提交的镜像会在后面继续用到。上面看到RUN后的格式有两种写法。shell格式,相当于执行:

/bin/sh -c "<command>":
RUN apt-get install vim -y

exec格式,不会触发shell,所以$HOME这样的环境变量无法使用,但它可以在没有bash的镜像中执行,而且可以避免错误的解析命令字符串:

RUN ["apt-get", "install", "vim", "-y"] 
或 RUN ["/bin/bash", "-c", "apt-get install vim -y"] 与shell风格相同
  1. ENTRYPOINT

ENTRYPOINT命令设置在容器启动时执行命令,如果有多个ENTRYPOINT指令,那只有最后一个生效。有以下两种命令格式:

ENTRYPOINT ["executable", "param1", "param2"] 数组/exec格式,推荐
或 ENTRYPOINT command param1 param2   shell格式
比如:
docker run -i -t --rm -p 80:80  nginx 

使用exec格式,在docker run <image>的所有参数,都会追加到ENTRYPOINT之后,并且会覆盖CMD所指定的参数(如果有的话)。当然可以在run时使用--entrypoint来覆盖ENTRYPOINT指令。 使用shell格式,ENTRYPOINT相当于执行/bin/sh -c <command..>,这种格式会忽略docker run和CMD的所有参数。 以推荐使用的exec格式为例:我们可以使用ENTRYPOINT来设置基本不会变化的命令,用CMD来设置其它的可能改变的默认启动命令或选项(docker run会覆盖的)。

FROM ubuntu
ENTRYPOINT ["top", "-b"] 
CMD ["-c"]

docker build -t registry.tp-link.net:8000/ubuntu:dockerfile_test . 运行

$ docker run -it --rm --name test 44f178c416b0 -H
这里的top后的选项会追加到上面的ENTRYPOINT,同时会覆盖CMD的,所以实际相当于执行top -b -H,没有-c:
top - 04:32:07 up 10 days, 11:27, 0 users, load average: 0.01, 0.03, 0.00
Threads: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.1 us, 0.1 sy, 0.0 ni, 99.7 id, 0.2 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem: 4056784 total, 3749188 used, 307596 free, 209372 buffers
KiB Swap:       0 total,       0 used,       0 free. 571388 cached Mem
PID USER     PR NI   VIRT   RES   SHR S %CPU %MEM     TIME+ COMMAND
1 root     20 0 19688 1208   940 R 0.0 0.0   0:00.01 top

如果在使用的docker版本在v1.3及以上,则可以使用docker exec继续在容器中验证,看到完整的top命令docker exec -it test ps aux

  1. CMD
CMD ["executable","param1","param2"] (数组/exec格式) 
CMD ["param1","param2"] (as default parameters to ENTRYPOINT) 
CMD command param1 param2 (shell格式)

一个Dockerfile里只能有一个CMD,如果有多个,只有最后一个生效。CMD指令的主要功能是在build完成后,为了给docker run启动到容器时提供默认命令或参数,这些默认值可以包含可执行的命令,也可以只是参数(此时可执行命令就必须提前在ENTRYPOINT中指定)。

它与ENTRYPOINT的功能极为相似,区别在于如果docker run后面出现与CMD指定的相同命令,那么CMD会被覆盖;而ENTRYPOINT会把容器名后面的所有内容都当成参数传递给其指定的命令(不会对命令覆盖)。

另外, CMD还可以单独作为ENTRYPOINT的所接命令的可选参数。CMD与RUN的区别在于,RUN是在build成镜像时就运行的,先于CMD和ENTRYPOINT的,CMD会在每次启动容器的时候运行,而RUN只在创建镜像时执行一次,固化在image中。

举例1:

Dockerfile:           
    CMD ["echo CMD_args"] 
运行:
    docker run <image> echo run_arg 
结果:
    输出run_arg

因为echo run_arg覆盖了CMD。如果run后没有echo run_arg,则输出CMD_args。

举例2:

Dockerfile:
    ENTRYPOINT ["echo", "ENTRYPOINT_args"] 
运行:
    docker run <image> run_arg 
结果:
    输出 ENTRYPOINT_args run_arg

因为echo run_arg追加到ENTRYPOIINT的echo后面了。如果在ENTRYPOINT后再加入一行CMD ["CMD_args"],则结果依旧,除非去掉run后的所有参数。当出现ENTRYPOINT指令时CMD指令只可能(当ENTRYPOINT指令使用exec方式执行时)被当做ENTRYPOINT指令的参数使用,其他情况则会被忽略。

  1. EXPOSE

EXPOSE指令告诉容器在运行时要监听的端口,但是这个端口是用于多个容器之间通信用的(links),外面的host是访问不到的。要把端口暴露给外面的主机,在启动容器时使用-p选项。

示例:

# expose memcached(s) port 
EXPOSE 11211 11212
  1. ADD
ADD <src>... <dest>

将文件<src>拷贝到container的文件系统对应的路径<dest>下。

<src>可以是文件、文件夹、URL,对于文件和文件夹<src>必须是在Dockerfile的相对路径下(build context path),即只能是相对路径且不能包含../path/。

<dest>只能是容器中的绝对路径。如果路径不存在则会自动级联创建,根据你的需要是<dest>里是否需要反斜杠/,习惯使用/结尾从而避免被当成文件。

示例:

支持模糊匹配 
ADD hom* /mydir/       # adds all files starting with "hom" 
ADD hom?.txt /mydir/   # ? is replaced with any single character 
ADD requirements.txt /tmp/ RUN pip install /tmp/requirements.txt 
ADD . /tmp/

另外ADD支持远程URL获取文件,但官方认为是strongly discouraged,建议使用wget或curl代替。ADD还支持自动解压tar文件,比如ADD trusty-core-amd64.tar.gz / 会线自动解压内容再COPY到在容器的/目录下。ADD只有在build镜像的时候运行一次,后面运行container的时候不会再重新加载,也就是你不能在运行时通过这种方式向容器中传送文件,-v选项映射本地到容器的目录。

  1. COPY

Same as ‘ADD’ but without the tar and remote url handling.

COPY的语法与功能与ADD相同,只是不支持上面讲到的<src>是远程URL、自动解压这两个特性,但是Best Practices for Writing Dockerfiles建议尽量使用COPY,并使用RUN与COPY的组合来代替ADD,这是因为虽然COPY只支持本地文件拷贝到container,但它的处理比ADD更加透明,建议只在复制tar文件时使用ADD,如ADD trusty-core-amd64.tar.gz /

  1. ENV

用于设置环境变量:ENV <key> <value>。 设置环境变量后,后续的RUN命令都可以使用,当运行生成的镜像时这些环境变量依然有效,如果需要在运行时更改这些环境变量可以在运行docker run时添加-env <key>=<value>参数来修改。

  1. VOLUME

VOLUME指令用来在容器中设置一个挂载点,可以用来让其他容器挂载以实现数据共享或对容器数据的备份、恢复或迁移。

VOLUME只是指定了一个目录,用以在用户忘记启动时指定-v参数也可以保证容器的正常运行。

比如mysql,当用户启动时没有指定-v,然后容器被删除时数据也被删除,那样生产上是会出大事故的,所以mysql的dockerfile里面就需要配置volume,这样即使用户没有指定-v,容器被删后也不会导致数据文件被删除,进而能够恢复数据。

  1. WORKDIR

WORKDIR指令用于设置Dockerfile中的RUN、CMD和ENTRYPOINT指令执行命令的工作目录(默认为/目录),该指令在Dockerfile文件中可以出现多次,如果使用相对路径则为相对于WORKDIR上一次的值,

示例:

WORKDIR /a
WORKDIR b
RUN pwd

最终输出的当前目录是/a/b。(RUN cd /a/b,RUN pwd是得不到/a/b的)

  1. ONBUILD

ONBUILD指令用来设置一些触发的指令,用于在当该镜像被作为基础镜像来创建其他镜像时(也就是Dockerfile中的FROM为当前镜像时)执行一些操作,ONBUILD中定义的指令会在用于生成其他镜像的Dockerfile文件的FROM指令之后被执行,上述介绍的任何一个指令都可以用于ONBUILD指令,可以用来执行一些因为环境而变化的操作,使镜像更加通用。

注意:

  1. ONBUILD中定义的指令在当前镜像的build中不会被执行。
  2. 可以通过查看docker inspect 命令执行结果的OnBuild键来查看某个镜像ONBUILD指令定义的内容。
  3. ONBUILD中定义的指令会当做引用该镜像的Dockerfile文件的FROM指令的一部分来执行,执行顺序会按ONBUILD定义的先后顺序执行,如果ONBUILD中定义的任何一个指令运行失败,则会使FROM指令中断并导致整个build失败,当所有的ONBUILD中定义的指令成功完成后,会按正常顺序继续执行build。
  4. ONBUILD中定义的指令不会继承到当前引用的镜像中,也就是当引用ONBUILD的镜像创建完成后将会清除所有引用的ONBUILD指令。
  5. ONBUILD指令不允许嵌套,例如ONBUILD ONBUILD ADD . /data是不允许的。
  6. ONBUILD指令不会执行其定义的FROM或MAINTAINER指令。

例如,Dockerfile使用如下的内容创建了镜像 image-A :

[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

如果基于 image-A 创建新的镜像时,新的Dockerfile中使用FROM image-A指定基础镜像时,会自动执行ONBUILD指令内容,等价于在后面添加了两条指令。

FROM image-A 
#Automatically run the following 
ADD . /app/src 
RUN /usr/local/bin/python-build --dir /app/src
  1. USER

为运行镜像时或者任何接下来的RUN指令指定运行用户名或UID:USER daemon

  1. MAINTAINER

使用MAINTAINER指令来为生成的镜像署名作者: MAINTAINER author's name mailaddress

8.Dockerfile示例

下面的Dockerfile是MySQL官方镜像的构建过程。从ubuntu基础镜像开始构建,安装mysql-server、配置权限、映射目录和端口,CMD在从这个镜像运行到容器时启动mysql。其中VOLUME定义的两个可挂载点,用于在host中挂载,因为数据库保存在主机上而非容器中才是比较安全的。

# MySQL Dockerfile 
# 
# <https://github.com/dockerfile/mysql> 
# 
# Pull base image. 
FROM dockerfile/ubuntu # Install MySQL. 
RUN \     
    apt-get update &&\     
    DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server &&\
    rm -rf /var/lib/apt/lists/*  &&\     
    sed -i 's/^\(bind-address\s.*\)/# \1/' /etc/mysql/my.cnf &&\     
    sed -i 's/^\(log_error\s.*\)/# \1/' /etc/mysql/my.cnf &&\     
    echo "mysqld_safe" > /tmp/config &&\     
    echo "mysqladmin --silent --wait=30 ping || exit 1" >> /tmp/config &&\
    echo "mysql -e 'GRANT ALL PRIVILEGES ON *.* TO \"root\"@\"%\" WITH GRANT OPTION;'" >> /tmp/config &&\
    bash /tmp/config &&\
    rm -f /tmp/config # Define mountable directories. 
VOLUME ["/etc/mysql", "/var/lib/mysql"] # Define working directory. 
WORKDIR /data # Define default command. 
CMD ["mysqld_safe"] 
# Expose ports. 
EXPOSE 3306

使用:

docker build -t="dockerfile/mysql" github.com/dockerfile/mysql 
或下载Dockerfile内容在当前目录: 
$ docker build -t="dockerfile/mysql" .

(提示,上述第一条命令,如果你的host不可以连接Docker Hub,那么需要在启动docker服务时使用HTTP_PROXY=——用于build的时更新下载软件,同时执行docker build的终端设置http_proxy和https_proxy用于下载Dockerfile)

运行:

$ docker run -d --name mysql -p 3306:3306 dockerfile/mysql 
或 
docker run −it −-rm −-linkmysql:mysqldockerfile/mysqlbash −c ′mysql−hMYSQL_PORT_3306_TCP_ADDR'

9.后记

通过上面基本的dockerfile文件的相关知识学习,我们可以清楚了解dockerfile的使用,帮忙我们更加高效的编写dockerfile和构建镜像了。后面有时间也可以继续通过docker源码来看看docker是怎么利用dockerfile继续镜像的构建的。