什么是 Dockerfile?

Dockerfile 是一个包含用于组合映像的命令的文本文档。可以使用在命令行中调用任何命令。 Docker通过读取 Dockerfile 中的指令自动生成映像。docker build 命令用于从 Dockerfile 构建映像。可以在 docker build 命令中使用 -f 标志指向文件系统中任何位置的 Dockerfile。

img

镜像的定制实际上就是定制镜像的每一层所添加的配置、文件等信息,实际上当我们在一个容器中添加或者修改了一些文件后,我们可以通过 docker commit 命令来生成一个新的镜像,但是这个方法不够直观,没办法追溯我们镜像里面到底有哪些内容,所以实际定制镜像的过程我们很少采用这种方式。而是使用一个名为 Dockerfile 的文本文件来进行镜像定制,我们可以把镜像的每一层修改、安装、构建、操作的命令都写入到这个文件中,一行就对应镜像的一层,这种方法显然要更高级,因为我们有一个文件来直观反映镜像内容,还可以作为版本记录进行跟踪。

1、Docker镜像定制

下面我们以 nginx:latest 镜像为例来定制一个 nginx:v1 镜像,要求默认的应用页面访问的内容是 Hello Docker。首先我们新建一个目录 testnginx,然后在该目录下面新建一个名为Dockerfile的空白文本文件:

# 拉取基础镜像
[root@localhost ~]# docker pull nginx           
Using default tag: latest
latest: Pulling from library/nginx
8559a31e96f4: Downloading 
8d69e59170f7: Download complete 
3f9f1ec1d262: Download complete 
d1f5ff4f210d: Download complete 
1e22bfa8652e: Download complete 
latest: Pulling from library/nginx
8559a31e96f4: Pull complete 
8d69e59170f7: Pull complete 
3f9f1ec1d262: Pull complete 
d1f5ff4f210d: Pull complete 
1e22bfa8652e: Pull complete 
Digest: sha256:21f32f6c08406306d822a0e6e8b7dc81f53f336570e852e25fbe1e3e3d0d0133
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
# 创建Dockerfile文件
[root@localhost ~]# mkdir -p testnginx && cd testnginx && touch Dockerfile

然后向Dockerfile文件中添加如下内容:

FROM nginx

RUN echo 'Hello Docker!' > /usr/share/nginx/html/index.html

这个文件很简单,一共就两行,其中包含两条指令,FROM指令 和 RUN指令。然后我们使用下面的命令开始定制镜像:

# 镜像定制
[root@localhost testnginx]# docker build -t nginx:v1 .
Sending build context to Docker daemon  3.072kB
Step 1/2 : FROM nginx
 ---> 2622e6cca7eb
Step 2/2 : RUN echo 'Hello Docker!' > /usr/share/nginx/html/index.html
 ---> Running in d12e3f7c364a
Removing intermediate container d12e3f7c364a
 ---> 3d003203452f
Successfully built 3d003203452f
Successfully tagged nginx:v1
[root@localhost testnginx]# cat Dockerfile 
FROM nginx

RUN echo 'Hello Docker!' > /usr/share/nginx/html/index.html
[root@localhost testnginx]# 
# 查看已经定制的镜像
[root@localhost testnginx]# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
nginx               v1                  3d003203452f        About a minute ago   132MB
nginx               latest              2622e6cca7eb        2 weeks ago          132MB
[root@localhost testnginx]# 
# 使用定制镜像运行容器
[root@localhost testnginx]# docker run --name webserv -d -p 80:80 nginx:v1
7271d6c651c15c09399f84f3412c8c0e26942781a4882d14832d84e2264f8063
[root@localhost testnginx]# 

上面我们使用定制的镜像运行了一个名为webserv的容器、并绑定了宿主机的80端口、使用nginx:v1镜像。然后我们就可以打开浏览器来验证一下能否看到我们定制的 Hello Docker 信息:

img

2、Dockerfile操作指令

2.1、FROM指令

既然是定制镜像,那么肯定是以一个镜像为基础,在其基础上进行定制,而 FROM 这个指令就是来指定基础镜像的,所以在一个 Dockerfile 文件中 FROM 指令是必备的,并且必须是第一条指令。

