Py学习  »  docker

Kubernetes-基于Dockerfile构建docker镜像实践

柚子露 • 5 年前 • 241 次点击  

1、Dockerfile文件和核心指令

在Kubernetes中运行容器的前提是已存在构建好的镜像文件,而通过Dockerfile文件构建镜像是最好方式。Dockerfile是一个文本文件,在此文件中的可以设置各种指令,以通过docker build命令自动构建出需要的镜像。Dockerfile文件必需以FROM命令开始,然后按照文件中的命令顺序逐条进行执行。在文件以#开始的内容会被看做是对相关命令的注释。

# Comment 
INSTRUCTION arguments

下面是一个典型的Dockerfile文件,此Dockerfile用于构建一个docker镜像仓库的镜像。Dockerfile文件的格式如下,在文件中对于大小写是不敏感的。但是为了方便的区分命令和参数,一般以大写的方式编写命令。此镜像的基础镜像为alpine:3.4,构建一个docker镜像仓库的镜像:

# Build a minimal distribution container
FROM alpine:3.4
RUN set -ex \
 && apk add --no-cache ca-certificates apache2-utils
COPY ./registry/registry /bin/registry
COPY ./registry/config-example.yml /etc/docker/registry/config.yml
VOLUME ["/var/lib/registry"]
EXPOSE 5000
COPY docker-entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/etc/docker/registry/config.yml"]

1.1 FROM:设置基础镜像

FROM命令为后续的命令设置基础镜像,它是Dockerfile文件的第一条命令,FROM命令的格式如下:

FROM <image>[:<tag>] [AS <name>]

1.2 RUN:设置构建镜像时执行的命令

RUN命令有两种格式,下面是shell格式的RUN命令,在Linux中RUN的默认命令是/bin/sh;在Windows中默认命令为cmd /S /C:

RUN <command>

下面是exec格式的RUN命令:

RUN ["executable", "param1", "param2"]

RUN指令将会在当前镜像顶部的新层中执行任何命令,并提交结果。提交的结果镜像将用于Dockerfile文件的下一步。分层RUN指令和生成提交符合Docker的核心概念,容器可以从镜像历史中的任何点镜像创建,非常类似于源代码管理。

1.3 CMD:设置容器的默认执行命令

CMD指令的主要目的是为容器提供一个默认的执行命令,在一个Dockerfile只能有一条CMD指令,如果设置多条CMD指令,只有最后一条CMD指令会生效。The CMD指令有如下三种格式:

exec格式,这是推荐的格式:

CMD ["executable","param1","param2"]

为ENTRYPOINT提供参数:

CMD ["param1","param2"]

shell格式:

CMD command param1 param2

如果在Dockerfile中,CMD被用来为ENTRYPOINT指令提供参数,则CMD和ENTRYPOINT指令都应该使用exec格式。当基于镜像的容器运行时,将会自动执行CMD指令。如果在docker run命令中指定了参数,这些参数将会覆盖在CMD指令中设置的参数。

1.4 ENTRYPOINT:设置容器为可执行文件

通过ENTRYPOINT指令可以将容器设置作为可执行的文件,ENTRYPOINT 有两种格式:

exec格式,这是推荐的格式:

ENTRYPOINT ["executable", "param1", "param2"]

shell格式:

ENTRYPOINT command param1 param2

下面是是启动一个nginx的例子,端口为80:

docker run -i -t --rm -p 80:80 nginx

docker run <image>命令行参数将会被追加到exec格式的ENTRYPOINT所有元素之后,并将会覆盖使用CMD指定的所有元素。这就允许江参数传递到入口点,例如,docker run <Image> -d 将通过-d 参数传递到入口点。可以使用docker run –entrypoint 字段覆盖“ENTRYPOINT ”指令。如果在Dockerfile文件设置了多条ENTRYPOINT指令,则只会生效最后的一条指令。

1.4.1 ENTRYPOINT指令exec格式示例:

可以使用ENTRYPOINT 的exec形式来设置相对稳定的默认命令和参数,然后使用任何形式的CMD指令来设置可能发生变化的参数。

