转|Dockerfile构建镜像

发现一篇帖子很受用,转载至此,别无它用,侵删。

一、关于 Dockerfile

我们知道 Docker 镜像的生成方式有两种,一种是基于现有容器进行 commit 生成一个镜像,另外一种就是通过 Docker 提供的 Dockerfile 构建一个镜像。

Dockerfile 其实就是一个包含了 build image 过程中需要执行的所有命令的文本文件,这些指令就是 Dockerfile

构建时规定的一些指令,不过区区不到二十个指令。另外,也可以理解为 Dockfile 是一种被 Docker

程序解释的脚本,由一条一条的指令组成,每条指令对应 Linux 系统下面的一条命令,由 Docker 程序将这些 Dockerfile

指令翻译成真正的 Linux 命令。

我们通过一定的格式和指令编写 Dockerfile 文件,然后通过 docker build 命令来读取 Dockerfile

文件中的内容从而构建一个完整的镜像。相比 image 这种黑盒子,Dockerfile 这种显而易见的脚本更容易被使用者接受,它明确的表明这个

image 是怎么产生的。有了 Dockerfile,当我们需要定制自己额外的需求时,只需在 Dockerfile 上添加或者修改指令,重新生成

image 即可,省去了敲命令的麻烦。


二、编写 Dockerfile

其中 Dockerfile 的格式其实很简单,就两类,一类是注释信息,以 # 号作为注释;另外一类就是一条条的指令了。其中指令是忽略大小写的,但建议使用大写,每一行只支持一条指令,每条指令可以携带多个参数。

而 Dockerfile 的指令根据作用可以分为两种:构建指令和设置指令。构建指令用于构建 image,其指定的操作不会在运行 image 的容器上执行;设置指令用于设置 image 的属性,其指定的操作将在运行 image 的容器中执行。

在通过 Dockerfile

构建镜像时是按照指令的顺序运行的,所以需要自己根据相互的依赖关系调整好指令的相应顺序。但需要特别注意的一点就是,在 Dockerfile

中的第一个指令必须是 FROM 指令,除注释信息。FROM

指令是用来引入一个基础镜像,我们后续的所有指令操作都是基于这个基础镜像而进行的,如果没有,那么 Dockerfile 是做不了的。

FROM(指定基础 image)

属于构建指令,用来加载一个基础镜像,比如 CentOS。此指令必须指定且需要是 Dockerfile 中的第一个指令,除注释信息外;后续的指令都依赖于该指令引入的基础镜像而工作。

实践中,FROM 指令指定的基础镜像可以是官方远程仓库中的,也可以位于本地仓库。默认情况下,docker build 会在 docker

主机上查找指定的镜像文件,在其不存在时,则会从 Docker Hub Registry

上拉取所需的镜像文件。如果找不到指定的镜像文件,docker build 会返回一个错误信息。

该指令有两种格式:

FROM <image>

指定基础 image 的名称。或者:

FROM <image>:<tag>

指定基础 image 的名称和标签,tag 为可选选项,省略时默认为 latest。

MAINTAINER(用来指定镜像创建者信息)

属于构建指令,用于将 image 的制作者相关的信息写入到 image 中。当我们对该 image 执行 docker inspect 命令时,输出中有相应的字段记录该信息。

Dockerfile 并不限制 MAINTAINER 指令可在出现的位置,但推荐将其放置于 FROM 指令之后。格式:

MAINTAINER <name>

此指令在新版本 Docker 中已经被标识为即将被废弃,由 LABEL 指令替代,LABEL 指令相比较 MAINTAINER 更加丰富。

LABEL(镜像标签信息)

属于构建指令,LABEL 相比于 MAINTAINER 有了更宽泛的适用领域。用来指令一个镜像的各种元数据,当然其中就包括 MAINTAINER 信息。

LABEL <key>=<value> <key>=<value>

可以看出 LABEL 支持的都是键值对,比如 author=’dkey’。

ENV(用于设置环境变量)

属于构建指令,用于为镜像定义所需的环境变量,并可被 Dockerfile 文件中位于其后的其它指令(如 ENV、ADD、COPY 等)所调用。语法格式如下:

ENV <key> <value>

ENV <key>=<value>

第一种格式中,<key> 之后的所有内容均会被视作其 <value> 的组成部分,包括空格,因此,一次只能设置一个变量。

第二种格式中,可用一次设置多个变量,每个变量为一个“<key>=<value>”的键值对,如果

