关于 Docker 系列其他文章的传送门
-
《 使用 Dockerfile 定制镜像 搭建 php 站点 》 您当前所在位置
一、Dockerfile 中需要了解的一些东西¶

制作 Docker 镜像可以使用 docker commit 命令,需要先启动镜像,然后打包时,会把最上层可写层的数据,以及依赖的关系,完整保存在联合文件系统中。但其实这整个过程,可以通过另一种方式完成,就是利用制作镜像的相关源码中的指令来自定义镜像,而不用自己手工完成。
安装软件包、创建文件夹、从本地指定或者互联网复制文件等,这就是 Dockerfile,它会自动创建一个镜像,把之前需要手动操作的步骤,隐式完成,并打包成镜像。
1. Dockerfile 的一些特性¶
- Dockerfile 其实就是构建 Docker 镜像的源码。
- Dockerfile 能够通过读取 file 文件里的指令 (instruction) 来构建一个镜像。
- Dockerfile 其实仅仅是个纯文本文件,只不过把文件里的内容作为 Dockerfile 的指令 (instruction) 来运行。
- 这个指令 (instruction) 是有特定格式的,只能使用语法格式当中定义的,能被 docker 识别的指令。
2. Dockerfile 的工作环境¶
Dockerfile 在工作时,需要一个工作目录,我们称之为 工作车间。
工作车间相当于是宿主机文件系统上的一个目录,这个目录内只放置构建镜像需要使用的 Dockerfile 文件,和打算复制的文件。
这个目录其实可以理解成 Docker 的存储卷,构建镜像时,从这个卷里找到文件,并复制到容器,所以需要注意的是,如果文件放在其他位置的话会无法复制,所以要复制的文件必须放在当前文件夹内。
同时,Dockerfile 文件本身也只能叫 Dockerfile。
3. 构建镜像¶
1 | docker image build Dockerfile 父目录/ -t IMAGES:TAG |
需要注意的一些问题:
- 指令本身并不区分大小写,但是按照约定俗成的惯例,指令一般写成纯大写。
- 没有多余的语法,所有指令按顺序执行。
- 一般来讲,第一行是 FROM,但是对于新版的 file,这个不是强制要求的了。
- 和手动构建镜像的区别是:每一条指令都会生成一个镜像层。
- 最好把关系联系紧密的操作放在同一层。
关于镜像层的数量控制:
| 分类 | 优点 | 缺点 |
|---|---|---|
| 镜像层多 | 可以控制的很精细,分发给其他容器时容易共享 | 第一次写入是 cow 机制,对性能略有影响 |
| 镜像层少 | 镜像层比较整体化,性能略优 | 不便于自由分发 |
4. 启动脚本 entrypoint.sh¶
大部分程序的定制是通过配置文件来实现的,而镜像中程序的配置文件已经写死在镜像中了,这就导致了如果有多个部署环境,需要为每个环境下的每个程序制作各不相同的 Docker 镜像,这就很头大了。
所以现在面向云原生开发的应用程序都支持 --> 容器启动时直接读取 Docker 宿主机的环境变量,然后根据环境变量把参数传入到配置文件内。
但是一些老牌的程序,并不支持从环境变量中传入配置,为了解决这个问题,便引入了指定环境变量的方法,如果程序不支持直接获取宿主机环境变量,比如 mysql 的镜像启动时,就需要使用 -e MYSQL_ROOT_PASSWORD 的选项来指定 root 密码,这就解决了老的程序不支持读取宿主机环境变量的问题,就像有句名言是这么说的 计算机科学领域中任何两个不衔接的层,都可以通过增加一个间接的中间层来解决,这个脚本就是衔接新老程序的中间层。但是还有一个问题,如果参数太多了,就需要在 shell 中输入很长的参数,这也很头大。
所以 Docker 支持在启动时使用脚本,作为容器启动的第一个进程,脚本运行完后,就把原本要运行的程序启动,然后自动退出,这个脚本就叫 entrypoint.sh,文件本身也叫这个。
5. 设置忽略的文件¶
如有在 Dockerfile 的工作车间目录下,有不希望在构建镜像时复制的文件,可以通过 .dockerignore 文件配置,Docker 会自动忽略这个文件以及文件内所有指定的文件。
二、Dockerfile 文件的指令¶
1. FROM¶
FROM 指令是最重要的一个且必须为 Dockerfile 文件开篇的第一个非注释行,用于为映像文件构建过程指定基准镜像,后续的指令运行于此基准镜像所提供的运行环境。
在实践中,基准镜像可以是任何可用镜像文件,默认情况下,docker build 会在 docker 主机上查找指定的镜像文件,在其不存在时,则会从 Docker Hub Registry 上拉取所需的镜像文件。如果找不到指定的镜像文件,docker build 会返回一个错误信息。
语法:
1 | FROM <repository>[:<tag>] 或 |
2. LABEL¶
用于让 Dockerfile 制作者提供本人的详细信息,Dockerfile 并不限制 label 指令可在出现的位置,但推荐将其放置于 FROM 指令之后。
语法:
1 | LABEL <key>=<value> <key>=<value> <key>=<value> ... |
需要注意的是:
- 要在 LABEL 值中包含空格,请使用引号和反斜杠。
- 图像可以有多个标签。
- 可以在一行中指定多个标签。
3. COPY¶
用于从 Docker 主机复制文件至创建的新映像文件。
语法:
1 | copy <src> <dest> 或 |
需要注意的是:
src必须是 build 上下文中的路径,不能是其父目录中的文件。- 如果
src是目录,则其内部文件或子目录会被递归复制,但src目录自身不会被复制。 - 如果指定了多个
src,或在src中使用了通配符,则dest必须是一个目录,且必须以 / 结尾。 - 如果
dest事先不存在,它将会被自动创建,也包括其父目录路径。
4. ADD¶
ADD 指令类似于 COPY 指令,ADD 支持使用 TAR 文件和 URL 路径。
语法:
1 | ADD <src> ... <dest> 或 |
操作准则基本同 COPY 指令,但需要注意的是:
- 如果
src为 URL 且dest不以/结尾,则src指定的文件将被下载并直接被创建为dest。 - 如果
dest以/结尾,则文件名 URL 指定的文件将被直接下载并保存为dest/filename。 - 如果
src是一个本地系统上的压缩格式的 tar 文件,它将被展开为一个目录,其行为类似于tar -x命令。然而,通过 URL 获取到的 tar 文件将不会自动展开。 - 如果
src有多个,或其间接或直接使用了通配符,则dest必须是一个以/结尾的目录路径。 - 如果
dest不以/结尾,则其被视作一个普通文件,src的内容将被直接写入到dest。
5. WORKDIR¶
用于为 Dockerfile 中所有的 RUN、CMD、ENTRYPOINT、COPY 和 ADD 指定工作目录。
语法:
1 | WORKDIR <dirpath> |
需要注意的是:
在 Dockerfile 文件中,WORKDIR 指令可出现多次,其路径也可以为相对路径,不过,其是相对此前一个 WORKDIR 指令指定的路径。另外,WORKDIR 也可调用由 ENV 指定定义的变量。
例如:
1 | WORKDIR /var/log |
6. VOLUME¶
VOLUME 是用于在 image 中创建一个挂载点目录,以挂载 Docker host 上的卷或其它容器上的卷。
语法:
1 | VOLUME <mountpoint> 或 |
需要注意的是:
- 如果挂载点目录路径下此前在文件存在,docker run 命令会在卷挂载完成后将此前的所有文件复制到新挂载的卷中。
- 这里定义的 VOLUME,只能用
容器管理的卷的方式挂载,如果想指定目录,必须通过docker run时用-v指定才可以。
7. EXPOSE¶
用于为容器打开指定要监听的端口以实现与外部通信。
语法:
1 | EXPOSE <port>[/<protocol>] [<port>[/<protocol>] ...] |
需要注意的是:
- EXPOSE 指令可一次指定多个端口,例如:
EXPOSE 11211/udp 11211/tcp。 - 因为网络暴露存在风险,所以就算在 Dockerfile 中指定了端口,而且 inspect 查看镜像信息也确实有 EXPOSE 信息,但是容器并不会真的暴露端口,而需要在
docker run时 使用大写 P-P来暴露端口,这时不用加任何参数,且只能使用动态端口的模式。
8. ENV¶
用于为镜像定义所需的环境变量,并可被 Dockerfile 文件中位于其后的其它指令(如 ENV、ADD、COPY 等)所调用,调用格式为 $variable_name 或 ${variable_name}。
当运行容器时,环境变量将保持不变。可以使用 docker inspect 查看值,如果对变量进行更改请使用 docker run --env|-e <key>=<value>。
语法:
1 | ENV <key> <value> 或 |
需要注意的是:
- 第一种格式中,
<key>之后的所有内容均会被视作其<value>的组成部分,因此,一次只能设置一个变量。 - 第二种格式可用一次设置多个变量,每个变量为一个
<key>=<value>的键值对,如果<value>中包含空格,可以以反斜线\进行转义,也可通过对<value>加引号进行标识。另外,反斜线也可用于续行。 - 定义多个变量时,建议使用第二种方式,以便在同一层中完成所有功能。
注意:ENV 定义的变量,会在容器启动后传入到容器内。
注意:使用 ENV 指令定义的环境变量始终覆盖同名的 ARG 指令。
9. ARG¶
ARG 是 Docker 新版本中支持的参数,能够在 build 阶段使用 --build-arg key="value" 定义一个变量,替换 Dockerfile 中 ARG。
注意:ARG 定义的变量,容器启动后不会传到容器内。
注意:建议在 build 阶段使用 ARG,比如在 Dockerfile 中定义了 ARG,那么 build 阶段就可以使用 --build-arg key="value" 进行指定了,由于容器启动后不会传到容器内,那么只有在 build 阶段的指令 COPY、ADD 等会受益。而 ENV 则建议在 RUN 阶段使用,由于 ENV 可以传入到容器内,那么 build 和 run 阶段的所有指令,包括 CMD、ENTYRPOINT 等都将受益,启动容器时就可以通过 --env 或 -e 来指定新变量,传入到容器内。
注意:使用 ENV 指令定义的环境变量始终覆盖同名的 ARG 指令。
ARG 的构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。
语法:
1 | ARG <name1>[=<default value1>] |
10. RUN¶
用于指定 docker build 过程中运行的程序,其可以是任何命令。
两种使用形式:
1 | shell 形式 |
注意:与 shell 不同,exec 不会调用 shell 命令。这意味着不会发生正常的 shell 处理。例如,RUN ["echo", "$HOME"] 不会对 $HOME 进行变量替换。如果你想要 shell 处理,那么要么使用 shell ,要么直接执行 shell,例如:RUN [ "sh", "-c", "echo $HOME" ]。当使用 exec 并直接执行 shell 时,就像 shell 的情况一样,它是执行环境变量扩展的 shell,而不是 docker。
注意:exec 形式被解析为 JSON,要使用双引号。
注意:如果要运行的不同的 shell 的话,可以将其替换为类似下面的格式。
1 | RUN ["/bin/bash", "-c", "<executable>", "<param1>"]` |
镜像构建有两个阶段:
-
BUILD
- COPY、ADD、RUN 等,发生在镜像制作阶段
-
RUN
- CMD、ENTRYPOINT 等,发生在容器启动时

11. CMD¶
类似于 RUN 指令,CMD 指令也可用于运行任何命令或应用程序,不过,二者的运行时间点不同:
| 指令 | 运行时机 | 有多行指令时 |
|---|---|---|
| RUN | 映像文件构建过程中 | 全部生效 |
| CMD | 基于 Dockerfile 构建出的新映像文件启动一个容器时 | 仅最后一个会生效 |
注意:不要混淆使用 RUN 和 CMD。RUN 实际上是运行一个命令并提交结果到构建的镜像中。而 CMD 在构建时不执行任何操作,当启动容器时才运行指定命令。
CMD 指令的首要目的在于为启动的容器指定默认要运行的程序
不过,CMD 指定的命令其可以被 docker run 的 command 选项所覆盖。
在 Dockerfile 中可以存在多个 CMD 指令,但仅最后一个会生效。
CMD 有三种使用形式:
1 | CMD ["executable","param1","param2"] # 使用 exec 执行,推荐方式 |
注意:如果 CMD 用于为 ENTRYPOINT 指令提供默认参数,则应使用 JSON 形式
注意:exec 形式被解析为 JSON,要使用双引号。
注意:与 shell 不同,exec 不会调用 shell 命令。这意味着不会发生正常的 shell 处理。例如,CMD ["echo", "$HOME"] 不会对 $HOME 进行变量替换。如果你想要 shell 处理,那么要么使用 shell ,要么直接执行 shell,例如:CMD [ "sh", "-c", "echo $HOME" ]。当使用 exec 并直接执行 shell 时,就像 shell 的情况一样,它是执行环境变量扩展的 shell,而不是 docker。
12. ENTRYPOINT¶
类似 CMD 指令的功能,运行时机也是一样,用于为容器指定默认运行程序,从而使得容器像是一个单独的可执行程序。
| 指令 | 运行时机 | 有多行指令时 | 覆盖方式 |
|---|---|---|---|
| CMD | 启动容器时 | 仅最后一个会生效 | 会被 docker run 中的 [COMMAND] 覆盖 – docker run [OPTIONS] IMAGE [COMMAND] [ARG...] |
| ENTRYPOINT | 启动容器时 | 仅最后一个会生效 | 只有 docker run 命令的 --entrypoint 选项的参数可覆盖,而 [COMMAND] 中的内容会被当成 ENTRYPOINT 程序的参数传入 |
两种使用形式:
1 | ENTRYPOINT ["executable", "param1", "param2"] # exec 形式,首选 |
注意:当两者同时出现时,CMD 中的内容会被当做 ENTRYPOINT 中指定程序的参数。
注意:shell 形式可以防止任何 CMD 或 RUN 作为命令行参数传入,但缺点是你的 ENTRYPOINT 将作为 /bin/sh -c 的子命令启动,这意味着此进程在容器中的 PID 不为 1,不能接收 Unix 信号,因此,当使用 docker stop <container> 命令停止容器时,此进程接收不到 SIGTERM 信号。
注意:exec 形式被解析为 JSON,要使用双引号。
注意:与 shell 不同,exec 不会调用 shell 命令。这意味着不会发生正常的 shell 处理。例如,CMD ["echo", "$HOME"] 不会对 $HOME 进行变量替换。如果你想要 shell 处理,那么要么使用 shell ,要么直接执行 shell,例如:CMD [ "sh", "-c", "echo $HOME" ]。当使用 exec 并直接执行 shell 时,就像 shell 的情况一样,它是执行环境变量扩展的 shell,而不是 docker。
13. USER¶
用于指定运行 image 时的或运行 Dockerfile 中任何 RUN、CMD 或 ENTRYPOINT 指令指定的程序时的用户名或 UID,默认情况下,container 的运行身份为 root 用户。
语法:
1 | USER <UID>|<UserName> |
需要注意的是,<UID> 可以为任意数字,但实践中其必须为 /etc/passwd 中某用户的有效 UID,否则,docker run 命令将运行失败。
14. HEALTHCHECK¶
HEALTHCHECK 指令告诉 Docker 应该如何进行判断容器的状态是否正常。
在没有 HEALTHCHECK 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。
而自 1.12 之后,Docker 提供了 HEALTHCHECK 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。
当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy。
语法:
1 | HEALTHCHECK [OPTIONS] CMD |
HEALTHCHECK 支持下列选项:
--interval=<间隔>:两次健康检查的间隔,默认为30秒;--timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认30秒;--retries=<次数>:当连续失败指定次数后,则将容器状态视为unhealthy,默认 3 次。
和 CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。
在 HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。
-
命令的返回值决定了该次健康检查的成功与否:
0:成功;1:失败;2:保留,不要使用这个值。
假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl 来帮助判断,其 Dockerfile 的 HEALTHCHECK 可以这么写:
1 | HEALTHCHECK --interval=5s --timeout=3s \ |
15. ONBUILD¶
ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。
语法:
1 | ONBUILD <其它指令> |
三、关于镜像的其他操作¶
把镜像打包成本地 tar 文件:
1 | docker image save IMAGE |
从本地 tar 文件导入镜像:
1 | docker image load IMAGE |
查看镜像历史 BUILD 信息:
1 | docker image history IMAGE |
删除所有当前未被使用的镜像(慎用):
1 | docker image prune IMAGE |
四、使用 Dockerfile 来手动制作镜像¶
这里使用 Dockerfile,来手动定制一个 httpd + php module 的镜像。
为了充分学习,本文将通过两种方式展示,第一种相对简单,第二种使用 ENTRYPOINT 加上脚本的方式。
第一种¶
创建工作目录 /root/httpd,然后创建 index.php 首页文件。
1 | -> cat index.php |
创建 Dockerfile 文件,内容为。
1 | FROM centos:7.6.1810 # 指定 base 镜像 |
构建镜像,标签为 httpd:1。
1 | docker build /root/httpd -t httpd:1 |
运行容器,指定 -d 在后台运行,指定大写 -P 发布 80 端口,这是在 Dockerfile 中定义过的。
1 | docker run --name httpd1 -d -P httpd:1 |
查看端口发布情况。
第一种:
- 在宿主机查看端口发布在
32791上
1 | ss -tnlp | grep docker |
第二种:
- 也可以这样查看
1 | docker container port httpd1 |
访问发布后的地址,宿主机 IP + 端口 http://192.168.50.11:32791/index.php,可以看到 phpinfo 的页面了。

查看容器内进程,httpd 进程的 PID 为 1
1 | [root@e2b06113d109 ~]# ps -ef |
这就完成最基本的 Linux + HTTP + PHP
但是这都是默认配置,用起来可能不合自己的心意,比如要想加个虚拟主机,还能从变量获取 ip 地址和端口,或者改改默认配置,怎么办呢?这就需要用到 ENTRYPOINT 指定启动脚本了。
第二种 ENTRYPOINT¶
在第一种的基础上,添加一个 ENTRYPOINT,将 CMD 和 ENTRYPOINT 结合使用,CMD 中的所有命令都会变成 ENTRYPOINT 中的命令的参数,所以:
ENTRYPOINT ["/root/entrypoint.sh"]
CMD ["/usr/sbin/httpd","-DFOREGROUND"]
就等于
/root/entrypoint.sh /usr/sbin/httpd -DFOREGROUND
Dockerfile 文件
1 | FROM centos:7.6.1810 |
1 | # cat entrypoint.sh |
entrypoint.sh 脚本中用到的 shell 变量语法提示
${variable:-word}:表示如果设置了变量,那么结果将是该值。如果未设置变量,那么 word 将是结果。${variable:+word}:表示如果设置了变量,那么 word 将是结果,否则结果是空字符串。
构建镜像
1 | docker build /root/httpd -t httpd:1 |
根据镜像启动容器,但是这次启动容器时我们通过定义环境变量的方式,将监听端口改成 8080,方法是这样:
在 entrypoint.sh 中,我们定制了一些 httpd 的配置,比如监听端口 ${LISTEN_PORT:-80},如果不指变量,默认就是 80,如果想换成 8080,那么从哪里获得变量呢?
上文说过,ENTRYPOINT 脚本是在容器启动后运行的,那么在这个脚本中,需要引用到变量的话,那必然只有 3 个地方可以获得,1、Dockerfile 中通过 ENV 设定;2、entrypoint.sh 脚本中事先定义了;3、容器内系统的环境变量。
而 1 和 2 我们都没有定义过,那么可以使用的就只有 --> 3、环境变量。
所以我们就可以通过指定 ENV 环境变量的方式,在启动容器时,使用 --env 或 -e 把 8080 传递到容器中。
1 | docker run --rm --name httpd2 -d --env LISTEN_PORT=8080 -p 8080 httpd:1 |
容器启动后登录查看 /etc/httpd/conf.d/vm.conf 文件,看到监听端口确实变成了我们指定的 8080
1 | root@node1 [11:50:18 PM] [~] -> docker exec -it httpd2 /bin/bash |
通过 env 命令可以看到容器内有一个我们传入的环境变量

查看端口发布信息:
1 | docker port httpd2 |
通过页面访问 http://192.168.50.11:32811/index.php

完毕~