镜像的定制实际上就是定制每一层所添加的配置、文件,如果把每一层修改、安装、构建、操作的命令都写进一个脚本,用这个脚本来构建、定制镜像,那么那些无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决,
这个脚本就是 Dockerfile( Dockerfile是一个文本文件,其内包含了一条条的指令(instruction),每一条指令构建一层,因此每一条指令的内容就是描述该层应当如何构建)
以定制nginx镜像为例,这次使用Dockerfile来定制
在一个空白目录中,建立一个文本文件,并命名为 Dockerfile:
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile (vim Dockerfile)
其内容为:
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,FROM 和 RUN。
1/ FROM 指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。
而 FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。
在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;
也有一些方便开发、构建、运行各种语言应用的镜像,如 node、openjdk、python、ruby、golang 等。
可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu、debian、centos、fedora、alpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择 现有镜像 为 基础镜像 外,Docker 还存在一个特殊的镜像,名为 scratch。
这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
FROM scratch
...
如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarm、coreos/etcd。
对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,
因此直接 FROM scratch 会让镜像体积更加小巧。
使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一
2/ RUN 执行命令
RUN 指令 是用来 执行命令行命令 的。由于命令行的强大能力,RUN 指令 在定制镜像时是最常用的指令之一。其格式有两种:
-
shell 格式:
RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。RUN echo '<h1>Hello, Docker!</h1>' > /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
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 的行为,就和前面 docker commit 指令那部分手工建镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。
举个反面例子:
像下面这样,创建了7层镜像,这完全是没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等,结果就是产生非常 臃肿、非常多层的镜像,不仅增加了构建部署的时间,而且很容易出错。
这是很多初学Docker的人常犯的一个错误。
Dockerfile 最正确的写法应该是这样:
docker build -t myredis:v1 .(点代表当前目录)
FROM debian:jessie
RUN buildDeps='gcc libc6-dev make' \
&& 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 文件所在的目录执行:
docker build -t nginx:v3 .(别漏掉这个小点)
------------------------------------------------------
这里我们使用了docker build 命令进行镜像构建。其格式为:
docker build [选项] <上下文路径/URL/->
使用 Dockerfile 定制镜像:
Sending build context to Docker daemon 2.048 kB
Step 1/2 : FROM nginx
---> 602e111c06b6
Step 2/2 : RUN echo '<h1>Hello Fan~ study in docker!<h1>' > /usr/share/nginx/html/index.html
---> Running in 5a6ea6fb6e6f
---> c3f72a129b65
Removing intermediate container 5a6ea6fb6e6f
Successfully built c3f72a129b65
在 Step2 中,RUN指令启动了一个容器 5a6ea6fb6e6f ,执行了所要求的命令,并最后提交了这一层 c3f72a129b65 ,随后删除了所用到的这个容器5a6ea6fb6e6f 。
在这里我们指定了最终镜像的名称 -t nginx:v3 ,构建成功后,我们可以像之前运行 nginx:v2 那样来 运行这个镜像,其结果会和 nginx:v2 一样。
镜像构建上下文( Context )
如果注意,会看到 docker build 命令最后有一个 . 。 . 表示当前目录,而 Dockerfile 就在 当前目录,因此不少初学者以为这个路径是在指定 Dockerfile 所在路径,这么理解其实是不准确的。如果 对应上面的命令格式,你可能会发现,这是在指定 上下文路径。那么什么是上下文呢? 首先我们要理解 docker build 的工作原理。
Docker 在运行时分为 Docker 引擎(也就是服务端守护进 程)和 客户端工具。
Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命 令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像 是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。 也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
当进行镜像构建的时候,也并非所有指令都通过 RUN指令 完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY指令、ADD指令 等。
docker build 命令构建镜像并非在本地构建,而是在服务端,也就是docker引擎中构建的
而 docker build 命令构建镜像,其实并非在本地构建, 而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地 文件呢?
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径, docker build 命令得知这 个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。
这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。
比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是 在让 docker build 打包整个硬盘,这显然是使用错误。
一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下 如果该目录下没有所需文件,那 么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore ,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?
这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。 这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile ,而且并不要求必须位于上下文目录中,比如可以用 -f …/Dockerfile.php 参数指定某个文件作为 Dockerfile 。
当然,一般大家习惯性的会使用默认的文件名 Dockerfile ,以及会将其置于镜像构建上下文目录中。