<value> 中包含空格,可以以反斜线 (\) 进行转义,也可通过对 <value>

加引号进行标识;另外,反斜线也可用于续行。

在定义多个变量时,建议使用第二种方式,以便构建镜像时在同一层中完成所有功能。

需要注意一点,ENV 定义的变量是在 docker build

时替换的,但同时也会注入到以此镜像启动的容器中,这是两个不同的阶段。容器启动后,可以通过 docker inspect

查看这个环境变量。如果想修改容器中的此环节变量,可以通过在启动容器时修改,比如使用 docker run –env key=value

命令修改环境变量,如果变量名称相同,那么就会替换掉容器中对应的环境变量,这个很好理解。

假如你安装了 JAVA 程序,需要设置 JAVA_HOME,那么可以在 Dockerfile 中这样写:

ENV JAVA_HOME=/path/to/java/dirent

那么在 Dockerfile 中引用这个变量时,其实跟 shell 中使用方式一样了,比如引用方式是

${variable_name},其中 {} 可省略,同样可以使用默认值,比如 ${variable_name: – WORD}

就是变量没有值时就显示 WORD 值,${variable_name: + WORD} 表示变量有值时就显示 WORD 值,而

${variable_name: = WORD} 表示变量没有值时就把 WORD 赋值给变量。

同样,这个变量会被注入到以这个镜像启动的容器中,就是这个容器启动后就会有 JAVA_HOME 变量及对应的值。

COPY(从 src 复制文件到容器的 dest 路径)

用于从 Docker 主机复制文件至创建的新镜像。

COPY <src> <dest>

<src>:要复制的源文件或目录,支持使用通配符。

<dest>:目标路径,即正在创建的 image 的文件系统路径;建议为 <dest> 使用绝对路径,否则 COPY 指定则以 WORKDIR 为其路径。

注意:在路径中有空白字符时,通常使用第二种格式。另外,<src> 必须是 build

上下文中的路径,不能是其父目录中的文件。如果 <src> 是目录,则其内部文件或子目录会被递归复制,但 <src>

目录自身不会被复制。如果指定了多个 <src>,或在 <src> 中使用了通配符,则 <dest>

必须是一个目录,且必须以 / 结尾,否则被视为一个普通文件,<src> 内容直接被写入到 <dest> 中。如果

<dest> 事先不存在,它将会被自动创建,这包括其父目录路径。

ADD(从 src 复制文件到容器的 dest 路径)

属于构建指令,ADD 指令类似于 COPY 指令,包括指令格式,但 ADD <src> 支持使用 TAR 文件和 URL

路径。如果 <src> 是一个可识别的压缩格式,则 docker 会帮忙解压缩,它将会被展开为一个目录,其行为类似于 “tar

-x” 命令。然而通过 URL 获取到的 tar 文件将不会自动展开。

RUN(镜像构建时的执行命令)

属于构建指令,RUN 是用来运行命令的,但是属于构建镜像阶段,RUN 可以运行任何被基础镜像支持的命令。比如基础镜像选择了 centos,那么软件管理部分只能使用 centos 的命令,就可以使用 RUN 来运行 yum 命令安装一些软件包。

该指令有两种格式:

RUN <command> (the command is run in a shell - `/bin/sh -c`)

RUN ["<executable>", "<param1>", "<param2>" ... ] (exec form)

第一种格式中,<command> 通常是一个 shell 命令,系统会自动以“/bin/sh -c”来运行此

<command>,这意味着 <command> 属于 shell 的子进程,那么此进程在容器中的 PID 不为

1,不能接收 Unix 信号,因此,当使用 docker stop <container> 命令停止容器时,此进程接收不到

SIGTERM 信号。

Tips:bash -c STRING 的含义,如果存在 -c

选项,则表示从字符串中读取命令。如果字符串有多个空格,第一个空格前面的字符串是要执行的命令,也就是

$0,后面的是参数,即$1,$2….。比如执行命令 bash -c “./test hello world”,那么 ./test 就是

$0,hello 就是 $1,world 就是 $2,以此类推。

第二种语法格式中的参数是一个 JSON 格式的数组(数组中的元素要使用双引号,不能使用单引号,不然会报错),其中

<executable> 为要运行的命令,后面的 <paramN>

为传递给命令的选项或参数;然而,此种格式指定的命令系统不会以“/bin/sh -c”来发起,由内核创建,因此常见的 shell

操作,如变量替换以及通配符(?,*)替换将不会进行;不过,如果要运行的命令依赖于此 shell 特性的话,可以将其替换为类似下面的格式。

