dockerfile 详解

docker 文档

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

FROM 指定基础镜像

FROM 就是指定 基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarmetcd。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN 指令是用来执行命令行命令的。

其格式有两种:

shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样

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

exec 格式:RUN [“可执行文件”, “参数1”, “参数2”],这更像是函数调用中的格式

既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:

FROM debian:stretch 
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-5.0.3.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 也不例外,而上面的这种写法,创建了 7 层镜像。这是完全没有意义的。

Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式。很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

构建镜像

Dockerfile 文件所在目录执行:

# docker build [选项] <上下文路径/URL/-> 
docker build -t nginx:v3 .

镜像构建上下文(Context)

docker build 命令最后有一个  . 表示当前目录,这是在指定 上下文路径

 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的,当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将这个路径下的所有内容打包,然后上传给 Docker 引擎

如果在 Dockerfile 中这么写:

COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

那么为什么会有人误以为  .  是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。但是Dockerfile 并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile

其它 docker build 的用法

直接用 Git repo 进行构建

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1

并且指定默认的 master 分支,构建目录为 /11.1/

用给定的 tar 压缩包构建

$ docker build http://server/context.tar.gz

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

COPY 复制文件

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径>

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

COPY hom* /mydir/ 
COPY hom?.txt /mydir/ 
COPY --chown=55:mygroup files* /mydir/ //选项来改变文件的所属用户及所属组

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

CMD 容器启动命令

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD [“可执行文件”, “参数1”, “参数2″…]
  • 参数列表格式:CMD [“参数1”, “参数2″…]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD 是 /bin/bash,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 “,而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]

使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 CMD service nginx start 会被理解为 CMD [ “sh”, “-c”, “service nginx start”], 因此主进程实际上是 sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。

正确的做法:直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT 入口点 有两个场景

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

场景一:让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

FROM ubuntu:18.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "https://ip.cn" ]

假如我们使用 docker build -t myip . 来构建镜像,我们需要查询当前公网 IP,只需要执行:

$ docker run myip 
当前 IP:61.148.226.66 来自:北京市 联通

跟在镜像名后面的是 command运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD。那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

$ docker run myip curl -s https://ip.cn -i

而使用 ENTRYPOINT 就可以解决这个问题

FROM ubuntu:18.04 
RUN apt-get update \ 
  && apt-get install -y curl \ 
  && rm -rf /var/lib/apt/lists/* 
ENTRYPOINT [ "curl", "-s", "https://ip.cn" ]

直接使用 docker run myip -i  //CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl

场景二:应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD> )作为命令,在脚本最后执行

ENV 设置环境变量

两种格式

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2> …
ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet" 
RUN curl -SLO "https://nodejs.org/dist/v$VERSION/node-v$VERSION-linux-x64.tar.xz" \

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。

Leave a Reply

Your email address will not be published. Required fields are marked *