Tian Jiale's Blog

docker 镜像的创建

docker 镜像是什么?

Docker 镜像 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 不包含 任何动态数据,其内容在构建之后也不会被改变。

分层存储

因为镜像包含操作系统完整的 root 文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 ISO 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。

镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。

分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。

使用 commit 创建镜像

容器是通过镜像创建的,因为镜像是分层存储的文件系统,因此容器实际上是通过在容器中新加入一个存储层来实现容器的文件使用的。

由此可以看出,我们在容器中进行的文件修改实际上保存在新增加的存储层中,所以我们可以使用 commit 来将容器定制成一个镜像。

docker commit 的语法格式为:

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

但实际中我们应该避免使用该方法创建镜像,因为多层的文件结构使得我们在容器新创建的存储层中对前面层内的文件修改并不会直接作用于前面的层中,而是保存在当前层中,如果我们删除了部分文件,该文件并不会在我们的层中消失,而是被标记为删除,此行为会使文件系统愈发臃肿。

另一方面,我们在用镜像的时候总是希望知道该镜像的每一层进行了何种修改,而在 commit 的方法中,修改是不可见的,这对于实际使用和维护是非常不利的,最终会导致该镜像成为垃圾镜像。

使用 Dockerfile 创建镜像

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

Dockerfile 例子:

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

这个 Dockerfile 很简单,一共就两行。涉及到了两条指令,FROMRUN

docker build 命令使用:

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

例如 docker build -t nginx:v3 . 意思是在当前目录下创建名字为 nginx 标签为 v3 的镜像。

FROM :指令基础镜像

可以使用各种官方提供的镜像,同时scratch是一个空白镜像,意味着你不以任何镜像为基础,不需要有操作系统提供运行时支持,Go 语言常见。

RUN :执行命令

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

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

COPY:复制文件

COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

ADD:更高级的文件

源路径可以是 url,但权限默认为 600,不推荐使用。

源路径若为 tar 压缩包,ADD 命令会自动解压压缩文件到目标路径,而只有在这种情况下才推荐使用,其他情况推荐使用 COPY。

CMD:容器启动命令

shell 格式:CMD <命令>
exec 格式:CMD ["可执行文件", "参数1", "参数2"...]

容器无前台后台之分,容器就是为主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

所以容器启动命令应当是前台形式运行而不是启动服务等类型。

ENTRYPOINT:入口点

分为 exec 格式和 shell 格式,用于让镜像变成像命令一样使用和应用运行前的准备工作。

ENV:设置环境变量

ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

ARG:构建参数

格式:ARG <参数名>[=<默认值>] ,与 ENV 不同之处在于,在将来容器运行时是不会存在这些环境变量的。

ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。

VOLUME:定义匿名卷

VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>

对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

EXPOSE:暴露端口

格式为 EXPOSE <端口1> [<端口2>...]

声明容器运行时提供服务的端口,并不会在容器运行时开启端口服务,在 -P 选项时会随即映射该端口。

WORKDIR:指定工作目录

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

USER:指定当前用户

格式:USER <用户名>[:<用户组>]

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

HEALTHCHECK:健康检查

HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令

HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 支持下列选项:

  • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
  • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

ONBUILD:为他人作嫁衣裳

格式:ONBUILD <其它指令>

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

LABEL:为镜像添加元数据

LABEL 指令用来给镜像以键值对的形式添加一些元数据(metadata)。

LABEL <key>=<value> <key>=<value> <key>=<value> ...

SHELL:指令

格式:SHELL ["executable", "parameters"]

SHELL 指令可以指定 RUN ENTRYPOINT CMD 指令的 shell,Linux 中默认为 ["/bin/sh", "-c"]