RUN ["/bin/bash", "-c", "executable", "param1", "param2" ... ]

比如我需要构建一个 web 镜像,运行一个 httpd 服务并设置一个默认页面,如下:

```

FROM Busybox

ENV WEB_DOC_ROOT=“/opt/web/"

RUN mkdir -p ${DIR} && \

    echo "<h1>httpd server.</h1>" > ${WEB_DOC_ROOT}/index.html

```

由于我们使用第一种 RUN 运行格式,所以默认会以“/bin/sh -c”运行命令,所以在 shell 下是可以使用 ENV 定义的环境变量。

CMD(容器启动时的执行命令)

属于设置指令,CMD 跟 RUN 类似,也是用来运行命令的。但两者运行的时间点不同,RUN 用于构建镜像时运行命令,而 CMD

用于容器启动时运行的命令,可以是一个服务启动命令,也可以是一个系统命令或脚本程序,且其运行结束后,容器也将终止。但该指令只能在文件中存在一次,如果有多个,则只执行最后一条。这主要是因为

Docker 容器的启动只能运行一个进程,并且这个进程 ID 号为 1 在容器中。至于为什么,这就涉及到了 namespaces

了,也就是容器的原理部分了。

我们知道在 Linux 中,如果我们在当前 shell 下启动了一个前台进程,那么这个进程就属于这个 shell 的子进程了。当我们退出或

kill 了这个 shell 进程时,此 shell 进程会一并关闭掉它的相关子进程。注意,就算我们使用 &

把这个前台进程放置到后台运行,它还是属于当前 shell 进程的。所以,大多数时候我们可能都是会使用一个叫 nohup 的命令配合 &

符号使用,nohub 其实主要就是把我们要运行的进行剥离当前 shell,也就是不归属于 shell 的子进程了,这个时候我们退出 shell

时,那么自然就不会关闭我们使用 nohub 运行的进程了。

比如说我们使用 RUN 命令运行一个 CentOS 系统的容器,并且启动时执行一个 bash 程序,如下:

```

$ docker run -ti centos /bin/bash

```

正常情况下,我们就会进入到这个容器的 shell 交互式模式,此时这个 shell 进程就是一个 ID 号为 1 的进程。如果说我们在当前

shell 下启动一个 nginx 服务,那么这个 nginx 服务就属于这个 shell 的子进程。当我们使用 exit 命令退出这个

shell 时,可以想到此时整个容器都会退出,父进程退出子进程也会退出,这个容器都没有进程存在了,自然也就退出了。

该指令有三种格式:

```

CMD command

CMD ["executable","param1","param2"] (like an exec, this is the preferred form)

CMD ["<param1>" "<param2>"] (as a shell)

```

前两种语法格式的意义同 RUN 指令。

然后,我们接着 RUN 命令时用到的 Dockerfile 信息,运行起来这个容器,就需要使用 CMD 指令来启动 httpd 服务,如下:

```

FROM Busybox

ENV WEB_DOC_ROOT="/opt/web"

RUN mkdir -p ${DIR} && \

    echo "<h1>httpd server.</h1>" > ${WEB_DOC_ROOT}/index.html

CMD /bin/httpd -f -h ${WEB_DOC_ROOT}

```

此刻,CMD 指令也使用了第一种语法格式,所以自然也是以“/bin/sh -c”来启动命令,当我们以这个镜像来启动一个 httpd 容器时自然也可以引用 shell 下的 WEB_DOC_ROOT 变量。

如果我们使用第二种 JSON 数组的命令格式,如下:

CMD ["/bin/httpd", "-f", "-h ${WEB_DOC_ROOT}"]

前面说过,这种格式默认不会以“/bin/sh -c”来运行命令,所以自然引用不到 shell 环境变量,自然也就启动不了容器。

Tips:当我们构建完镜像后,我们可以通过 docker inspect image_name 命令查看镜像详细信息,就可以看到 RUN 或 CMD 命令运行的详细信息,包括有没有使用“/bin/sh -c”来运行。

CMD 的第三种语法格式则用于为 ENTRYPOINT 指令提供的默认参数。这种使用方式在介绍 ENTRYPOINT 的时候介绍。

不过,需要注意的是,CMD 指定的命令其可以被 docker run 命令行参数所替换。简单来说就是,运行 docker run 时,如果指定了要运行的命令,命令就是指 docker run 的参数了,此命令会替换 CMD 指定的运行命令。