FROM ubuntu
ENTRYPOINT ["top", "-b"]
CMD ["-c"]

当运行容器是,可以看到只有一个top进程在运行:

$ docker run -it --rm --name test  top -H
top - 08:25:00 up  7:27,  0 users,  load average: 0.00, 0.01, 0.05
Threads:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.1 us,  0.1 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem:   2056668 total,  1616832 used,   439836 free,    99352 buffers
KiB Swap:  1441840 total,        0 used,  1441840 free.  1324440 cached Mem

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
    1 root      20   0   19744   2336   2080 R  0.0  0.1   0:00.04 top

通过docker exec命令,能够参考容器的更多信息。

$ docker exec -it test ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  2.6  0.1  19752  2352 ?        Ss+  08:24   0:00 top -b -H
root         7  0.0  0.1  15572  2164 ?        R+   08:25   0:00 ps aux

下面的Dockerfile显示使用ENTRYPOINT在前台运行Apache:

FROM debian:stable
RUN apt-get update && apt-get install -y --force-yes apache2
EXPOSE 80 443
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]

1.4.2 ENTRYPOINT指令的shell格式

通过为ENTRYPOINT指定文本格式的参数,此参数将在/bin /sh -c 中进行执行。这个形式将使用shell处理,而不是shell环境变量,并且将忽略任何的CMD或docker run运行命令行参数。

FROM ubuntu
ENTRYPOINT exec top -b

1.4.3 CMD和ENTRYPOINT交互

CMD和ENTRYPOINT指令都可以定义容器运行时所执行的命令,下面是它们之间协调的一些规则:

1)在Dockerfile至少需要设置一条CMD或者ENTRYPOINT指令;

2)当将容器作为可执行文件使用时,建议定义ENTRYPOINT指令;

3)CMD作为为ENTRYPOINT命令定义默认参数的一种方式;

4)当使用带有参数的命令运行容器时,CMD将会被覆盖。

下表是显示了不同的ENTRYPOINT / CMD指令组合的命令执行情况:

