跟我学docker:使用Dockerfile定制镜像
今日课程推荐:https://edu.51cto.com/course/27045.html
镜像的定制实际上就是定制镜像的每一层所添加的配置、文件等信息,实际上当我们在一个容器中添加或者修改了一些文件后,我们可以通过docker commit命令来生成一个新的镜像,但是这个方法不够直观,没办法追溯我们镜像里面到底有哪些内容,所以实际定制镜像的过程我们很少采用这种方式。而是使用一个名为 Dockerfile 的文本文件来进行镜像定制,我们可以把镜像的每一层修改、安装、构建、操作的命令都写入到这个文件中,一行就对应镜像的一层,这种方法显然要更高级,因为我们有一个文件来直观反映我们的镜像内容,还可以作为版本记录进行跟踪。
定制镜像
以我们之前的 nginx 镜像为例,我们现在来定制一个 nginx 镜像,要求默认的应用页面访问的内容是 Hello Docker。
首先我们新建一个目录 testnginx,然后在该目录下面新建一个名为Dockerfile的空白文本文件:
$ mkdir -p testnginx && cd testnginx && touch Dockerfile
然后向Dockerfile文件中添加如下内容:
FROM nginxRUN echo 'Hello Docker!' > /user/share/nginx/html/index.html
这个文件很简单,一共就两行,其中包含两条指令,FROM 和 RUN。
FROM 指令
定制镜像,那么肯定是一个镜像为基础,在其基础上进行定制,而 FROM 这个指令就是来指定基础镜像的,所以在一个 Dockerfile 文件中 FROM 指令是必备的,并且必须是第一条指令。
在 Docker Hub 上有非常多的高质量的官方镜像,有一些可以直接拿来使用的服务类的镜像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node、openjdk、python、ruby、golang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到合适的基础镜像,则可以使用官方提供的一些更为基础的操作系统镜像,比如 ubuntu、debian、centos、alpine 等,这些基础镜像为我们提供了更大的扩展空间,就类似于平时我们在操作系统上面部署自己的服务一样的操作。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch,这个镜像是一个虚拟的镜像,并不实际存在,表示一个空白的镜像:
FROM scratch ...
如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。有的同学可能感觉很奇怪,没有任何基础镜像,我怎么去执行我的程序呢,其实对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有很多观点认为 Go 是特别适合容器微服务架构的语言的原因之一。
RUN 指令
RUN 指令是用来执行命令行命令的。由于命令行的强大能力,所以 RUN 指令是定制镜像时是最常用的指令之一。其格式有两种:
shell 格式:
RUN,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式:RUN echo 'Hello, Docker!' > /usr/share/nginx/html/index.html
exec 格式:
RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:FROM debian:jessie RUN apt-get update RUN apt-get install -y gcc libc6-dev make wget RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" RUN mkdir -p /usr/src/redis RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 RUN make -C /usr/src/redis RUN make -C /usr/src/redis install # 其他指令...
之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 就会新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。
而上面的这种写法,创建了 7 层镜像。这是完全没有必要的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。
UnionFS 层限制
UnionFS 实际上是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。
上面的 Dockerfile 正确的写法应该是这样:
FROM debian:jessie RUN buildDeps='gcc libc6-dev make wget' \ && apt-get update \ && apt-get install -y $buildDeps \ && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \ && mkdir -p /usr/src/redis \ && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \ && make -C /usr/src/redis \ && make -C /usr/src/redis install \ && rm -rf /var/lib/apt/lists/* \ && rm redis.tar.gz \ && rm -r /usr/src/redis \ && apt-get purge -y --auto-remove $buildDeps # 其他指令...
首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。所以我们这里没有使用很多个 RUN 指令来对应不同的命令,而是仅仅使用一个 RUN 指令,并使用&&将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加\的命令换行方式,以及行首#进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。
此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建下载的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。 很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。
WORKDIR 指令
WORKDIR 指令设置 Dockerfile 中的任何 RUN,CMD,ENTRPOINT,COPY 和 ADD 指令的工作目录。如果 WORKDIR 指定的目录不存在,即使随后的指令没有用到这个目录,都会创建该目录。
格式: WORKDIR /path/to/workdir
为了清晰性和可靠性,你应该总是在 WORKDIR 中使用绝对路径,而且单个 Dockerfile 可以使用多次WORKDIR。另外,我们应该使用 WORKDIR 来替代类似于 RUN cd ... && do-something 的指令,后者难以阅读、排错和维护。
ADD & COPY 指令
Dockerfile 中的 COPY 指令和 ADD 指令都可以将主机上的资源复制或加入到容器镜像中,都是在构建镜像的过程中完成的。
COPY 指令和 ADD 指令的唯一区别在于是否支持从远程 URL 获取资源。COPY 指令只能从执行docker build所在的主机上读取资源并复制到镜像中。而 ADD 指令还支持通过 URL 从远程服务器读取资源并复制到镜像中。
一般来说满足同等功能的情况下,推荐使用COPY指令。ADD 指令更擅长读取本地 tar 文件并解压缩。
COPY 指令
COPY 指令能够将构建命令所在的主机本地的文件或目录,复制到镜像文件系统。COPY 指令同样也支持 exec 和 shell 两种格式:
exec 格式用法:
COPY [",特别适合路径中带有空格的情况。shell 格式用法:
COPY
ADD 指令
ADD 指令不仅能够将构建命令所在的主机本地的文件或目录,而且能够将远程 URL 所对应的文件或目录,作为资源复制到镜像文件系统。所以,可以认为 ADD 是增强版的 COPY,支持将远程 URL 的资源加入到镜像的文件系统。同样也支持 exec 和 shell 两种格式用法: * exec 格式用法:ADD [",特别适合路径中带有空格的情况
- shell 格式用法:
ADD
从远程 URL 获取资源,比如:
ADD http://foo.com/bar.go /tmp/main.go
不过需要注意的是对于从远程 URL 获取资源的情况,由于 ADD 指令不支持认证,如果从远程获取资源需要认证,则只能使用RUN wget 或 RUN curl 替代了。
有能力自动解压文件,比如:
ADD /foo.tar.gz /tmp/
上述指令会使 foo.tar.gz 压缩文件解压到容器的 /tmp 目录。
不过一般来说虽然 ADD 指令支持从远程获取资源,但是并不推荐使用,而是建议使用 RUN 指令去执行 wget 或 curl 命令。
比如前面我们定制的 nginx 镜像,可以改成下面的形式:
$ echo 'Hello Docker!' > index.html
然后修改 Dockerfile:
FROM nginx# COPY或者ADD指令都可以COPY index.html /user/share/nginx/html/index.html
注意事项
COPY 指令和 ADD指令的用法非常相似,具体注意事项如下:
- 源路径可以有多个
- 源路径是相对于执行 build 的相对路径
- 源路径如果是本地路径,必须是构建上下文中的路径
- 源路径如果是一个目录,则该目录下的所有内容都将被加入到容器,但是该目录本身不会
- 目标路径必须是绝对路径,或相对于 WORKDIR 的相对路径
- 目标路径如果不存在,则会创建相应的完整路径
- 目标路径如果不是一个文件,则必须使用/结束
- 路径中可以使用通配符