ENTRYPOINT(容器启动时执行的命令)

类似 CMD 指令的功能,用于为容器指定默认启动时执行的程序,从而使得容器像是一个单独的可执行程序,但 ENTRYPOINT 没有 CMD

的可替换特性,CMD 指定的命令是可以被 docker run 命令行所指定的参数覆盖(就是 docker run 运行的命令会替换 CMD

指定的命令),但 ENTRYPOINT 是无法被覆盖的,并且 docker run 命令行指定的运行命令会被当作参数传递给 ENTRYPOINT

指定的运行程序。同样,一个 Dockerfile 中只能有一条 ENTRYPOINT 命令,如果多条,则只执行最后一条。

Tips:不过,docker run 命令的 –entrypoint 选项的参数可覆盖 ENTRYPOINT 指令指定的程序。

支持两种语法格式:

ENTRYPOINT command

ENTRYPOINT ["executable", "param1", "param2"]

该指令的使用分为两种情况,一种是独自使用,另一种和 CMD 指令配合使用。

当独自使用时,如果你还使用了 CMD 命令且 CMD 是一个完整的可执行的命令,那么 CMD 指令和 ENTRYPOINT 会互相覆盖,只有最后一个 CMD 或者 ENTRYPOINT 有效。

CMD echo "Hello, World!"

ENTRYPOINT ls -l

CMD 指令将不会被执行,只有 ENTRYPOINT 指令被执行。ENTRYPOINT 默认就是以“/bin/sh -c”方式运行,所以 ENTRYPOINT 指定的命令都是当做 shell 的子进程来运行。

另一种用法和 CMD 指令配合使用来指定 ENTRYPOINT 的默认参数,这时 CMD 指令不是一个完整的可执行命令,仅仅是参数部分;ENTRYPOINT 指令只能使用数组方式指定执行命令,而不能指定参数。

CMD ["-l", "-s"]

ENTRYPOINT ["/usr/bin/ls"]

这就是 CMD 的第三种命令格式。此时, CMD 里面的内容会被当做 ENTRYPOINT

运行命令的默认参数。默认参数是什么意思呢?如果我们使用 docker run 运行一个容器时没有指定运行命令,那么这里的 CMD 命令就会传递给

ENTRYPOINT 当做参数使用,如果 docker run 指定了运行命令,那么此时 CMD 就不会生效了,会把 docker run

指定的运行命令当做 ENTRYPOINT 的参数。

在介绍 CMD 指令时也说了,当以数组格式运行命令时,是不会以“/bin/sh -c”方式来启动命令。如果 CMD 与

ENTRYPOINT 相结合,我们就可以让 CMD 以数组格式运行,同时以“/bin/sh -c”方式来运行,意味着 CMD 中可以使用

shell 变量引用了。操作方式如下:

CMD ["/bin/httpd", "-f", "-h ${WEB_DOC_ROOT}"]

ENTRYPOINT ["/bin/sh", "-c"]

默认 ENTRYPOINT 就是以 [“/bin/sh”, “-c”] 来运行,可通过 docker inspect

命令来查看镜像详细信息,如果我们这里直接 ENTRYPOINT /bin/sh -c 来运行,那么意思就是把我们指定的“/bin/sh

-c”当做默认 [“/bin/sh”, “-c”] 的参数运行,那么会在镜像中看到两层“/bin/sh

-c”。当我们目标就是明确使用“/bin/sh -c”来运行,就可以使用上面数组的方式。

Tips:有了 CMD 之后,为什么还需要 ENTRYPOINT 呢?本质上还是为了灵活,CMD 可以作为

ENTRYPOINT 的参数运行,所以一般就用来指定容器最终要运行的程序或命令,而 ENTRYPOINT

会用来最终运行容器中要运行的命令。你会发现大多数镜像都会使用 ENTRYPOINT 运行一个叫 entrypoint.sh

的脚本,脚本中用来处理各种配置或环境,脚本有多强大,你这个容器所能适应的环境或功能就有多强大。比如用户运行一个 nginx 容器,你可以在

entrypoint.sh 脚本中生成配置文件时引用一个 PORT 变量,用户就可以在 docker run nginx 时通过 -e

指定这个容器要运行的端口信息了。

EXPOSE(暴露容器端口)

EXPOSE 可以用来暴露端口,或者在 docker run 时指定--expose=1234,这两种方式作用相同。但是,--expose可以接受端口范围作为参数,比如--expose=2000-3000。EXPOSE 和--expose都不依赖于宿主机器。默认状态下,这些规则并不会使这些端口可以通过宿主机来访问。