No ENTRYPOINT ENTRYPOINT exec_entry p1_entry ENTRYPOINT [“exec_entry”, “p1_entry”]
No CMD 报错,这种情况不运行出现 /bin/sh -c exec_entry p1_entry exec_entry p1_entry
CMD [“exec_cmd”, “p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry exec_cmd p1_cmd
CMD [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry p1_cmd p2_cmd
CMD exec_cmd p1_cmd /bin/sh -c exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd

1.5 ENV:设置环境变量

Env指令通过<键>和<值>对设置环境变量。此值将在环境中用于生成阶段中的所有后续指令,并且也可以在许多情况下被替换为内联。
“Env”指令有两种形式。第一种形式,即ENV <Key>  < value >,将一个变量设置为一个值。第一个空间之后的整个字符串将被处理为“<值>”,包括空白字符。

ENV <key> <value>

第二种形式,即ENV <Key>=Value>…,允许一次设置多个变量。注意,第二个表单在语法中使用等号(=),而第一个表单则不使用。与命令行解析一样,引用和反斜杠可用于在值内包含空格。

ENV <key>=<value> ...

例如:

ENV myName="John Doe" myDog=Rex\ The\ Dog \
    myCat=fluffy

和:

ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy

1.6 ADD:添加内容到容器中

ADD指令用于从当前机器或远程URL中的<src>中拷贝文件、目录,并将它们添加到镜像文件系统的<dest>中。在指令中能够设置多个<src>,–chown仅仅在构建Linux容器镜像时起作用,ADD指令有两种格式:

ADD [--chown=<user>:<group>] <src>... <dest>

下面的ADD指令格式可以运行源和目标路径包含空格。

ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]

<src>可以包含通配符,例如:

ADD hom* /mydir/        # 添加所有以"hom"开头的文件到镜像中的/mydir目录下。
ADD hom?.txt /mydir/    # ? is replaced with any single character, e.g., "home.txt"

<dest>是容器一个绝对路径,或者是一个相对于WORKDIR的相对路径,

ADD test relativeDir/          # 添加"test"到容器中`WORKDIR`/relativeDir/
ADD test /absoluteDir/         # 添加"test"到容器中的/absoluteDir/

ADD指令遵循下面的规则:

  • <src>路径必需在构建的上下文中;不能使用 ADD ../someting /someting,这是因为docker build的第一步就是发送上下文目录给docker daemon。
  • 如果<src>是一个URL,并且<dest>不是以斜线结束的情况,则会从URL中下载一个文件,并将其拷贝到<dest>;
  • 如果<src>是一个URL,并且<dest>以斜线结束,则会然后从URL中导出文件名,并将文件下载到<dest>/<filename>中。例如:ADD http://example.com/foobar /,则会在容器的/目录下创建foobar文件,并将URL中foobar文件中的内容复制到容器中/foobar文件中。
  • 如果<src>是一个目录,那么将会拷贝整个目录下的内容,并包括文件系统的元数据。需要注意的时,拷贝时,并不会拷贝目录本身,而只是拷贝目录下内容。
  • 如果<src>是本地的一个压缩(例如:gzip、bzip2、xz等格式)文件,则会对其进行解压缩。对于来自于远程的URL,则不会进行解压缩。
  • 如果<src>是一个普通文件,将会直接将文件和它的元数据拷贝到镜像的<dest>目录下。
  • 如果指定了多个<src>,如果这些<src>中存在目录或使用了通配符,则<Dest>必须是一个目录,并且必须以斜杠/结尾。
  • 如果<dest>不是以斜杠/结尾,它将被认为是一个文件,那么<src>的内容将被写到<dest>中。

1.7 COPY:拷贝内容到镜像中

COPY指令用于从<src>中拷贝文件或目录,并将其添加到镜像文件系统的<path>目录下。在指令中可以指定多个< src>资源,但是文件和目录的路径将被解释为相对于当前构建上下文的资源。COPY指令与ADD指令的功能基本上相似,但ADD能够从远程拷贝,以及解压缩文件。COPY指令有两种格式:

COPY [--chown=<user>:<group>] <src>... <dest>

当目录中存在空格时,请使用下面的格式:

COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]

1.8 WORKDIR:设置当前工作目录

WORKDIR指令用于为RUN、CMD、ENTRYPOINT、COPY和ADD指令设置当前的工作目录。如果WORKDIR不存在,则会自动创建一个,即使后续不使用。

WORKDIR /path/to/workdir

在Dockerfile文件中,可以设置多个WORKDIR指令。如果给定了一个相对路径,则后续WORKDIR设置的路径是相对于上一个相对路径的路径:




    
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

在Dockerfile中,最后的pwd命令输出的为:/a/b/c

1.9 EXPOSE:设置暴露的端口

EXPOSE指令告知docker,容器在运行时将监听指定哪个指定的网络端口。并可以指定端口的协议是TCP或UDP,如果没有指定协议,则默认为TCP协议。EXPOSE指令的格式如下:

EXPOSE <port> [<port>/<protocol>...]

“EXPOSE”指令实际上并不发布端口,它在构建镜像的人员和运行容器的人员之间起着文档告知的作用。要在运行容器时实际发布端口,则需要通过在docker run命令使用-p和-P来发布和映射一个或者多个端口。

1.10 LABEL:设置镜像的元数据信息

LABEL指令拥有为镜像添加一些描述的元数据。LABEL是一系列的键值对,它的格式如下:

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

下面是LABEL指令的示例:

LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."

通过docker inspect命令,可以查看镜像中的标签信息:

"Labels": {
    "com.example.vendor": "ACME Incorporated"
    "com.example.label-with-value": "foo",
    "version": "1.0",
    "description": "This text illustrates that label-values can span multiple lines.",
    "multi.label1": "value1",
    "multi.label2": "value2",
    "other": "value3"
},

1.12 VOLUME:设置存储卷