在 Docker Hub 上有非常多的高质量的官方镜像,有一些可以直接拿来使用的服务类的镜像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node、openjdk、python、ruby、golang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

img

如果没有找到合适的基础镜像,则可以使用官方提供的一些更为基础的操作系统镜像,比如 ubuntu、debian、centos、alpine 等,这些基础镜像为我们提供了更大的扩展空间,就类似于平时我们在操作系统上面部署自己的服务一样的操作。除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch,这个镜像是一个虚拟的镜像,并不实际存在,他表示一个空白的镜像:

FROM scratch
...

如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。有的同学可能感觉很奇怪,没有任何基础镜像,我怎么去执行我的程序呢,其实对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch会让镜像体积更加小巧。

使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有很多观点认为 Go 是特别适合容器微服务架构的语言的原因之一。

2.2、RUN指令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力,所以 RUN 指令是定制镜像时是最常用的指令之一。其格式有两种:

1、shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式:

RUN echo 'Hello, Docker!' > /usr/share/nginx/html/index.html

2、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 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

2.3、WORKDIR指令

WORKDIR 指令设置 Dockerfile 中的任何 RUN,CMD,ENTRPOINT,COPY 和 ADD 指令的工作目录。如果 WORKDIR 指定的目录不存在,即使随后的指令没有用到这个目录,都会创建该目录。

格式: WORKDIR /path/to/workdir

为了清晰性和可靠性,你应该总是在 WORKDIR 中使用绝对路径,而且单个 Dockerfile 可以使用多次WORKDIR。另外,我们应该使用 WORKDIR 来替代类似于 RUN cd … && do-something 的指令,后者难以阅读、排错和维护。

2.4、ADD指令

Dockerfile 中的 COPY 指令和 ADD 指令都可以将主机上的资源复制或加入到容器镜像中,都是在构建镜像的过程中完成的。

COPY 指令和 ADD 指令的唯一区别在于是否支持从远程 URL 获取资源。COPY 指令只能从执行docker build所在的主机上读取资源并复制到镜像中。而 ADD 指令还支持通过 URL 从远程服务器读取资源并复制到镜像中。

一般来说满足同等功能的情况下,推荐使用COPY指令。ADD 指令更擅长读取本地 tar 文件并解压缩。

COPY 指令能够将构建命令所在的主机本地的文件或目录,复制到镜像文件系统。COPY 指令同样也支持 exec 和 shell 两种格式:

  • exec 格式用法:COPY [“”,… “”],特别适合路径中带有空格的情况。
  • shell 格式用法:COPY
2.5、COPY指令

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 的相对路径;
  • 目标路径如果不存在,则会创建相应的完整路径;
  • 目标路径如果不是一个文件,则必须使用/结束;
  • 路径中可以使用通配符;

3、镜像构建

现在让我们再回到之前定制的 nginx 镜像的 Dockerfile 来。现在我们明白了这个 Dockerfile 的内容,那么让我们来构建这个镜像吧。在 Dockerfile 文件所在目录执行:

[root@localhost testnginx]# docker build -t nginx:v1 .
Sending build context to Docker daemon  3.072kB
Step 1/2 : FROM nginx
 ---> 2622e6cca7eb
Step 2/2 : RUN echo 'Hello Docker!' > /usr/share/nginx/html/index.html
 ---> Running in d12e3f7c364a
Removing intermediate container d12e3f7c364a
 ---> 3d003203452f
Successfully built 3d003203452f
Successfully tagged nginx:v1

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2 中,如同我们之前所说的那样,RUN 指令启动了一个容器 d12e3f7c364a,执行了所要求的命令,并最后提交了这一层 3d003203452f,随后删除了所用到的这个容器 d12e3f7c364a。这里我们使用了 docker build 命令进行镜像构建。其格式为:

docker build [选项] <上下文路径/URL/->

在这里我们通过 -t 参数指定了最终镜像的名称 nginx:v1,构建成功后,我们就可以直接使用这个镜像来运行容器了。好了、到这里我们讲解了关于 Dockerfile 的基础知识,后面我们继续讲解Dockerfile最佳实践的相关内容,比如如何使用多阶段构建镜像、应用解耦、最小化镜像层数等内容。

推荐文章