基于 EXPOSE 指令的上述限制,Dockerfile 的作者一般在包含 EXPOSE 规则时都只将其作为哪个端口提供哪个服务的提示。使用时,还要依赖于容器的操作人员进一步指定网络规则,需要配合docker run -p PORT:EXPORT使用,这样 EXPOSE 设置的端口号会被指定需要映射到宿主机器的端口,这时要确保宿主机器上的端口号没有被使用。如果直接指定docker run -p EXPORT,这样 EXPOSE 设置的端口号会被随机映射成宿主机器中的一个端口号。不过通过 EXPOSE 命令文档化端口的方式十分有用。

本质上说,EXPOSE 或者--expose只是为其他命令提供所需信息的元数据(比如容器间 link 操作就依赖 EXPOSE 元数据),或者只是告诉容器操作人员有哪些已知选择。

语法格式:

EXPOSE <port> [<port>...]

EXPOSE 指令可以一次设置多个端口号,相应的运行容器的时候,可以配套的多次使用 -p 选项。

# 暴露一个端口;

EXPOSE port1

# 如果想代理EXPOSE端口, 相应的运行容器使用的命令;

docker run -p port1 image

# 暴露多个端口;

EXPOSE port1 port2 port3

# 如果想代理EXPOSE端口, 相应的运行容器使用的命令;

```

docker run -p port1 -p port2 -p port3 image

```

# 还可以指定需要映射到宿主机器上的某个端口号;

```

docker run -p host_port1:port1 -p host_port2:port2 -p host_port3:port3 image

```

注意,EXPOSE 仅仅是暴露一个端口,一个标识,在没有定义任何端口映射时,外部是无法访问到容器提供的服务。而端口映射(-p)是

docker 比较重要的一个功能,原因在于我们每次运行容器的时候容器的 IP 地址不能指定,而是在桥接网卡的地址范围内随机生成的。宿主机器的

IP

地址是固定的,我们可以将容器的端口的映射到宿主机器上的一个端口,免去每次访问容器中的某个服务时都要查看容器的IP的地址。对于一个运行的容器,可以使用

docker port 加上容器 ID 和 EXPOSE 暴露的端口来查看该端口号在宿主机器上的映射端口。

$ docker port redis 6379

0.0.0.0:6380

VOLUME(挂载卷)

属于设置指令,使容器中的一个目录具有持久化存储数据的功能,该目录可以被容器本身使用,也可以共享给其他容器使用。我们知道容器使用的是

AUFS 联合文件系统,这种文件系统不能持久化数据,当容器删除后,所有读写层的数据都会丢失。当容器中的应用有持久化数据的需求时可以在

Dockerfile 中使用该指令。格式:

VOLUME ["<mountpoint>"]

比如我们在 Dockerfile 中添加 VOLUME /data,然后通过该 Dockerfile 生成 image 的容器,/data

目录中的数据在容器关闭后,里面的数据还存在。当我们使用 docker inspect 命令查看已经启动的容器信息时,可以看到 Mounts

信息,如下:

```

"Mounts": [

    {

        "Type": "volume",

        "Name": "c3e3bda4e64cc51af272bb7bb4a75f45b07216b6b34e87f24261813caf4e0347",

        "Source": "/var/lib/docker/volumes/c3e3bda4e64cc51af272bb7bb4a75f45b07216b6b34e87f24261813caf4e0347/_data",

        "Destination": "/data",

        "Driver": "local",

        "Mode": "",

        "RW": true,

        "Propagation": ""

    }

]

```

可以看到容器的 /data 目录挂载在了 docker host 的具体路径,如果使用 docker rm -f 命令删除容器,此数据目录也还是存在的。但如果使用 docker rm -fv 命令删除容器,那么数据目录会被一并删除,这是需要注意的。

另外,另一个容器也有持久化数据的需求,且想使用上面容器共享的 /data 目录,那么可以运行下面的命令启动一个容器:

```

$ docker run -ti -volumes-from container_name image_name /bin/bash

```

其中 container_name 为 /data 目录所在的容器名称,image_name 为第二个容器运行 image 的名字。

WORKDIR(切换目录)

属于设置指令,可以多次切换(相当于 cd 命令),对 RUN、CMD、ENTRYPOINT 生效。格式:

WORKDIR /path/to/workdir

# 在/p1/p2下执行vim a.txt;