VOLUME指令用于创建一个带有指定名称的挂载点,并将其标记为来自于本地主机或其他容器的存储卷。该值可以是JSON数组、VOLUME [“/var/log/“],或者是具有多个参数的普通字符串,例如VOLUME /var/log 或 VOLUME /var/log /var/db。
VOLUME ["/data"]

2、构建镜像

在定义后Dockerfile文件,并准备好相关的内容后,就可以通过docker build命令从Dockerfile和上下文构建docker镜像。构建的上下文是位于指定路径或URL中的文件集合。构建过程可以引用上下文中的任何文件。例如,您的构建可以使用复制指令来引用上下文中的文件。

docker build [OPTIONS] PATH | URL | -

2.1 命令选项

名称 默认值 描述
--add-host 添加定制 host-to-IP映射(host:ip)
--build-arg 设置构建时的变量
--cache-from 考虑被作为缓存源的镜像
--cgroup-parent 容器的可选父cgroup
--compress 使用gzip压缩构建上下文
--cpu-period 限制CPU CFS(完全公平调度程序)周期
--cpu-quota 限制CPU CFS(完全公平调度程序)配额
--cpu-shares , -c CPU份额(相对权重)
--cpuset-cpus 允许执行的CPU(0-3,0,1)
--cpuset-mems 允许执行的内存(0-3,0,1)
--disable-content-trust true 忽略镜像验证
--file , -f Dockerfile文件的名称(默认值为”PATH/Dockerfile“)
--force-rm 总是移除中间容器
--iidfile 将镜像ID写入文件
--isolation 容器隔离技术
--label 为镜像设置元数据
--memory , -m 内存限制
--memory-swap Swap限制等于内存加swap:“-1”允许无限swap
--network 在构建期间,为RUN指令设置联网模式
--no-cache 在构建镜像时不使用缓存
--platform 如果服务器是多平台能力的,设置平台
--pull 一直尝试拉取镜像的最新版本
--quiet , -q 抑制构建输出和打印镜像ID
--rm true 成功构建后,移除中间容器
--security-opt 安全选项
--shm-size /dev/shm的大小
--squash 将新建的层挤压成一个新的层
--stream 流连接到服务器,以协商构建的上下文
--tag , -t 为构建的镜像以”name:tab“格式打上标签
--target 设置目标构建阶段进行构建
--ulimit Ulimit选项

1.2 URL参数

URL参数可以引用三种资源:Git存储库、预打包的tabball上下文和纯文本文件,本文主要描述如何使用Git仓库构建镜像。当 URL 参数指向一个Git仓库的位置,仓库将作为构建的上下文。系统的递归获取库及其子模块,提交历史不保存。仓库是首先被拉取到本地主机的临时目录。成功后,此临时目录被发送给Docker daemon作为构建上下文。

Git URL接受的上下文配置,由冒号分隔:进行分割。第一部分表示Git将签出的引用,可以是分支、标签或远程引用。第二部分表示存储库内的子目录,该目录将用作构建上下文。

例如:使用container分支的docker目录构建镜像:

$ docker build https://github.com/docker/rootfs.git#container:docker

下面是通过git构建镜像的合法表达:

建立语法后缀 提交使用 构建上下文使用
myrepo.git refs/heads/master /
myrepo.git#mytag refs/tags/mytag /
myrepo.git#mybranch refs/heads/mybranch /
myrepo.git#pull/42/head refs/pull/42/head /
myrepo.git#:myfolder refs/heads/master /myfolder
myrepo.git#master:myfolder refs/heads/master /myfolder
myrepo.git#mytag:myfolder refs/tags/mytag /myfolder
myrepo.git#mybranch:myfolder refs/heads/mybranch /myfolder

1.3 构建示例

下面是通过本地路径构建一个私有镜像仓库镜像的示例,在此示例中,通过-t设置了镜像的标签为registry:latest;构建上下文为当前执行命令所在的目录,Dockerfile为当前上下文中的文件。

$ docker build -t registry:latest .

