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指令分开进行拷贝。
本文转自中文社区-Kubernetes-基于Dockerfile构建docker镜像实践