WORKDIR /p1 WORKDIR p2 RUN vim a.txt

USER(设置容器的用户)

属于设置指令,用于指定运行镜像时的或运行 Dockerfile 中任何 RUN、CMD或ENTRYPOINT 指令指定的程序时的用户名或 UID。默认容器的运行身份是 root 用户。语法格式如下:

USER <UID>|<UserName>

需要注意的是,<UID>可以为任意数字,但实践中其必须为 /etc/passwd 中某用户的有效 UID,否则,docker run 命令将运行失败。

HEALTHCHECK(健康检查)

我们知道容器就是被隔离的进程,当一个容器中的进程退出,那么意味着容器也就退出了。最常见的场景就是一个运行一个容器的时候,如果进程起不来,那么容器自然也起不来。Docker

引擎判断一个容器是否能正常工作,是根据进程的状态,而不是根据进程运行的服务是否正常来判断的。比如我们运行一个 nginx,进程都没正常,但是

nginx 里面的静态资源没有,自然对用户来说访问就是有问题的,但 Docker 引擎并知道。

那么 Docker 就提供了 HEALTHCHECK 指令,让我们可以定义服务层面的健康状况检查。一个简单的示例如下:

HEALTHCHECK --interval=5m --timeout=3s --start-period=1m --retries=3 CMD curl -f http://localhost || exit 1

其中--interval用来指定每次检查的间隔时间,默认 30s;--timeout是指检测一次的等待超时时间,默认 30s;--start-period表示容器启动后多久开始检测,有的服务启动时间较长,默认 0s;--retries表示检测重试的次数,避免误杀,默认 3 次。

当检测命令发出后,我们可以定义返回检测码,0 表示健康,1 表示不健康,2 暂时是预留的。

STOPSIGNAL(定义停止容器的信号)

一个容器就是运行了一个 PID 为 1 的进程,也只有 PID 为 1 的进程才可以接收信号。当我们执行 docker stop

命令时,其实就是给进程发送了一个 -15 的信号,进程接收到了之后就退出了,进程退出容器自然也就退出了,从而实现关闭容器的功能。

如果我们想使用别的信号来杀死进程也可以的,比如 -9 信号,强制杀死进程。就可以使用 STOPSIGNAL 来指定,大多数情况下也不需要改。

ARG(构建镜像传参)

ENV 指令可以用来定义变量,其定义的变量可以在构建镜像和容器运行时使用。但是细心就会发现,ENV 定义的变量在 Dockerfile 中都是固定的值,如果我们想在构建镜像时传参,根据同一个 Dockerfile 可以构建不同的镜像,那么就需要 ARG 指令了。ARG 指令就是用来定义镜像构建时传参,然后docker build --build-arg key="value"指定要传给镜像构建时的变量即可。

但需要注意,ARG 指令也没有办法放在 FROM 指令的前面,也就是说如果使用传参的方式来构建不同版本的 nginx,是不支持的。因为

ARG 没办法放在 FROM 指令前面,但 ARG 放在 FROM 后面时,FROM 指令又无法找到它要引用的变量,这就成了一个死循环了。

ONBUILD(子镜像中执行操作)

用于在 Dockerfile 中定义一个触发器,Dockerfile 用于 build 映象文件,此映象文件亦可作为 base image 被另一个 Dockerfile 用作 FROM 指令的参数,并以之构建新的映象文件。

在后面的这个 Dockerfile 中的 FROM 指令在 build 过程中被执行时,将会“触发”创建其 base image 的

Dockerfile 文件中的 ONBUILD 指令定义的触发器。简单说就是,你定义了 Dockerfile,并使用了 ONBUILD

指令,在你进行镜像构建时,这个 ONBUILD 指令并不会执行;然后,其他人以你这个镜像作为他编写 Dockerfile 时的 base

image,也就是 FROM 引用,他在进行构建时会执行你的 ONBUILD 指令定义的动作。

ONBUILD <INSTRUCTION>

尽管任何指令都可注册成为触发器指令,但 ONBUILD 不能自我嵌套,且不会触发 FROM 和 MAINTAINER 指令。

在 ONBUILD 指令中使用 ADD 或 COPY 指令时应该格外小心,因为新构建过程的上下文在缺少指定的源文件时会报错。但如果你使用 ADD 添加一个互联网的 URL 就没啥问题了。


三、构建Dockerfile文件

创建一个构建 nginx 镜像的 Dockerfile 文件,Git地址:https://github.com/dongwenpeng/nginx

