构建中使⽤变量

在实际编写 Dockerfile 时,与搭建环境相关的指令会是其中占有⼤部分⽐例的指令。在搭建程序所需运⾏环境时,难免涉及到⼀些可变变量,例如依赖软件的版本,编译的参数等等。我们可以直接将这些数据写⼊到 Dockerfile 中是完全没有问题,有问题的是这些可变变量我们会经常调整,在调整时就需要我们到 Dockerfile 中找到它们并进⾏更改,如果只是简单的 Dockerfile ⽂件尚且好说,但如果是相对复杂
或是存在多处变量的 Dockerfile ⽂件,这个⼯作就变得繁琐⽽让⼈烦躁了。

在 Dockerfile ⾥,我们可以⽤ ARG 指令来建⽴⼀个参数变量,我们可以在构建时通过构建指令传⼊这个参数变量,并且在 Dockerfile ⾥使⽤它。

例如,我们希望通过参数变量控制 Dockerfile 中某个程序的版本,在构建时安装我们指定版本的软件,我们可以通过 ARG 定义的参数作为占位符,替换版本定义的部分。

FROM debian:stretch-slim
## ......
ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION
## ......
RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
## ......

在这个例⼦⾥,我们将 Tomcat 的版本号通过 ARG 指令定义为参数变量,在调⽤下载 Tomcat 包时,使⽤变量替换掉下载地址中的版本号。通过这样的定义,就可以让我们在不对 Dockerfile 进⾏⼤幅修改的前提下,轻松实现对 Tomcat 版本的切换并重新构建镜像了。

如果我们需要通过这个 Dockerfile ⽂件构建 Tomcat 镜像,我们可以在构建时通过 docker build--build-arg选项来设置参数变量。

docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat

环境变量

环境变量也是⽤来定义参数的东西,与 ARG 指令相类似,环境变量的定义是通过 ENV 这个指令来完成的。

FROM debian:stretch-slim
## ......
ENV TOMCAT_MAJOR 8
ENV TOMCAT_VERSION 8.0.53
## ......
RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"

环境变量的使⽤⽅法与参数变量⼀样,也都是能够直接替换指令参数中的内容。与参数变量只能影响构建过程不同,环境变量不仅能够影响构建,还能够影响基于此镜像创建的容器。环境变量设置的实质,其实就是定义操作系统环境变量,所以在运⾏的容器⾥,⼀样拥有这些变
量,⽽容器中运⾏的程序也能够得到这些变量的值。

另⼀个不同点是,环境变量的值不是在构建指令中传⼊的,⽽是在 Dockerfile 中编写的,所以如果我们要修改环境变量的值,我们需要到 Dockerfile 修改。不过即使这样,只要我们将 ENV 定义放在Dockerfile 前部容易查找的地⽅,其依然可以很快的帮助我们切换镜像环境中的⼀些内容。

由于环境变量在容器运⾏时依然有效,所以运⾏容器时我们还可以对其进⾏覆盖,在创建容器时使⽤ -e 或是 --env选项,可以对环境变量的值进⾏修改或定义新的环境变量。

docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7

事实上,这种⽤法在我们开发中是⾮常常见的。也正是因为这种允许运⾏时配置的⽅法存在,环境变量和定义它的 ENV 指令,是我们更常使⽤的指令,我们会优先选择它们来实现对变量的操作。

另外需要说明⼀点,通过 ENV 指令和 ARG指令所定义的参数,在使⽤时都是采⽤ $ + NAME 这种形式来占位的,所以它们之间的定义就存在冲突的可能性。对于这种场景,⼤家只需要记住,ENV 指令所定义的变量,永远会覆盖 ARG 所定义的变量,即使它们定时的顺序是相反的

合并命令