下面是通过Git仓库构建镜像的示例:

$ docker build -t regiestry:latest https://github.com/docker/distribution-library-image.git

3、最佳实践

1)不安装不必要的包

为了减少复杂性、依赖性、文件大小和构建时间,避免安装额外的或不必要的包。

2)最小化层的数量

在旧版本的Docker中,最小化镜像中的层数是非常重要,这样可以确保它们的性能。添加以下特征能够减少这种限制:

  • 在docker 1.10和更高版本中,只有RUN、COPY和ADD会创建层。其他指令仅会创建临时的中间镜像,并且不直接增加构建的大小。
  • 在docker17.05和更高版本中,您可以进行多阶段构建,只将需要的工件复制到最终镜像中。这允许您在中间构建阶段中包含工具和调试信息,而不增加最终镜像的大小。

3)解耦应用

每个容器应该只关注一个业务问题。将应用程序分解到多个容器中,从而可以更容易地进行水平扩容和重用。例如,Web应用程序栈可能由三个单独的容器组成,每个容器都有自己的镜像,以解耦的方式管理Web应用程序、数据库和内存缓存。尽最大的努力使容器尽可能保持清晰和模块化。如果容器相互依赖,可以使用docker容器的网络来确保这些容器可以进行通信。

4)排序多行参数

只要有可能,尽量按字母顺序排序多行参数,可以减轻以后的变化。这有助于避免重复包,并使列表更容易更新。
下面是buildpack-deps镜像的一个例子:

RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion

5)利用构建缓存

在构建镜像时,Docker会通过Dockerfile文件中的指令,并按指定的顺序执行每一个指令。在检查每个指令时,Docker会在缓存中寻找可重用的现有图像,而不是创建新的(重复的)图像。
如果您根本不想使用缓存,可以在docker构建命令上使用–no-cache=true选项。但是,如果让Docker使用缓存,则需要了解它何时能找到匹配的镜像。docker遵循的基本规则如下:

  • 从已经存在于缓存中的父镜像开始,将下一条指令与从该基础镜像派生的所有子镜像进行比较,以查看其中是否使用完全相同的指令构建了其中的一个子镜像。如果没有,则缓存无效。
  • 在大多数情况下,简单地将Dockerfile文件中的指令与其中一个子镜像中指令进行比较就足够了。然而,某些指令需要更多的检查和解释。
  • 对于ADD和COPY指令,检查镜像中文件的内容,并为每个文件计算校验和。这些校验和中未考虑文件的最后修改和上次访问时间。在缓存查找期间,将校验和与现有镜像中的校验和进行比较。如果文件中的任何内容(如内容和元数据)发生变化,则缓存被无效。
  • 除了ADD和COPY命令之外,缓存检查并不查看容器中的文件来确定缓存匹配情况。例如,在处理RUN apt-get -y update更新命令时,不检查容器中更新的文件,以确定是否存在缓存命中。在这种情况下,仅使用命令字符串本身来查找匹配项。

一旦缓存失效,所有后续Dockerfile命令都生成新的图像,并且不使用缓存。

6)尽量使用官方的alphine镜像作为基础镜像

只要有可能,使用当前官方的镜像基础。建议使用alpine镜像,因为它尺寸会被严格控制(目前低于5 MB),但仍然是一个完整的Linux发行版。

7)ADD和COPY的使用

虽然ADD和COPY功能类似,一般来说,优先使用COPY,那是因为COPY比ADD更透明。COPY只支持将本地文件拷贝到容器中

如果需要将构建上下文中多个文件拷贝到镜像中,请使用COPY指令分开进行拷贝。

 

参考资料

1.《docker build》地址:https://docs.docker.com/engine/reference/commandline/build/

2.《Dockerfile reference》地址:https://docs.docker.com/engine/reference/builder/

3.《Best practices for writing Dockerfiles》地址:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

作者简介:
季向远,北京神舟航天软件技术有限公司产品经理。本文版权归原作者所有。


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/cb3U3PmAI5
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/21073
 
241 次点击