```

FROM nginx

LABEL MAINTAINER="dkey"

ENV RUN_USER nginx

ENV RUN_GROUP nginx

ENV DATA_DIR /data/web

ENV LOG_DIR /data/log/nginx

RUN mkdir /data/log/nginx -p

RUN chown nginx.nginx -R /data/log/nginx

ADD nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

ENTRYPOINT nginx -g "daemon off;"

```

此 Dockerfile 很简单,做了这么几件事:

1. 拉取一个 nginx 镜像。

2. 设置了几个变量。

3. 创建了几个需要的目录。

4. 把当前目录下的 web 程序复制到镜像的 /data/web 目录。

5. 把 nginx.conf 配置文件和 default.conf 配置文件复制到镜像中。

6. 设置一个默认端口。

7. 最后设置了容器启动时执行的命令,我用来启动nginx程序,注意这个命令不能错,不然容器启动不了。这样设置后,当你docker run运行此镜像时不需要在后面再次执行需要执行的命令了。

如果我能提前把这个 nginx

容器可能需要更改的配置都进行变量化,那么对于用户来说是不是就可以不用关心这个容器的构建过程,下载容器之后想改什么配置都可以通过传递变量的方式来进行,是不是就方便很多。我们可以简单模拟一下这个场景,当然,不会把所有参数都进行变量化,只是作为演示。

```

FROM nginx

LABEL MAINTAINER="dkey"

ENV RUN_USER nginx

ENV RUN_GROUP nginx

ENV NGX_DOC_ROOT="/data/web/html/"

ADD entrypoint.sh /bin

EXPOSE 80

CMD ["/usr/sbin/nginx", "-g", "daemon off;"]

ENTRYPOINT ["/bin/entrypoint.sh"]

```

entrypoint.sh 脚本的内容:

```

#!/bin/sh

#

cat > /etc/nginx/conf.d/www.conf << EOF

server {

server_name ${HOSTNAME};

listen ${IP:-0.0.0.0}:${PORT:-80};

root ${NGX_DOC_ROOT:-/usr/share/nginx/html};

}

EOF

exec "$@"

```

最后这个 exec 特别关键了,其中 $@ 是用来取所有参数的,也就是 CMD 指令指定的容器运行程序。使用 exec

来执行命令,表示替换执行 entrypoint.sh 的进程,从而把 nginx 进程变成 1 号进程,只有成为 1 号进程才能接受信号,比如

stop、kill 等指令。


四、构建nginx镜像

Dockerfile 编写好了之后就可以通过 docker build 来进行构建了,需要注意 docker build 命令的执行需要在

dockerfile 文件所在目录,会自动找到以 dockerfile 命令的文件,然后就会开始进行镜像构建。可以使用 -t

参数指定镜像的名称或名称加 tag。

```

$ docker build -t nginx_01 .

Sending build context to Docker daemon  3.43 MB

Step 1 : FROM nginx

latest: Pulling from library/nginx

64f0219ba3ea: Pull complete

325b624bee1c: Pull complete

Digest: sha256:2a07a07e5bbf62e7b583cbb5257357c7e0ba1a8e9650e8fa76d999a60968530f

Status: Downloaded newer image for nginx:latest

---> 19146d5729dc

Step 2 : MAINTAINER dkey

---> Running in a9d12e4fa972

---> 3e6bcc394986

Removing intermediate container a9d12e4fa972

Step 3 : ENV RUN_USER nginx

---> Running in 55e165c28124

---> 024ba866269e

Removing intermediate container 55e165c28124

Step 4 : ENV RUN_GROUP nginx

---> Running in 577cb4556709

---> 95f5b6de5080

Removing intermediate container 577cb4556709

Step 5 : ENV DATA_DIR /data/web

---> Running in e472aaa991f2

---> d50decbaf7bc

Removing intermediate container e472aaa991f2

Step 6 : ENV LOG_DIR /data/log/nginx

---> Running in 5ccf9955685d

---> 00f1deefcc1b

Removing intermediate container 5ccf9955685d

Step 7 : RUN mkdir /data/log/nginx -p

---> Running in 63a61c463ad4

---> 467d1548998c

Removing intermediate container 63a61c463ad4

Step 8 : RUN chown nginx.nginx -R /data/log/nginx

---> Running in 00d559b2de7b

---> a550d1639d98

Removing intermediate container 00d559b2de7b

Step 9 : ADD web /data/web

---> e75181ff11e0

Removing intermediate container b4c03ff5df61

Step 10 : ADD nginx.conf /etc/nginx/nginx.conf

---> 00dcb2ec895f

Removing intermediate container 4a75f67c4f1c

Step 11 : ADD default.conf /etc/nginx/conf.d/default.conf

---> 9b2668d59294

Removing intermediate container d57c3e297464

Step 12 : EXPOSE 80

---> Running in 076209139acf

---> fa3247254200

Removing intermediate container 076209139acf

Step 13 : ENTRYPOINT nginx -g "daemon off;"

---> Running in e0a4845172cb

---> fa37625af3ac

Removing intermediate container e0a4845172cb

Successfully built fa37625af3ac

```

