发现一篇帖子很受用,转载至此,别无它用,侵删。
一、关于 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 ,仅作个人学习,侵删!