DockerFile-构建容器的基石

DockerFile 非常的关键,它不同于 docker commit 的手动命令方式来进行镜像的构建和修改,类似 docker commit 的交互被称为命令式交互。命令式交互是运维一直绕不开的一种,但是随着机器规模的越来越大,工程结构越来越复杂,我们应该规避这种命令式交互的方式,而应该采用声明式交互

从后面 k8s 的发展中,也吸取了这种声明式交互的精华,当然最早你也可以将其追索到 ansible 的 playbook 上。

从上一篇的 docker commit 的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

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

指令也很简单,下面一一介绍:

FROM-指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个在 centos 上安装了 vim 的镜像容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定 基础镜像,因此一个 DockerfileFROM 是必备的指令,并且必须是第一条指令。

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

比如创建一个 php 的官方镜像:

FROM php:7.4-cli
COPY . /usr/src/myapp
WORKDIR /usr/src/myapp
CMD [ "php", "./your-script.php" ]

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu、debian、centos、fedora、alpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

FROM centos

还有一种特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch
...

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

 

RUN-执行命令

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

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。
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 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大
所以一般会将 && 和 \ 结合起来用

RUN yum update && yum install -y vim \
    Python-dev              # 反斜杠换行

所以上面的 dockerfile 可以改写成如下:

FROM debian:stretch

RUN set -x; 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-5.0.3.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

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

 

CMD-容器启动命令

CMD 类似于 RUN 但是两者的时机不同

  • CMD 在 docker run 的时候运行
  • RUN 是在 docker build 的时候运行

特别注意的是,这是指定启动的容器后默认运行的程序,程序运行结束,容器也就结束了,类似做过的 hello-world , 值得注意的是,如果有多个 CMD 命令,只有最后一个才生效。

写法格式如下:

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

上面展示了两种写法,ShellExec。推荐使用 Exec
这里两者的区别是

  • shell:调用的是 /bin/sh -c 'ping localhost’,当外部进行 Ctrl+C 发送信号停止的时候,/bin/sh 并不会转发,所以进程并不会被杀死
  • Exec:调用的是 /bin/ping localhost。注:这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号

下面还是用在 centos 中安装 vim 来作为例子说说这两者在写法上的不同:

shell

RUN apt-get install -y vim
CMD echo “hello world”

exec

RUN [“apt-get”, “install”, “-y”, “vim”]
CMD [“/bin/echo”, “hello world"]

 

ENTERPOINT-入口点

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。
ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。
当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

刚开始看的时候,会有个疑问,为什么需要 enterpoint 呢??

我们试想一下在镜像中,如果引入不同的镜像,恰好每个镜像的最后一步都有 CMD 命令。

调用了 CMD ‘/bin/bash’ 方法,前面的 CMD 会被最后添加的 CMD 覆盖,如果镜像是一个可执行的程序。

例如,这里有个 docker-cmd-ping 的镜像 dockerfile,主要作用就是运行 ping 命令

# 运行 ping 命令
FROM ubuntu:trusty
CMD ping localhost

如果在 run 运行的时候添加一些参数,比如:

$ docker run docker-cmd-ping hostname

docker 启动后并没有执行 ping 命令,而是运行了 hostname

 

 

 

Docker在很多情况下被用来打包一个程序. 想象你有一个用 python 脚本实现的程序, 你需要发布这个 python 程序. 如果用 docker 打包了这个 python 程序, 你的最终用户就不需要安装 python 解释器和 python 的库依赖. 你可以把所有依赖工具打包进 docker 镜像里, 然后用 ENTRYPOINT 指向你的 Python 脚本本身. 当然你也可以用 CMD 命令指向 Python 脚本. 但是通常用 ENTRYPOINT 可以表明你的 docker 镜像只是用来执行这个 python 脚本,也不希望最终用户用这个 docker 镜像做其他操作。

如果这样子写 dockerfile:

# 运行 ping 命令
FROM ubuntu:trusty
ENTRYPOINT ["/bin/ping","-c","3"]
CMD ["localhost"]

在运行的时候就不会变成 hostname,但是又可以定制运行的命令参数:

docker run docker-cmd-ping hostname

以mongo为例,看看 ENTERPOINT 以服务的形式运行容器:

COPY docker-entrypoint.sh /user/local/bin/
ENTRYPOINT [“docker-entrypoint.sh”]
EXPOSE 27017
CMD [“mongod"]

 

LABEL-镜像标签

镜像的标签,标识镜像的版本号啊,描述之类

LABEL maintainer="xxx"
LABEL version="1.0"LABEL description="this is description"

 

WORKDIR-指定工作目录

指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。(WORKDIR 指定的工作目录,必须是提前创建好的)。docker build 构建镜像过程中的,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。

WORKDIR /root     # 指定目录在每一层镜像中都会保留
WORKDIR
/test # 如果没有此目录,会自动创建test目录 WORKDIR demo # 这里的目录会 /test/demo

 

COPY-复制文件

格式:

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

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

COPY package.json /usr/src/app/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/

 

ADD-复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

  • 自动下载url资源文件

比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

  • 可以自动解压缩

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。
在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

 

ENV-环境变量

格式:

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

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

除此之外这里声明的环境变量还可以贯穿整个 dockerfile 文件的每一层镜像层

ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc” && echo “version:${NODE_VERSION}"
posted @ 2021-11-08 15:49  Blackbinbin  阅读(67)  评论(0编辑  收藏  举报