从执行过程可以看出一共十三个步骤,都完成了。

下面可以看看镜像了。

```

$ docker images

REPOSITORY                  TAG                IMAGE ID            CREATED            SIZE

nginx_01                    latest              fa37625af3ac        38 seconds ago      182.7 MB

nginx                      latest              19146d5729dc        6 days ago          181.6 MB

```

可以看到从官方拉取了一个 nginx 镜像,然后在此镜像的基础上构建了 nginx_01 镜像。注意,由于 nginx_01 是在

nginx 镜像的基础上构建出来的,所以如果你要删除 nginx 镜像是不允许的,只有先删除 nginx_01 镜像后才可以删除 nginx

镜像。

同理,你可以接着创建第二个镜像,这个时候你会发现构建速度非常之快。

```

$ docker build -t nginx_02 .

$ docker images

REPOSITORY                  TAG                IMAGE ID            CREATED            SIZE

nginx_02                    latest              fa37625af3ac        54 seconds ago      182.7 MB

nginx_01                    latest              fa37625af3ac        54 seconds ago      182.7 MB

nginx                      latest              19146d5729dc        6 days ago          181.6 MB

```

但你也应该发现,这两个镜像的 IMAGE ID 是相同的,因为 docker 检测出你的 dockerfile 文件没有任何内容改变(包括要复制文件的内容),所以只是做了一个链接。这个时候你可以运行这个镜像看看。

```

$ docker run --name nginx_01 -d -p 80:80 nginx_01

$ docker exec -ti nginx_01 bash

root@0dbeb4d3852f:/# ss -nplt

State      Recv-Q Send-Q                                  Local Address:Port                                    Peer Address:Port

LISTEN      0      128                                                *:80                                                *:* 

root@0dbeb4d3852f:/# cat /data/log/nginx/access.log

172.28.24.96 - - [27/Dec/2016:08:37:58 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) Chrome/55.0.2883.87 Safari/537.36" "-"

```

从上面的结果可以看出,这个 nginx 容器运行一切正常。你可以在浏览器访问看看。

这个时候你可以选择删除一个镜像看看。

$ docker rmi nginx_02

Untagged: nginx_02:latest

成功删除,不管你删除哪个镜像都可以删除,但是只能删除一个。再次删除另一个时就会报错。

$ docker rmi nginx_01

Error response from daemon: conflict: unable to remove repository reference "nginx_01"

如果我们改变了网站代码或者改变了配置文件,再次构建镜像时会怎么样呢?

其实 dockerfile 中有任何的改动,再次构建镜像时都会重新构建,但是只会从改动的地方开始重新构建,速度很快,这就是镜像分层的好处。

```

$ docker build -t nginx_02 .

$ docker images

REPOSITORY                  TAG                IMAGE ID            CREATED            SIZE

nginx_02                    latest              9868ca64a680        9 seconds ago      182.7 MB

```

五、使用commit命令提交新镜像

通过上面我们知道,通过 dockerfile 可以构建一个镜像。同样使用 commit 也可以提交一个新镜像。跟 build

不同的是,commit 只能从现有的容器之上提交出一个新的镜像。比如,你对 nginx

镜像做好了基本配置,然后就可以把这个镜像提交为一个新的镜像。

commit 语法:

docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

提交一个新的镜像。

```

$ docker commit -a pengdongwen nginx_01 nginx_10

sha256:2cdae15e08fd9160fc24998a4f6cae270cd2130980fd7c48cc9da70b1b67671e

$ docker images

REPOSITORY                  TAG                IMAGE ID            CREATED            SIZE

nginx_10                    latest              2cdae15e08fd        6 seconds ago      182.7 MB

```

commit 是不是很方便,也很好用。

转自 https://www.ywnds.com/?p=7611 ,仅作个人学习,侵删!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容