#方式一
RUN apt-get update; \
		 apt-get install -y --no-install-recommends $fetchDeps; \
		 rm -rf /var/lib/apt/lists/*;
#方式二
RUN apt-get update
RUN apt-get install -y --no-install-recommends $fetchDeps
RUN rm -rf /var/lib/apt/lists/*

很多时候Dockerfile中采用的是第一种方式,而非第二种,为什么?这就要从镜像构建的过程说起了。

看似连续的镜像构建过程,其实是由多个⼩段组成。每当⼀条能够形成对⽂件系统改动的指令在被执⾏前,Docker 先会基于上条命令的结果启动⼀个容器,在容器中运⾏这条指令的内容,之后将结果打包成⼀个镜像层,如此反复,最终形成镜像。

docker images 变量 dockerfile设置变量_tomcat

所以说,我们之前谈到镜像是由多个镜像层叠加⽽得,⽽这些镜像层其实就是在我们 Dockerfile 中每条指令所⽣成的。所以绝⼤多数镜像会将命令合并到⼀条指令中,因为这种做法不但减少了镜像层的数量,也减少了镜像构建过程中反复创建容器的次数,提⾼了镜像构建的速度。

构建缓存

Docker 在镜像构建的过程中,还⽀持⼀种缓存策略来提⾼镜像的构建速度。由于镜像是多个指令所创建的镜像层组合⽽得,那么如果我们判断新编译的镜像层与已经存在的镜像层未发⽣变化,那么我们完全可以直接利⽤之前构建的结果,⽽不需要再执⾏这条构建指令,这就是
镜像构建缓存的原理。

那么 Docker 是如何判断镜像层与之前的镜像间不存在变化的呢?这主要参考两个维度,第⼀是所基于的镜像层是否⼀样,第⼆是⽤于⽣成镜像层的指令的内容是否⼀样。

基于这个原则,我们在条件允许的前提下,更建议将不容易发⽣变化的搭建过程放到 Dockerfile 的前部,充分利⽤构建缓存提⾼镜像构建的速度。另外,指令的合并也不宜过度,⽽是将易变和不易变的过程拆分,分别放到不同的指令⾥。

在另外⼀些时候,我们可能不希望 Docker 在构建镜像时使⽤构建缓存,这时我们可以通过 --no-cache 选项来禁⽤它。

docker build --no-cache ./webapp

搭配 ENTRYPOINT 和 CMD

两个命令都是⽤来指定基于此镜像所创建容器⾥主进程的启动命令的。

两个指令的区别在于,ENTRYPOINT 指令的优先级⾼于 CMD 指令。当 ENTRYPOINTCMD 同时在镜像中被指定时,CMD ⾥的内容会作为 ENTRYPOINT 的参数,两者拼接之后,才是最终执⾏的命令。

这⾥列出所有的 ENTRYPOINTCMD 的组合

docker images 变量 dockerfile设置变量_docker images 变量_02


既然两者都是⽤来定义容器启动命令的,为什么还要分成两个,合并为⼀个指令岂不是更⽅便吗?

这其实在于 ENTRYPOINT 和 CMD 设计的⽬的是不同的。ENTRYPOINT 指令主要⽤于对容器进⾏⼀些初始化,⽽ CMD 指令则⽤于真正定义容器中主程序的启动命令。

另外,我们之前谈到创建容器时可以改写容器主程序的启动命令,⽽这个覆盖只会覆盖 CMD 中定义的内容,⽽不会影响 ENTRYPOINT 中的内容。

这是 Redis 镜像中对 ENTRYPOINT 和 CMD 的定义。

## ......
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
## ......
CMD ["redis-server"]

可以很清晰的看到,CMD 指令定义的正是启动 Redis 的服务程序,⽽ ENTRYPOINT 使⽤的是⼀个外部引⼊的脚本⽂件。
事实上,使⽤脚本⽂件来作为 ENTRYPOINT 的内容是常见的做法,因为对容器运⾏初始化的命令相对较多,全部直接放置在 ENTRYPOINT 后会特别复杂。

以下是Redis 中的 ENTRYPOINT 脚本docker-entrypoint.sh,可以看到其中会根据脚本参数进⾏⼀些处理,⽽脚本的参数,其实就是 CMD 中定义的内容。

#!/bin/sh
set -e
# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "$@"
fi
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "$@"
fi
exec "$@"

这⾥我们要关注脚本最后的⼀条命令,也就是 exec "$@"。在很多镜像的 ENTRYPOINT 脚本⾥,我们都会看到这条命令,其作⽤其实很简单,就是运⾏⼀个程序,⽽运⾏命令就是 ENTRYPOINT 脚本的参数。反过来,由于 ENTRYPOINT 脚本的参数就是 CMD 指令中的内容,所以实际执⾏的就是 CMD ⾥的命令。

所以说,虽然 Docker 对容器启动命令的结合机制为 CMD 作为 ENTRYPOINT 的参数,合并后执⾏ ENTRYPOINT 中的定义,但实际在我们使⽤中,我们还会在 ENTRYPOINT 的脚本⾥使用到 CMD 命令。