Dockerfile

Dockerfile

容器化之路:谁偷走了我的构建时间

CCE_SWR 发表了文章 • 0 个评论 • 614 次浏览 • 2019-04-11 15:10 • 来自相关话题

随着全面云时代到来,很多公司都走上了容器化道路,老刘所在的公司也不例外。作为一家初创型的互联网公司,容器化的确带来了很多便捷,也降低了公司成本,不过老刘却有一个苦恼,以前每天和他一起下班的小王自从公司上云以后每天都比他早下班一个小时,大家手头上的活都差不多,讲 ...查看全部
随着全面云时代到来,很多公司都走上了容器化道路,老刘所在的公司也不例外。作为一家初创型的互联网公司,容器化的确带来了很多便捷,也降低了公司成本,不过老刘却有一个苦恼,以前每天和他一起下班的小王自从公司上云以后每天都比他早下班一个小时,大家手头上的活都差不多,讲道理不应该呀,经过多番试探、跟踪、调查,终于让老刘发现了秘密的所在。

作为一个开发,每天总少不了要出N个测试版本进行调试,容器化以后每次出版本都需要打成镜像,老刘发现每次他做一个镜像都要20分钟,而小王只要10分钟,对比来对比去只有这个东西不一样!


0411_1.jpg


Storage-Dirver到底是何方神圣?为什么能够导致构建时间上的差异?现在让我们来一窥究竟。

在回答这个问题之前我们需要先回答三个问题——什么是镜像?什么是镜像构建?什么是storage-driver?

什么是镜像?

说到镜像就绕不开容器,我们先看一张来自官方对镜像和容器解释的图片:


0411_2.jpg


看完以后是不是更疑惑了,我们可以这样简单粗暴的去理解,镜像就是一堆只读层的堆叠。那只读层里到底是什么呢,另外一个简单粗暴的解释:里边就是放了一堆被改动的文件。这个解释在不同的storage-driver下不一定准确但是我们可以先这样简单去理解。

那不对呀,执行容器的时候明明是可以去修改删除容器里的文件的,都是只读的话怎么去修改呢?实际上我们运行容器的时候是在那一堆只读层的顶上再增加了一个读写层,所有的操作都是在这个读写层里进行的,当需要修改一个文件的时候我们会将需要修改的文件从底层拷贝到读写层再进行修改。那如果是删除呢,我们不是没有办法删除底层的文件么?没错,确实没有办法删除,但只需要在上层把这个文件隐藏起来,就可以达到删除的效果。按照官方说法,这就是Docker的写时复制策略。

为了加深大家对镜像层的理解我们来举个栗子,用下面的Dockerfile构建一个etcd镜像:


0411_3.jpg


构建完成以后生成了如下的层文件:


0411_4.jpg


每次进入容器的时候都感觉仿佛进入了一台虚机,里面包含linux的各个系统目录。那是不是有一层目录里包含了所有的linux系统目录呢?

bingo答对!在最底层的层目录的确包含了linux的所有的系统目录文件。


0411_5.jpg


上述Dockerfile中有这样一步操作

ADD . /go/src/github.com/coreos/etcd

将外面目录的文件拷到了镜像中,那这一层镜像里究竟保存了什么呢?


0411_6.jpg


打开发现里面就只有

/go/src/github.com/coreos/etcd这个目录,目录下存放了拷贝进来的文件。

到这里是不是有种管中窥豹的感觉,接下来我们再来了解什么是镜像构建,这样基本上能够窥其全貌了。

什么是镜像构建?

通过第一节的内容我们知道了镜像是由一堆层目录组成的,每个层目录里放着这一层修改的文件,镜像构建简单的说就是制作和生成镜像层的过程,那这一过程是如何实现的呢?以下图流程为例:


0411_7.jpg


Docker Daemon首先利用基础镜像ubuntu:14.04创建了一个容器环境,通过第一节的内容我们知道容器的最上层是一个读写层,在这一层我们是可以写入修改的,Docker Daemon首先执行了RUN apt-update get命令,执行完成以后,通过Docker的commit操作将这个读写层的内容保存成一个只读的镜像层文件。接下来再在这一层的基础上继续执行 ADD run.sh命令,执行完成后继续commit成一个镜像层文件,如此反复直到将所有的Dockerfile都命令都被提交后,镜像也就做好了。



这里我们就能解释为什么etcd的某个层目录里只有一个go目录了,因为构建的过程是逐层提交的,每一层里只会保存这一层操作所涉及改动的文件。

这样看来镜像构建就是一个反复按照Dockerfile启动容器执行命令并保存成只读文件的过程,那为什么速度会不一样呢?接下来就得说到storage-driver了。

什么是storage-driver?

再来回顾一下这张图:


0411_8.jpg


之前我们已经知道了,镜像是由一个个的层目录叠加起来的,容器运行时只是在上面再增加一个读写层,同时还有写时复制策略保证在最顶层能够修改底层的文件内容,那这些原理是怎么实现的呢?就是靠storage-driver!

简单介绍三种常用的storage-driver:

  1. AUFS

AUFS通过联合挂载的方式将多个层文件堆叠起来,形成一个统一的整体提供统一视图,当在读写层进行读写的时,先在本层查找文件是否存在,如果没有则一层一层的往下找。aufs的操作都是基于文件的,需要修改一个文件时无论大小都会将整个文件从只读层拷贝到读写层,因此如果需要修改的文件过大,会导致容器执行速度变慢,docker官方给出的建议是通过挂载的方式将大文件挂载进来而不是放在镜像层中。


0411_9.jpg


  1. OverlayFS

OverlayFS可以认为是AUFS的升级版本,容器运行时镜像层的文件是通过硬链接的方式组成一个下层目录,而容器层则是工作在上层目录,上层目录是可读写的,下层目录是只读的,由于大量的采用了硬链接的方式,导致OverlayFS会可能会出现inode耗尽的情况,后续Overlay2对这一问题进行了优化,且性能上得到了很大的提升,不过Overlay2也有和AUFS有同样的弊端——对大文件的操作速度比较慢。


0411_10.jpg


  1. DeviceMapper

DeviceMapper和前两种Storage-driver在实现上存在很大的差异。首先DeviceMapper的每一层保存的是上一层的快照,其次DeviceMapper对数据的操作不再是基于文件的而是基于数据块的。

下图是devicemapper在容器层读取文件的过程:


0411_11.jpg


首先在容器层的快照中找到该文件指向下层文件的指针。

再从下层0xf33位置指针指向的数据块中读取的数据到容器的存储区

最后将数据返回app。



在写入数据时还需要根据数据的大小先申请1~N个64K的容器快照,用于保存拷贝的块数据。



DeviceMapper的块操作看上去很美,实际上存在很多问题,比如频繁操作较小文件时需要不停地从资源池中分配数据库并映射到容器中,这样效率会变得很低,且DeviceMapper每次镜像运行时都需要拷贝所有的镜像层信息到内存中,当启动多个镜像时会占用很大的内存空间。

针对不同的storage-driver我们用上述etcd的dockerfile进行了一组构建测试


0411_1.jpg



注:该数据因dockerfile以及操作系统、文件系统、网络环境的不同测试结果可能会存在较大差异

我们发现在该实验场景下DevivceMapper在时间上明显会逊于AUFS和Overlay2,而AUFS和Overlay2基本相当,当然该数据仅能作为一个参考,实际构建还受到具体的Dockerfile内容以及操作系统、文件系统、网络环境等多方面的影响,那要怎么样才能尽量让构建时间最短提升我们的工作效率呢?

且看下回分解!

创建了一个有ssh服务的容器,如何ssh登录后,可以获取到Dockerfile中的环境变量呢?

徐新坤 回复了问题 • 2 人关注 • 1 个回复 • 2214 次浏览 • 2019-03-14 17:42 • 来自相关话题

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

themind 发表了文章 • 0 个评论 • 3599 次浏览 • 2018-06-18 17:51 • 来自相关话题

#1. Dockerfile文件和核心指令 在Kubernetes中运行容器的前提是已存在构建好的镜像文件,而通过Dockerfile文件构建镜像是最好方式。Dockerfile是一个文本文件,在此文件中的可以设置各种指令,以通过do ...查看全部
#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 [:] [AS ] 

##1.2 RUN:设置构建镜像时执行的命令
RUN命令有两种格式,下面是shell格式的RUN命令,在Linux中RUN的默认命令是/bin/sh;在Windows中默认命令为cmd /S /C:
RUN 

下面是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 命令行参数将会被追加到exec格式的ENTRYPOINT所有元素之后,并将会覆盖使用CMD指定的所有元素。这就允许江参数传递到入口点,例如,docker run
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指令组合的命令执行情况:
1.png

##1.5 ENV:设置环境变量
Env指令通过<键>和<值>对设置环境变量。此值将在环境中用于生成阶段中的所有后续指令,并且也可以在许多情况下被替换为内联。

“Env”指令有两种形式。第一种形式,即ENV < value >,将一个变量设置为一个值。第一个空间之后的整个字符串将被处理为“<值>”,包括空白字符。
ENV  

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

例如:
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中的中拷贝文件、目录,并将它们添加到镜像文件系统的中。在指令中能够设置多个,--chown仅仅在构建Linux容器镜像时起作用,ADD指令有两种格式:
ADD [--chown=:] ... 

下面的ADD指令格式可以运行源和目标路径包含空格。
ADD [--chown=:] ["",... ""]

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

是容器一个绝对路径,或者是一个相对于WORKDIR的相对路径,
ADD test relativeDir/          # 添加"test"到容器中`WORKDIR`/relativeDir/
ADD test /absoluteDir/ # 添加"test"到容器中的/absoluteDir/

ADD指令遵循下面的规则:

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

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

COPY [--chown=:] ... 

当目录中存在空格时,请使用下面的格式:
COPY [--chown=:] ["",... ""]

##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  [/...]

“EXPOSE”指令实际上并不发布端口,它在构建镜像的人员和运行容器的人员之间起着文档告知的作用。要在运行容器时实际发布端口,则需要通过在docker run命令使用-p和-P来发布和映射一个或者多个端口。
##1.10 LABEL:设置镜像的元数据信息
LABEL指令拥有为镜像添加一些描述的元数据。LABEL是一系列的键值对,它的格式如下:
LABEL = = = ...

下面是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 命令选项
11.jpg

##2.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构建镜像的合法表达:
2.png

##2.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
  2. Dockerfile reference
  3. Best practices for writing Dockerfiles

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

Dockerfile 中的 multi-stage(多阶段构建)

博云BoCloud 发表了文章 • 0 个评论 • 1625 次浏览 • 2018-06-06 11:04 • 来自相关话题

在应用了容器技术的软件开发过程中,控制容器镜像的大小可是一件费时费力的事情。如果我们构建的镜像既是编译软件的环境,又是软件最终的运行环境,这是很难控制镜像大小的。所以常见的配置模式为:分别为软件的编译环境和运行环境提供不同的容器镜像。 ...查看全部
在应用了容器技术的软件开发过程中,控制容器镜像的大小可是一件费时费力的事情。如果我们构建的镜像既是编译软件的环境,又是软件最终的运行环境,这是很难控制镜像大小的。所以常见的配置模式为:分别为软件的编译环境和运行环境提供不同的容器镜像。

比如为编译环境提供一个 Dockerfile.build,用它构建的镜像包含了编译软件需要的所有内容,比如代码、SDK、工具等等。同时为软件的运行环境提供另外一个单独的 Dockerfile,它从 Dockerfile.build 中获得编译好的软件,用它构建的镜像只包含运行软件所必须的内容。这种情况被称为构造者模式(builder pattern),本文将介绍如何通过 Dockerfile 中的 multi-stage 来解决构造者模式带来的问题。


常见的容器镜像构建过程

比如我们创建了一个 GO 语言编写了一个检查页面中超级链接的程序 app.go(请从 sparkdev 获取本文相关的代码):

package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"
"golang.org/x/net/html"
)
type scrapeDataStore struct {
Internal int `json:"internal"`
External int `json:"external"`
}
func isInternal(parsedLink [i]url.URL, siteUrl [/i]url.URL, link string) bool {
return parsedLink.Host == siteUrl.Host || strings.Index(link, "#") == 0 ||
len(parsedLink.Host) == 0
}
func main() {
urlIn := os.Getenv("url")
if len(urlIn) == 0 {
urlIn = "https://www.cnblogs.com/"
}
resp, err := http.Get(urlIn)
scrapeData := &scrapeDataStore{}
tokenizer := html.NewTokenizer(resp.Body)
end := false
for {
tt := tokenizer.Next()
switch {
case tt == html.StartTagToken:
token := tokenizer.Token()
switch token.Data {
case "a":
for _, attr := range token.Attr {
if attr.Key == "href" {
link := attr.Val
parsedLink, parseLinkErr := url.Parse(link)
if parseLinkErr == nil {
if isInternal(parsedLink, siteUrl, link) {
scrapeData.Internal++
} else {
scrapeData.External++
}
}
if parseLinkErr != nil {
fmt.Println("Can't parse: " + token.Data)
}
}
}
break
}
case tt == html.ErrorToken:
end = true
break
}
if end {
break
}
}
data, _ := json.Marshal(&scrapeData)
fmt.Println(string(data))
}

下面我们通过容器来构建它,并把它部署到生产型的容器镜像中。

首先构建编译应用程序的镜像:

FROM golang:1.7.3
WORKDIR /go/src/github.com/sparkdevo/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .


把上面的内容保存到 Dockerfile.build 文件中。

接着把构建好的应用程序部署到生产环境用的镜像中:

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]


把上面的内容保存到 Dockerfile 文件中。

最后需要使用一个脚本把整个构建过程整合起来:

#!/bin/sh
echo Building sparkdevo/href-counter:build # 构建编译应用程序的镜像
docker build --no-cache -t sparkdevo/href-counter:build . -f Dockerfile.build # 创建应用程序
docker create --name extract sparkdevo/href-counter:build
# 拷贝编译好的应用程序
docker cp extract:/go/src/github.com/sparkdevo/href-counter/app ./app docker rm -f extractecho Building sparkdevo/href-counter:latest # 构建运行应用程序的镜像
docker build --no-cache -t sparkdevo/href-counter:latest .


把上面的内容保存到 build.sh 文件中。这个脚本会先创建出一个容器来构建应用程序,然后再创建最终运行应用程序的镜像。


把 app.go、Dockerfile.build、Dockerfile 和 build.sh 放在同一个目录下,然后进入这个目录执行 build.sh 脚本进行构建。构建后的容器镜像大小:


微信图片_20180606110034.jpg



从上图中我们可以观察到,用于编译应用程序的容器镜像大小接近 700M,而用于生产环境的容器镜像只有 10.3 M,这样的大小在网络间传输的效率是很高的。

运行下面的命令可以检查我们构建的容器是否可以正常的工作:

$ docker run -e url=https://www.cnblogs.com/ sparkdevo/href-counter:latest
$ docker run -e url=http://www.cnblogs.com/sparkdev/ sparkdevo/href-counter:latest


OK,我们写的程序正确的统计了博客园首页和笔者的首页中超级链接的情况。


微信图片_20180606110040.jpg



采用上面的构建过程,我们需要维护两个 Dockerfile 文件和一个脚本文件 build.sh。能不能简化一些呢? 下面我们看看 docker 针对这种情况提供的解决方案:multi-stage。


在 Dockerfile 中使用 multi-stage


multi-stage 允许我们在 Dockerfile 中完成类似前面 build.sh 脚本中的功能,每个 stage 可以理解为构建一个容器镜像,后面的 stage 可以引用前面 stage 中创建的镜像。所以我们可以使用下面单个的 Dockerfile 文件实现前面的需求:

FROM golang:1.7.3
WORKDIR /go/src/github.com/sparkdevo/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/sparkdevo/href-counter/app .
CMD ["./app"]


把上面的内容保存到文件 Dockerfile.multi 中。这个 Dockerfile 文件的特点是同时存在多个 FROM 指令,每个 FROM 指令代表一个 stage 的开始部分。我们可以把一个 stage 的产物拷贝到另一个 stage 中。本例中的第一个 stage 完成了应用程序的构建,内容和前面的 Dockerfile.build 是一样的。第二个 stage 中的 COPY 指令通过 --from=0 引用了第一个 stage ,并把应用程序拷贝到了当前 stage 中。接下来让我们编译新的镜像:

$ docker build --no-cache -t sparkdevo/href-counter:multi . -f Dockerfile.multi


这次使用 href-counter:multi 镜像运行应用:

$ docker run -e url=https://www.cnblogs.com/ sparkdevo/href-counter:multi$ docker run -e url=http://www.cnblogs.com/sparkdev/ sparkdevo/href-counter:multi



微信图片_20180606110044.jpg



结果和之前是一样的。那么新生成的镜像有没有特别之处呢:


微信图片_20180606110047.jpg



好吧,从上图我们可以看到,除了 sparkdevo/href-counter:multi 镜像,还生成了一个匿名的镜像。因此,所谓的 multi-stage 不过时多个 Dockerfile 的语法糖罢了。但是这个语法糖还好很诱人的,现在我们维护一个结构简洁的 Dockerfile 文件就可以了!

使用命名的 stage

在上面的例子中我们通过 --from=0 引用了 Dockerfile 中第一个 stage,这样的做法会让 Dockerfile 变得不容易阅读。其实我们是可以为 stage 命名的,然后就可以通过名称来引用 stage 了。下面是改造后的 Dockerfile.mult 文件:

FROM golang:1.7.3 as builder
WORKDIR /go/src/github.com/sparkdevo/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/sparkdevo/href-counter/app .
CMD ["./app"]


我们把第一个 stage 使用 as 语法命名为 builder,然后在后面的 stage 中通过名称 builder 进行引用 --from=builder。通过使用命名的 stage, Dockerfile 更容易阅读了。


总结

Dockerfile 中的 multi-stage 虽然只是些语法糖,但它确实为我们带来了很多便利。尤其是减轻了 Dockerfile 维护者的负担(要知道实际生产中的 Dockerfile 可不像 demo 中的这么简单)。需要注意的是旧版本的 docker 是不支持 multi-stage 的,只有 17.05 以及之后的版本才开始支持。

dockerfile编译openresty时候configure报错

回复

李扯火 发起了问题 • 1 人关注 • 0 个回复 • 2057 次浏览 • 2017-05-26 11:12 • 来自相关话题

大家在写 dockerfile 时有啥最佳实践?希望得到大家的建议。

炮灰程序猿 回复了问题 • 9 人关注 • 6 个回复 • 6570 次浏览 • 2017-05-18 00:51 • 来自相关话题

容器和应用程序:扩展、重构或重建?

Rancher 发表了文章 • 1 个评论 • 2076 次浏览 • 2017-03-20 10:33 • 来自相关话题

技术领域是不断变化的,因此,任何应用程序都可能在很短时间内面临过时甚至淘汰,更新换代的速度之快给人的感觉越来越强烈,我们如何使传统应用程序保持活力不落伍?工程师想的可能是从头开始重建传统应用程序,这与公司的业务目标和产品时间表通常是相悖的。如果现阶段正在运行的 ...查看全部
技术领域是不断变化的,因此,任何应用程序都可能在很短时间内面临过时甚至淘汰,更新换代的速度之快给人的感觉越来越强烈,我们如何使传统应用程序保持活力不落伍?工程师想的可能是从头开始重建传统应用程序,这与公司的业务目标和产品时间表通常是相悖的。如果现阶段正在运行的应用程序是正常工作的,这时候你很难找到正当而充分的理由让技术人员花六个月重写应用程序。代码债似乎注定意味着失败。

众所周知,产品开发向来都不是非黑即白那么简单,必须要权衡各方妥协折衷进行,虽然完全重写的可行性不大,但应用程序现代化的长远利益仍然值得重视。虽然许多组织尚未能构建全新的云本地应用程序,但通过使用一些技术比如Docker等容器技术,仍然能够实现传统应用程序的现代化。

这些现代化技术最终可以归纳为三种类别:扩展,重构和重建。在开始介绍它们之前,让我们先来谈谈关于Dockerfile的一些基础知识。

Dockerfile基础知识

对于初学者来说,Docker是一个容器化平台,它包含了基本上可以安装在服务器上的所有东西,即“在一个完整的文件系统中包含一个软件运行所需的一切:代码,运行时,系统工具,系统库”, 而且没有虚拟化平台的开销。

虽然容器的优点和缺点不在本文的讨论范围之内,但还是不得不提,Docker的最大优点之一即只需几行代码就能够快速轻松地启动轻量级、可重复的服务器环境。这种配置是通过一个名为Dockerfile的文件完成的,Dockerfile本质上是Docker用来构建容器镜像的蓝图。在这里,Dockerfile启动了一个简单的基于Python的Web服务器以供参考:


# Use the python:2.7 base image
FROM python:2.7

# Expose port 80 internally to Docker process
EXPOSE 80

# Set /code to the working directory for the following commands
WORKDIR /code

# Copy all files in current directory to the /code directory
ADD . /code

# Create the index.html file in the /code directory
RUN touch index.html

# Start the python web server
CMD python index.py


这个例子比较简单,但已经很能说明关于Dockerfile一些基础知识,涵盖扩展预先存在的镜像、暴露端口以及运行命令和服务。只要基础源代码架构设计合理,此时只需几个指令就可以启动非常强大的微服务。

应用程序现代化

从根本上说,传统应用程序容器化并不困难,困难在于并不是每个应用程序都是建构在容器化的基础上。Docker有一个临时文件系统,这意味着容器内的存储并不持久。如果不采取一些特定措施,保存在Docker容器中的任何文件都可能丢失。此外,并行化是应用程序容器化的面临另一个难题,因为Docker的一个最大优点就在于它能快速适应日益增长的流量需求,这些应用程序需要能够与多个实例并行运行。

综上所述,为使传统应用程序容器化,有以下几种路径:扩展、重构或者重建。哪种方法最适合,则完全取决于组织的需求和资源。

#扩展

一般来说,扩展非容器化应用程序的已有功能在这几种办法中最为简便,但如果处理不好,所做的更改可能会导致技术债显著增加。利用容器技术扩展传统应用程序的最好办法是通过微服务和API。虽然传统应用程序本身并没有被容器化,为使产品实现现代化,可将新特性从基于Docker的微服务中隔离,同时开发遗留代码,易于将来重构或重建。

从高层面来说,对于那些在不久的将来很可能变得落后或必须经历重建的应用程序而言,扩展是很好的选择——不过代码库越老,为适应Docker平台,应用程序的某些部分就越需要彻底重构。

#重构

但有时,通过微服务或API扩展应用程序是不实际甚至不可行的。无论是欠缺要添加的新功能,还是通过扩展添加新功能很困难,重构旧代码库的某些部分都可能是必要的。将当前应用程序的各个现有功能从容器化的微服务中隔离出来,就能轻松完成重构了。例如,将整个社交网络重构到Docker化的应用程序可能是不切实际的,但通过退出运行用户搜索引擎,就能够将各个组件作为单独的Docker容器隔离。

重构传统应用程序另一途径是用于写入日志、用户文件等内容的存储机制。在Docker中运行应用程序的最大障碍之一是临时文件系统。这种情况可以通过几种方式进行处理,最常见的是通过使用基于云的存储方法,如AmazonS3或Google云存储。通过重构文件存储方法以利用这些平台,应用程序可以很容易地在Docker容器中运行而不丢失任何数据。

#重建

当传统应用程序无法支持多个运行的实例时,不从头重建的话,可能无法添加Docker支持。传统应用程序服务周期可以很长,但如果应用程序的架构和设计决策在初始阶段就不够合理的话,则可能影响将来对应用程序的有效重构。意识到即将发生的阻碍对于识别生产率风险至关重要。

大体来说,利用容器技术实现传统应用程序的现代化并没有硬性规则。至于哪种才是最佳决策则要视产品需求和业务需求而定。但是,要想确保应用程序稳定运行而不损失生产力,充分了解哪些决策会如何影响组织长期运行,是至关重要的。

原文来源:Rancher Labs

.NET程序在Linux容器中的演变

龙影随风 发表了文章 • 0 个评论 • 2731 次浏览 • 2017-03-17 13:54 • 来自相关话题

【编者的话】Linux容器技术已被开发人员所熟知,现在.NET程序可以跑在Docker容器中,这为以Windows中心的开发人员带来了好处。 【上海站|3天烧脑式微服务架构训练营】培训内容包括:DevOps、微服务、Spring Cl ...查看全部
【编者的话】Linux容器技术已被开发人员所熟知,现在.NET程序可以跑在Docker容器中,这为以Windows中心的开发人员带来了好处。

【上海站|3天烧脑式微服务架构训练营】培训内容包括:DevOps、微服务、Spring Cloud、Eureka、Ribbon、Feign、Hystrix、Zuul、Spring Cloud Config、Spring Cloud Sleuth等。

本文将首先讨论镜像的构建时间和启动时间,接着会将一个简单的.NET程序运行在基于容器的应用上,然后观察镜像大小的变化,最终缩短镜像的构建和加载时间。此外,代码优化是本文的另一个主题。

现在,.NET开发人员可以无障碍地使用如Docker这样的Linux容器,那么让我们来尝试如何以正确的方式配置一个容器。

可能,文章的标题改成“Linux容器开发人员的演变”会更好。由于.NET可在Linux(以及Windows和macOS)上运行,所以整个世界的Linux容器和微服务已经开放给了.NET开发人员。

有着大量的开发人员,长期的运行记录和优异性能指标的.NET,现在给以Windows为中心的开发人员提供了一个使用Linux容器的机会。

虽然在Linux容器中尝试运行.NET代码是诱人的,同时也会产生一些细微差别,但是这样做是不会错的。你可以很容易地将一些.NET代码推送到镜像中。

毕竟,一切都发生的这么快,一定都很好。 对不对?

事实并非如此。让.NET代码运行在Linux容器中并不是一件简单的事情,但请记住:“先让它工作,然后让它工作得很快。”

在下面的例子中,上文说的“很快”指的是构建镜像所需的时间,启动镜像所需的时间和镜像内部代码的性能。本文将首先讨论镜像的构建时间和启动时间,接着会将一个简单的.NET程序运行在基于容器的应用上,然后观察镜像大小的变化,最终缩短镜像的构建和加载时间。此外,代码优化是本文的另一个主题。
# 短暂的停留
考虑一个非常简单的微服务示例,它只给出一个“Hello world”类型的HTTP响应。也就是说,当在浏览器中填写URL,你就会得到一个包括主机名的Web页面。

我们可从这个代码库中下载源码,并制作第一个Dockerfile(Dockerfile.attempt1),接着使用以下命令构建镜像:
#  docker  build  -t  attempt1   -f   Dockerfile.attempt1   .

然后在容器中运行镜像:
#  docker  run   -d   -p   5000:5000   --name   attempt1   attempt1

将浏览器的URL指向主机的IP地址,情况如下:
01.png

# 数字
第一次构建镜像,一共耗时95秒。其中,下载红帽企业Linux(简称RHEL)镜像与安装.NET SDK,这些文件一共490MB。最终,镜像大小为659MB。

一般而言,镜像的后续构建将更快,因为Docker化的镜像已经在主机上可用。改变源码后,我们再次运行构建。这一次构建镜像,大约耗时50秒,得到了相同大小的镜像,也是659MB。

镜像的大小很重要。因为镜像使用操作系统的存储空间,虽然空间便宜,但它仍然是有限的商品。当定期使用容器时,我们很容易忽略过时的镜像,然而它仍然在占用磁盘。如果你不注意的话,磁盘空间将很快用尽。

如何使镜像尽可能的小?
#移除镜像不需要的部分
使用命令`dotnet restore --no-cache`可以消除任何缓存,这样镜像的大小下降到608.6MB,减少了50.6 MB,同比缩小超过7%。
#在构建镜像之前构建应用
应用是在容器中运行镜像时构建.NET程序的。这耗时大约1.6秒——虽然时间不长,但却是在浪费时间。

在恢复之前插入的`dotnet build`命令,并在构建镜像之前构建应用,这样的话容器将会更快地启动。这个结果可在Dockerfile.attempt3中实现。

与此同时,镜像大小却增加到610.2MB,而我们还得运行`dotnet build`,虽然现在花这个时间,但却可在每次启动容器时受益。
# 运行Dotnet Publish命令
因为容器是一个运行时环境,那我们为什么不使用`dotnet publish`命令发布代码,然后把代码放入镜像呢?如果这样做的话,我们就没必要在镜像中安装.NET程序了。毕竟,我们需要的是一个可在任何地方独立运行的应用。

使用dotnet发布代码,会减少镜像大小和缩短容器启动时间。更改project.json文件,注释掉下图中红框的内容,这告诉编译器此文件为一个平台构建。您可以在下图中看到它:
02.jpg

接下来,我们使用`dotnet publish -c Release -r rheh.7.2-x64`发布代码,这会把所有的编译文件和运行时文件,放入一个文件夹,我们把此文件夹复制到镜像中。

因为我们不再需要安装.NET程序,只要一个包含RHEL文件的基础镜像即可,这样就减少了镜像的大小。这是Dockerfile的第四次迭代——Dockerfile.attempt4:
FROM registry.access.redhat.com/rhel7
RUN yum install -y libunwind
RUN yum install -y libicu
ADD bin/Release/netcoreapp1.0/rhel.7.2-x64/publish/. /opt/app-root/src/
WORKDIR /opt/app-root/src/
EXPOSE 5000
CMD ["/bin/bash", "-c", "/opt/app-root/src/dotnet_docker_msa"]

请注意,`yum install`命令将安装一些.NET需要的依赖文件,然后运行`docker build `命令,最终生成一个694.6MB的镜像。
# 谁需要缓存?
多次运行`yum install`命令,前一次操作将为后一次构建缓存。如果在每个`yum install`命令之后,我们立即清除缓存,效果将会很好。下面是Dockerfile的第五次迭代———Dockerfile.attempt5:
FROM registry.access.redhat.com/rhel7
RUN yum install -y libunwind && yum clean all
RUN yum install -y libicu && yum clean all
ADD bin/Release/netcoreapp1.0/rhel.7.2-x64/publish/. /opt/app-root/src/
WORKDIR /opt/app-root/src/
EXPOSE 5000
CMD ["/bin/bash", "-c", "/opt/app-root/src/dotnet_docker_msa"]

基于Dockerfile.attempt5构建的镜像,其大小减少到293.7MB,这比第一次构建缩小了55%。
# 堆叠命令
对Dockerfile做最后更改,我们需要堆叠`yum install`命令,具体内容如下所示:
FROM registry.access.redhat.com/rhel7
`RUN yum install -y libunwind libicu && yum clean all
`ADD bin/Release/netcoreapp1.0/rhel.7.2-x64/publish/. /opt/app-root/src/
`WORKDIR /opt/app-root/src/
`EXPOSE 5000
`CMD ["/bin/bash", "-c", "/opt/app-root/src/dotnet_docker_msa"]

最终得到的镜像大小为257.5MB,这比第一次构建缩小了60%。

下面是各个Dockerfile构建的镜像大小对比图:
03.jpg

# 总结
在探索新技术与新模式时,我们不能将早期的结果与最优做法相混淆。虽然早期的成功会给我们带来兴奋和鼓励,但它也可能使我们丧失进步的动力。勤奋,然后不断尝试,并且始终接受改进的建议,会帮助我们走的更远。

原文链接:The Evolution of a Linux Container(译者:Jack)

===========================================
译者介绍
Jack,开源软件爱好者,研究方向是云计算PaaS平台与深度学习,现积极活跃于Docker,Kubernetes,Tensorflow社区。

Windows上该如何制作Dockefile

coagent 回复了问题 • 2 人关注 • 1 个回复 • 2081 次浏览 • 2017-03-01 11:13 • 来自相关话题

Dockerfile实践优化建议

ylzhang 发表了文章 • 0 个评论 • 7078 次浏览 • 2017-01-20 18:36 • 来自相关话题

【编者的话】Dockerfile是一种被Docker程序解释的脚本,Dockerfile由一条一条的指令组成,每条指令对应Linux下面的一条命令。Docker程序将这些Dockerfile指令翻译真正的Linux命令。类似于Makefile,Dockerfi ...查看全部
【编者的话】Dockerfile是一种被Docker程序解释的脚本,Dockerfile由一条一条的指令组成,每条指令对应Linux下面的一条命令。Docker程序将这些Dockerfile指令翻译真正的Linux命令。类似于Makefile,Dockerfile有自己书写格式和支持的命令,Docker程序解决这些命令间的依赖关系。下面是resin.io关于Dockerfile编写经验和建议的总结。

上个月,Docker发起了Docker Global Mentor Week 2016,旨在帮助开发者用户提高各项技术水平。在resin.io技术栈中,Docker是一个关键的技术之一,而且我们也积攒了很多与Docker关联的最佳实践经验、注意事项、以及提高resin.io开发经验的小技巧。Docker本身已经有很多优秀的实践范例,但并不是所有的场景都在resin.io使用。根据Global Mentor Week的议题精神,在这篇博客中我们整理了关于resin.io应用程序和硬件设备使用Docker场景的一些常见问题。

文章主要分为两个部分:1,必须在实践中使用的; 2,提示部分,建议使用可以提高代码质量和经验,但是并非时强制的。
# 必须使用部分
以下这些实践经验能在开发中为您缩减痛苦过程。
## 固定软件版本
固定所有依赖的版本是实现良好实践最佳途径。这包括基本映象,从GitHub中提取的代码,代码依赖的库等等。通过版本控制,您可以简化应用程序已知的工作版本。

如果没有版本控制,您的组件很容易改变,导致以前工作的Dockerfile不能再构建。

您可以在resin.io官方Docker Hub拉取基础映象最新的可用版本,可以依据基础映象的Tags查询选择。例如,使用`resin/raspberrypi3-debian`关键字搜索列出映象,按照更新日期排序版本的新旧,应当选择当时最新的jessie-20161119版本而不是jessie版本。
FROM resin/raspberrypi3-debian:jessie-20161119

基础映象的架构会发现变化(这种情况极少,但是也是存在的),而使用日期标记排序,就可以标识处稳定可用的最新映象版本。(这样对于Docker来说,他们就一直可以下载可用版本)

一个棘手的事情是固定操作系统中使用包安装器安装的软件的版本问题,再Debian中,运行apt-get安装特定的版本信息,例如:
RUN apt-get update && \  
apt-get install -yq --no-install-recommends \
i2c-tools=3.1.1-1 \
...

Debian软件包Alpine软件包Fedora软件包及其各自的软件包管理器也是如此。 如果你已经安装了大量的软件包,这需要花更多的时间设置版本信息,但是从长远来看它是值得的。

通常,您将从版本控制(例如从git / GitHub)安装软件,在这种情况下,没有理由不使用由唯一ID(如git的hash / SHA)定义的特定提交或标签 。 下面是一个如何使用git检出代码的特定标记版本的示例:
# Can use tag or commit hash to set MRAAVERSION
ENV MRAAVERSION v1.3.0
RUN git clone https://github.com/intel-iot-devkit/mraa.git && \
cd mraa && \
git checkout -b build ${MRAAVERSION} && \
...

最终,安装版本都来自于不管任何库申请都是固定的版本,不论使用了requirements.txt(Python管理安装模块),package.json(Node.js管理安装模块),Cargo.toml(Rust管理安装模块),或者是其他语言的管理的安装包管理器,这样就总是固定版本(或者是经常锁定冻结)依赖版本号或者唯一提交。
## 自我清理
普遍来讲,加快计算机程序最好的方式之一是消除不必要计算(做的更少)。通常来讲,软件部署也是如此,加快部署和更新的最佳方式不发送不需要的代码。所以,自身来讲从容器中清除不必要的代码,可以提高效率。

什么是不需要的代码? 最常见的是,它们是保存在包管理器中的临时文件或者是在Dockerfile中构建和安装的软件源代码。
在包管理器之后清理的方式取决于在您的基本映像中使用的分发方式。 在Debian和Raspbian的情况下使用的是apt-get,Docker已经有很多建议Dockerfile中使用apt-get。 最后,完成安装步骤,删除临时信息,如下:
RUN apt-get update && \  
apt-get install -yq --no-install-recommends \
\
&& apt-get clean && rm -rf /var/lib/apt/lists/*

上面的最后一行通过apt-get rm删除了设备上不需要的的临时文件。

如果你使用Alpine Linux,apk包管理工具有一个方便的--no-cache选项:
RUN apk add --no-cache 

Fedora系统中,dnf包管理器可以通过apt-get简单处理:
RUN dnf makecache && \  
dnf install -y \
\
&& dnf clean all && rm -rf /var/cache/dnf/*

清除已安装软件的源代码通常非常简单,只需删除在生成过程的早期步骤中创建的目录即可。 为了保持上面的MRAA示例,通过git checkout后通过这个方式执行清理:
ENV MRAAVERSION v1.3.0  
RUN git clone https://github.com/intel-iot-devkit/mraa.git && \
cd mraa && \
git checkout -b build ${MRAAVERSION} && \

make install && \
cd .. && rm -rf mraa

还要确保所有的清理语句在同一个部分运行,否则它们将看起来清除,但最终仍然存在于Docker容器成为残留。
## 组合运行语句
上面的最后一个注释引导必须要做,这就要把逻辑上属于一起操作步骤的语句合并进入Dockerfile中,这样以避免类似缓存和不必要地使用磁盘空间有关常见问题。 首先,由于缓存,您可能会有意外的构建结果。 如果您的apt-get更新步骤是从apt-get install 步骤单独运行的,则前者可能会被缓存,并且不会在您期望更新的时候更新。 如果你分离你的git克隆和实际构建,类似的事情也可能发生。其次,在单独的后续RUN步骤中删除的文件保留在最终容器中,但不可访问(残留)。

Docker文档有更多的注释和建议说明
# 推荐的实现
强烈推荐以下做法,通常情况下这是从优秀到卓越的经验,但是并不一定是一个瓶颈。
## 整理 Dockerfile 语句
Docker尝试缓存您的Dockerfile中尚未更改的所有步骤,但如果更改任何语句,将重做其后的所有步骤。 您可以在构建过程中节省相当多的时间,只要有可能,就尽可能按照最不可能更改的顺序编写Dockerfile。 例如,一般设置,如设置工作目录,启用initsystem,设置维护应该更早发生。
MAINTAINER Awesome Developer   
WORKDIR /usr/src/app
ENV INITSYSTEM on

这些语句执行之后,可以使用操作系统的软件包管理器安装软件,然后编译依赖关系,启用系统服务和其他设置等。 例如,在Dockerfile末尾的这一部分执行,你应该安装Python:
COPY requirements.txt ./  
RUN pip install -r requirements.txt

或者 Node.js 依赖.
COPY package.json ./  
RUN npm install

复制应用程序源代码的做法放应该在最后,这也是最常用的做法。我们可以适用复制命令:
COPY . ./

这样就可以加快构建部署的过程,而且Dockerfile文件的可读性强!上面的例子只供参考,逻辑上实现可以依赖域当前应用程序的部分。
## 使用 .dockerignore
接着上一步,我们总是定义一个.dockerignore文件,这个文件是用来区分源码中那些是非必须的设备文件,那些不用经过Copy ../拷贝步骤。忽略的文件可是是README.md或者其他的文件,或者是图片、文件、其他不需要要求应用程序的功能而又必须放在一个库或者其他原因的文件。
## 使用启动脚本
创建和调试比较大型的项目,我们还有一个建议:不要直接使用CMD命令运行,建议时用一个开始脚本,每次调用运行:
CMD ["bash", "start.sh"]

然后在start.sh脚本文件中你可以使用python app.py之类的命令启动、运行你的应用程序。这样的优势是可以很方便扩展运行脚本文件,增加调试功能,而不用在CMD中一步一步执行。

核心代码发布之前你想增加一些调试信息?仅仅增加几条你想要的几条测试逻辑?

另一方面,你可以使用Resin sync可以提高我们的开发进度。Resin sync可以直接拷贝源代码到正在运行的设备中实现更新(不用重新编译Dockerfile文件),然后重启容器加载新的配置即可。然而,这些只有在Docker容器没有缓存的情况下才能生效,例如通过cmd直接重定向的。
## 创建 Non-Root User
Docker的默认设置中,容器中的代码都是通过Root账户运行的。作为一个良好的预防性安全实践,建议创建一个Non-Root用户,授予它只需要尽可能多的特权即可。

例如:
RUN useradd --user-group --shell /bin/false resin  
USER resin

上面命令是创建一个resin用户,后续的步骤中都使用这个用户。Docker docs上有更详细的说明,或者参考这个博客
# 总结
通过检查我们的编译优化文档Docker最佳实践经验文档(那些已经应用),我们可以更进一步的了解。你可能也想看看Dockerfile Linter的一些优化建议。

原文链接:Dockerfile Tips and Tricks (翻译:ylzhang)

.NET程序在Linux容器中的演变

龙影随风 发表了文章 • 0 个评论 • 2731 次浏览 • 2017-03-17 13:54 • 来自相关话题

【编者的话】Linux容器技术已被开发人员所熟知,现在.NET程序可以跑在Docker容器中,这为以Windows中心的开发人员带来了好处。 【上海站|3天烧脑式微服务架构训练营】培训内容包括:DevOps、微服务、Spring Cl ...查看全部
【编者的话】Linux容器技术已被开发人员所熟知,现在.NET程序可以跑在Docker容器中,这为以Windows中心的开发人员带来了好处。

【上海站|3天烧脑式微服务架构训练营】培训内容包括:DevOps、微服务、Spring Cloud、Eureka、Ribbon、Feign、Hystrix、Zuul、Spring Cloud Config、Spring Cloud Sleuth等。

本文将首先讨论镜像的构建时间和启动时间,接着会将一个简单的.NET程序运行在基于容器的应用上,然后观察镜像大小的变化,最终缩短镜像的构建和加载时间。此外,代码优化是本文的另一个主题。

现在,.NET开发人员可以无障碍地使用如Docker这样的Linux容器,那么让我们来尝试如何以正确的方式配置一个容器。

可能,文章的标题改成“Linux容器开发人员的演变”会更好。由于.NET可在Linux(以及Windows和macOS)上运行,所以整个世界的Linux容器和微服务已经开放给了.NET开发人员。

有着大量的开发人员,长期的运行记录和优异性能指标的.NET,现在给以Windows为中心的开发人员提供了一个使用Linux容器的机会。

虽然在Linux容器中尝试运行.NET代码是诱人的,同时也会产生一些细微差别,但是这样做是不会错的。你可以很容易地将一些.NET代码推送到镜像中。

毕竟,一切都发生的这么快,一定都很好。 对不对?

事实并非如此。让.NET代码运行在Linux容器中并不是一件简单的事情,但请记住:“先让它工作,然后让它工作得很快。”

在下面的例子中,上文说的“很快”指的是构建镜像所需的时间,启动镜像所需的时间和镜像内部代码的性能。本文将首先讨论镜像的构建时间和启动时间,接着会将一个简单的.NET程序运行在基于容器的应用上,然后观察镜像大小的变化,最终缩短镜像的构建和加载时间。此外,代码优化是本文的另一个主题。
# 短暂的停留
考虑一个非常简单的微服务示例,它只给出一个“Hello world”类型的HTTP响应。也就是说,当在浏览器中填写URL,你就会得到一个包括主机名的Web页面。

我们可从这个代码库中下载源码,并制作第一个Dockerfile(Dockerfile.attempt1),接着使用以下命令构建镜像:
#  docker  build  -t  attempt1   -f   Dockerfile.attempt1   .

然后在容器中运行镜像:
#  docker  run   -d   -p   5000:5000   --name   attempt1   attempt1

将浏览器的URL指向主机的IP地址,情况如下:
01.png

# 数字
第一次构建镜像,一共耗时95秒。其中,下载红帽企业Linux(简称RHEL)镜像与安装.NET SDK,这些文件一共490MB。最终,镜像大小为659MB。

一般而言,镜像的后续构建将更快,因为Docker化的镜像已经在主机上可用。改变源码后,我们再次运行构建。这一次构建镜像,大约耗时50秒,得到了相同大小的镜像,也是659MB。

镜像的大小很重要。因为镜像使用操作系统的存储空间,虽然空间便宜,但它仍然是有限的商品。当定期使用容器时,我们很容易忽略过时的镜像,然而它仍然在占用磁盘。如果你不注意的话,磁盘空间将很快用尽。

如何使镜像尽可能的小?
#移除镜像不需要的部分
使用命令`dotnet restore --no-cache`可以消除任何缓存,这样镜像的大小下降到608.6MB,减少了50.6 MB,同比缩小超过7%。
#在构建镜像之前构建应用
应用是在容器中运行镜像时构建.NET程序的。这耗时大约1.6秒——虽然时间不长,但却是在浪费时间。

在恢复之前插入的`dotnet build`命令,并在构建镜像之前构建应用,这样的话容器将会更快地启动。这个结果可在Dockerfile.attempt3中实现。

与此同时,镜像大小却增加到610.2MB,而我们还得运行`dotnet build`,虽然现在花这个时间,但却可在每次启动容器时受益。
# 运行Dotnet Publish命令
因为容器是一个运行时环境,那我们为什么不使用`dotnet publish`命令发布代码,然后把代码放入镜像呢?如果这样做的话,我们就没必要在镜像中安装.NET程序了。毕竟,我们需要的是一个可在任何地方独立运行的应用。

使用dotnet发布代码,会减少镜像大小和缩短容器启动时间。更改project.json文件,注释掉下图中红框的内容,这告诉编译器此文件为一个平台构建。您可以在下图中看到它:
02.jpg

接下来,我们使用`dotnet publish -c Release -r rheh.7.2-x64`发布代码,这会把所有的编译文件和运行时文件,放入一个文件夹,我们把此文件夹复制到镜像中。

因为我们不再需要安装.NET程序,只要一个包含RHEL文件的基础镜像即可,这样就减少了镜像的大小。这是Dockerfile的第四次迭代——Dockerfile.attempt4:
FROM registry.access.redhat.com/rhel7
RUN yum install -y libunwind
RUN yum install -y libicu
ADD bin/Release/netcoreapp1.0/rhel.7.2-x64/publish/. /opt/app-root/src/
WORKDIR /opt/app-root/src/
EXPOSE 5000
CMD ["/bin/bash", "-c", "/opt/app-root/src/dotnet_docker_msa"]

请注意,`yum install`命令将安装一些.NET需要的依赖文件,然后运行`docker build `命令,最终生成一个694.6MB的镜像。
# 谁需要缓存?
多次运行`yum install`命令,前一次操作将为后一次构建缓存。如果在每个`yum install`命令之后,我们立即清除缓存,效果将会很好。下面是Dockerfile的第五次迭代———Dockerfile.attempt5:
FROM registry.access.redhat.com/rhel7
RUN yum install -y libunwind && yum clean all
RUN yum install -y libicu && yum clean all
ADD bin/Release/netcoreapp1.0/rhel.7.2-x64/publish/. /opt/app-root/src/
WORKDIR /opt/app-root/src/
EXPOSE 5000
CMD ["/bin/bash", "-c", "/opt/app-root/src/dotnet_docker_msa"]

基于Dockerfile.attempt5构建的镜像,其大小减少到293.7MB,这比第一次构建缩小了55%。
# 堆叠命令
对Dockerfile做最后更改,我们需要堆叠`yum install`命令,具体内容如下所示:
FROM registry.access.redhat.com/rhel7
`RUN yum install -y libunwind libicu && yum clean all
`ADD bin/Release/netcoreapp1.0/rhel.7.2-x64/publish/. /opt/app-root/src/
`WORKDIR /opt/app-root/src/
`EXPOSE 5000
`CMD ["/bin/bash", "-c", "/opt/app-root/src/dotnet_docker_msa"]

最终得到的镜像大小为257.5MB,这比第一次构建缩小了60%。

下面是各个Dockerfile构建的镜像大小对比图:
03.jpg

# 总结
在探索新技术与新模式时,我们不能将早期的结果与最优做法相混淆。虽然早期的成功会给我们带来兴奋和鼓励,但它也可能使我们丧失进步的动力。勤奋,然后不断尝试,并且始终接受改进的建议,会帮助我们走的更远。

原文链接:The Evolution of a Linux Container(译者:Jack)

===========================================
译者介绍
Jack,开源软件爱好者,研究方向是云计算PaaS平台与深度学习,现积极活跃于Docker,Kubernetes,Tensorflow社区。

Dockerfile实践优化建议

ylzhang 发表了文章 • 0 个评论 • 7078 次浏览 • 2017-01-20 18:36 • 来自相关话题

【编者的话】Dockerfile是一种被Docker程序解释的脚本,Dockerfile由一条一条的指令组成,每条指令对应Linux下面的一条命令。Docker程序将这些Dockerfile指令翻译真正的Linux命令。类似于Makefile,Dockerfi ...查看全部
【编者的话】Dockerfile是一种被Docker程序解释的脚本,Dockerfile由一条一条的指令组成,每条指令对应Linux下面的一条命令。Docker程序将这些Dockerfile指令翻译真正的Linux命令。类似于Makefile,Dockerfile有自己书写格式和支持的命令,Docker程序解决这些命令间的依赖关系。下面是resin.io关于Dockerfile编写经验和建议的总结。

上个月,Docker发起了Docker Global Mentor Week 2016,旨在帮助开发者用户提高各项技术水平。在resin.io技术栈中,Docker是一个关键的技术之一,而且我们也积攒了很多与Docker关联的最佳实践经验、注意事项、以及提高resin.io开发经验的小技巧。Docker本身已经有很多优秀的实践范例,但并不是所有的场景都在resin.io使用。根据Global Mentor Week的议题精神,在这篇博客中我们整理了关于resin.io应用程序和硬件设备使用Docker场景的一些常见问题。

文章主要分为两个部分:1,必须在实践中使用的; 2,提示部分,建议使用可以提高代码质量和经验,但是并非时强制的。
# 必须使用部分
以下这些实践经验能在开发中为您缩减痛苦过程。
## 固定软件版本
固定所有依赖的版本是实现良好实践最佳途径。这包括基本映象,从GitHub中提取的代码,代码依赖的库等等。通过版本控制,您可以简化应用程序已知的工作版本。

如果没有版本控制,您的组件很容易改变,导致以前工作的Dockerfile不能再构建。

您可以在resin.io官方Docker Hub拉取基础映象最新的可用版本,可以依据基础映象的Tags查询选择。例如,使用`resin/raspberrypi3-debian`关键字搜索列出映象,按照更新日期排序版本的新旧,应当选择当时最新的jessie-20161119版本而不是jessie版本。
FROM resin/raspberrypi3-debian:jessie-20161119

基础映象的架构会发现变化(这种情况极少,但是也是存在的),而使用日期标记排序,就可以标识处稳定可用的最新映象版本。(这样对于Docker来说,他们就一直可以下载可用版本)

一个棘手的事情是固定操作系统中使用包安装器安装的软件的版本问题,再Debian中,运行apt-get安装特定的版本信息,例如:
RUN apt-get update && \  
apt-get install -yq --no-install-recommends \
i2c-tools=3.1.1-1 \
...

Debian软件包Alpine软件包Fedora软件包及其各自的软件包管理器也是如此。 如果你已经安装了大量的软件包,这需要花更多的时间设置版本信息,但是从长远来看它是值得的。

通常,您将从版本控制(例如从git / GitHub)安装软件,在这种情况下,没有理由不使用由唯一ID(如git的hash / SHA)定义的特定提交或标签 。 下面是一个如何使用git检出代码的特定标记版本的示例:
# Can use tag or commit hash to set MRAAVERSION
ENV MRAAVERSION v1.3.0
RUN git clone https://github.com/intel-iot-devkit/mraa.git && \
cd mraa && \
git checkout -b build ${MRAAVERSION} && \
...

最终,安装版本都来自于不管任何库申请都是固定的版本,不论使用了requirements.txt(Python管理安装模块),package.json(Node.js管理安装模块),Cargo.toml(Rust管理安装模块),或者是其他语言的管理的安装包管理器,这样就总是固定版本(或者是经常锁定冻结)依赖版本号或者唯一提交。
## 自我清理
普遍来讲,加快计算机程序最好的方式之一是消除不必要计算(做的更少)。通常来讲,软件部署也是如此,加快部署和更新的最佳方式不发送不需要的代码。所以,自身来讲从容器中清除不必要的代码,可以提高效率。

什么是不需要的代码? 最常见的是,它们是保存在包管理器中的临时文件或者是在Dockerfile中构建和安装的软件源代码。
在包管理器之后清理的方式取决于在您的基本映像中使用的分发方式。 在Debian和Raspbian的情况下使用的是apt-get,Docker已经有很多建议Dockerfile中使用apt-get。 最后,完成安装步骤,删除临时信息,如下:
RUN apt-get update && \  
apt-get install -yq --no-install-recommends \
\
&& apt-get clean && rm -rf /var/lib/apt/lists/*

上面的最后一行通过apt-get rm删除了设备上不需要的的临时文件。

如果你使用Alpine Linux,apk包管理工具有一个方便的--no-cache选项:
RUN apk add --no-cache 

Fedora系统中,dnf包管理器可以通过apt-get简单处理:
RUN dnf makecache && \  
dnf install -y \
\
&& dnf clean all && rm -rf /var/cache/dnf/*

清除已安装软件的源代码通常非常简单,只需删除在生成过程的早期步骤中创建的目录即可。 为了保持上面的MRAA示例,通过git checkout后通过这个方式执行清理:
ENV MRAAVERSION v1.3.0  
RUN git clone https://github.com/intel-iot-devkit/mraa.git && \
cd mraa && \
git checkout -b build ${MRAAVERSION} && \

make install && \
cd .. && rm -rf mraa

还要确保所有的清理语句在同一个部分运行,否则它们将看起来清除,但最终仍然存在于Docker容器成为残留。
## 组合运行语句
上面的最后一个注释引导必须要做,这就要把逻辑上属于一起操作步骤的语句合并进入Dockerfile中,这样以避免类似缓存和不必要地使用磁盘空间有关常见问题。 首先,由于缓存,您可能会有意外的构建结果。 如果您的apt-get更新步骤是从apt-get install 步骤单独运行的,则前者可能会被缓存,并且不会在您期望更新的时候更新。 如果你分离你的git克隆和实际构建,类似的事情也可能发生。其次,在单独的后续RUN步骤中删除的文件保留在最终容器中,但不可访问(残留)。

Docker文档有更多的注释和建议说明
# 推荐的实现
强烈推荐以下做法,通常情况下这是从优秀到卓越的经验,但是并不一定是一个瓶颈。
## 整理 Dockerfile 语句
Docker尝试缓存您的Dockerfile中尚未更改的所有步骤,但如果更改任何语句,将重做其后的所有步骤。 您可以在构建过程中节省相当多的时间,只要有可能,就尽可能按照最不可能更改的顺序编写Dockerfile。 例如,一般设置,如设置工作目录,启用initsystem,设置维护应该更早发生。
MAINTAINER Awesome Developer   
WORKDIR /usr/src/app
ENV INITSYSTEM on

这些语句执行之后,可以使用操作系统的软件包管理器安装软件,然后编译依赖关系,启用系统服务和其他设置等。 例如,在Dockerfile末尾的这一部分执行,你应该安装Python:
COPY requirements.txt ./  
RUN pip install -r requirements.txt

或者 Node.js 依赖.
COPY package.json ./  
RUN npm install

复制应用程序源代码的做法放应该在最后,这也是最常用的做法。我们可以适用复制命令:
COPY . ./

这样就可以加快构建部署的过程,而且Dockerfile文件的可读性强!上面的例子只供参考,逻辑上实现可以依赖域当前应用程序的部分。
## 使用 .dockerignore
接着上一步,我们总是定义一个.dockerignore文件,这个文件是用来区分源码中那些是非必须的设备文件,那些不用经过Copy ../拷贝步骤。忽略的文件可是是README.md或者其他的文件,或者是图片、文件、其他不需要要求应用程序的功能而又必须放在一个库或者其他原因的文件。
## 使用启动脚本
创建和调试比较大型的项目,我们还有一个建议:不要直接使用CMD命令运行,建议时用一个开始脚本,每次调用运行:
CMD ["bash", "start.sh"]

然后在start.sh脚本文件中你可以使用python app.py之类的命令启动、运行你的应用程序。这样的优势是可以很方便扩展运行脚本文件,增加调试功能,而不用在CMD中一步一步执行。

核心代码发布之前你想增加一些调试信息?仅仅增加几条你想要的几条测试逻辑?

另一方面,你可以使用Resin sync可以提高我们的开发进度。Resin sync可以直接拷贝源代码到正在运行的设备中实现更新(不用重新编译Dockerfile文件),然后重启容器加载新的配置即可。然而,这些只有在Docker容器没有缓存的情况下才能生效,例如通过cmd直接重定向的。
## 创建 Non-Root User
Docker的默认设置中,容器中的代码都是通过Root账户运行的。作为一个良好的预防性安全实践,建议创建一个Non-Root用户,授予它只需要尽可能多的特权即可。

例如:
RUN useradd --user-group --shell /bin/false resin  
USER resin

上面命令是创建一个resin用户,后续的步骤中都使用这个用户。Docker docs上有更详细的说明,或者参考这个博客
# 总结
通过检查我们的编译优化文档Docker最佳实践经验文档(那些已经应用),我们可以更进一步的了解。你可能也想看看Dockerfile Linter的一些优化建议。

原文链接:Dockerfile Tips and Tricks (翻译:ylzhang)

九个编写Dockerfiles的常见错误

cyeaaa 发表了文章 • 0 个评论 • 10445 次浏览 • 2016-06-16 16:36 • 来自相关话题

【编者的话】我们每天基于Dockerfiles工作;所有运行的代码都来自一系列的Dockerfiles。这篇文章将会讨论编写Dockerfile时人们经常犯的错误以及如何改进。对于Docker专家说,这篇文章里的许多技巧可能会非常明显进而会得到很多的认同。但是 ...查看全部
【编者的话】我们每天基于Dockerfiles工作;所有运行的代码都来自一系列的Dockerfiles。这篇文章将会讨论编写Dockerfile时人们经常犯的错误以及如何改进。对于Docker专家说,这篇文章里的许多技巧可能会非常明显进而会得到很多的认同。但是对于初级到中级开发者,该文章将会是一份很有用的指南,它有助于理清以及加速你们的工作流程。
#1. 执行 apt-get
执行`apt-get install`是每一个Dockerfile都有的东西之一。你需要安装一些外部的包来运行代码。但使用`apt-get`相应地会带来一些问题。

一个是运行`apt-get upgrade` 会更新所有包到最新版本 —— 不能这样做的理由是它会妨碍Dockerfile构建的持久与一致性。

另一个是在不同的行之间运行`apt-get update`与`apt-get install`命令。不能这样做的原因是,只有`apt-get update`的代码会在构建过程中被缓存,而且你需要运行`apt-get install`命令的时候不会每次都被执行。因此,你需要将`apt-get update`跟所要安装的包都在同一行执行,来确保它们正确的更新。

在以下 Golang Dockerfile中`apt-install`命令就是一个不错的例子:
# From https://github.com/docker-library/golang
RUN apt-get update && \
apt-get install -y --no-install-recommends \
g++ \
gcc \
libc6-dev \
make \
&& rm -rf /var/lib/apt/lists/*

#2. 使用ADD而非COPY
`ADD`与`COPY`是完全不同的命令。`COPY`是这两个中最简单的,它只是从主机复制一份文件或者目录到镜像里。`ADD`同样可以这么做,但是它还有更神奇的功能,像解压TAR文件或从远程URLs获取文件。为了降低Dockerfile的复杂度以及防止意外的操作,最好用`COPY`来复制文件。
FROM busybox:1.24

ADD example.tar.gz /add #解压缩文件到add目录
COPY example.tar.gz /copy #直接复制文件

#3. 在一行内添加整个应用目录
明确代码的哪些部分以及什么时候应该放在构建镜像内或许是最重要的事了,它可以显著加快构建速度。

Dockerfile里经常会看到如下这些内容:
# !!! ANTIPATTERN !!!
COPY ./my-app/ /home/app/
RUN npm install # or RUN pip install or RUN bundle install
# !!! ANTIPATTERN !!!

这就意味着每次修改文件之后都需要重新构建那行以下的所有东西。多数情况下(包括上面的例子),它意味着重新安装应用依赖。为了尽可能地使用Docker的缓存,首先复制所有安装依赖所需要的文件,然后执行命令安装这些依赖。在复制剩余文件(这一步尽可能放到最后一行)之前先做这两个步骤,会使代码的变更被快速的重建。
COPY ./my-app/package.json /home/app/package.json # Node/npm packages
WORKDIR /home/app/
RUN npm install
# 或许还要安装python依赖?
COPY ./my-app/requirements.txt /home/app/requirements.txt
RUN pip install -r requirements.txt
COPY ./my-app/ /home/app/

这样做会确保构建尽可能快的执行。
#4. 使用:latest标签
许多Dockerfiles在开头都使用`FROM node:latest`模板,用来从Docker registry拉取最新的镜像。简单地说,使用`latest`标签的镜像意味着如果这个镜像得到更新,那么Dockerfile的构建可能会突然中断。弄清这件事可能会非常难,因为Dockerfile的维护者实际上并没做任何修改。为了防止这种情况,只需要确保镜像使用特定的标签(例如:`node:6.2.1`)。这样就可以确保Dockerfile的一致性。
#5. 构建镜像时使用外部服务
很多人会忽视构建Docker镜像与运行一个Docker容器的区别。在构建镜像时,Docker读取Dockerfile里的命令并创建镜像。在依赖或代码修改之前,镜像是保持不变以及可重复使用的。这个过程完全独立于其它容器。需要与其它容器或服务(如数据库)进行交互则会在容器运行的时候发生。

举一个例子,执行数据库迁移。很多人试图在构建镜像时执行此操作。这样做会导致许多问题。首先,在构建时数据库可能不可用,因为它可能没建在它将要运行的服务器上。其次,你可能想使用同一个镜像来连接不同的数据库(在开发或生产环境中),在这种情况下,如果它在构建过程中,迁移是不能进行的。
# !!! ANTIPATTERN !!!
COPY /YOUR-PROJECT /YOUR-PROJECT
RUN python manage.py migrate
# 尝试迁移数据,但是并不能
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
# !!! ANTIPATTERN !!!

#6. 在Dockerfile开始部分加入EXPOSE和ENV
EXPOSE和ENV是廉价的执行命令。如果你破坏它们的缓存,几乎瞬时就可以重建。所以,最好尽可能晚地声明这些命令。在构建过程中应该直到需要的时候才声明ENV。如果在构建的时候不需要他们,那么应该在Dockerfile的末尾附加`EXPOSE`。

再次查看Golang的Dockerfile,你会看到,所有`ENVS`都是在使用前声明的,并且在最后声明其余的:
ENV GOLANG_VERSION 1.7beta1
ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
ENV GOLANG_DOWNLOAD_SHA256 a55e718935e2be1d5b920ed262fd06885d2d7fc4eab7722aa02c205d80532e3b
RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
&& echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
&& tar -C /usr/local -xzf golang.tar.gz \
&& rm golang.tar.gz
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH

如需修改`ENV GOPATH`或`ENV PATH`,镜像几乎会马上重建成功。
#7. 多个FROM声明
尝试使用多个`FROM`声明来将不同的镜像组合到一起,这样不会起任何作用。Docker仅使用最后一个`FROM`并且忽略前面所有的。

所以如果你有这样的Dockerfile:
# !!! ANTIPATTERN !!!
FROM node:6.2.1
FROM python:3.5
CMD ["sleep", "infinity"]
# !!! ANTIPATTERN !!!

那么`docker exec`进入运行的容器中,会得到下面的结果:
$ docker exec -it d86fcf0775d3 bash
root@d86fcf0775d3:/# which python
/usr/local/bin/python
root@d86fcf0775d3:/# which node
root@d86fcf0775d3:/#

这其实是GitHub上的一个问题:合并不同的镜像,但它看起来不会很快就增加的功能。
#8. 多个服务运行在同一个容器内
这可能是了解Docker的开发者遇到的最大问题。而公认的最佳实践是:每个不同的服务,包括应用,应该在它自己的容器中运行。在一个Docker镜像里面加入多个服务非常容易,但是有一定的负面影响。

首先,横向扩展应用会变得很困难。其次,额外的依赖和层次会使镜像构建变慢。最终,增大了Dockerfile的编写、维护以及调试难度。

当然,像所有的技术建议一样,你需要用你的最佳判断。如果想快速安装一个`Django`+`Nginx`的应用的开发环境,那么让它们运行在同一个容器里面,同时生产环境中有一个不同的Dockerfile,让他们分开运行,是合理可行的。
#9. 在构建过程中使用VOLUME
`Volume`是在运行容器时候加入的,而不是构建的时候。与第五个误区类似,在构建过程中不应该与你声明的`volume`有交互。相反地,你只是在运行容器的时候使用它。例如,如果在以下构建过程中创建文件并且在运行那个镜像时候使用它,一切正常:
FROM busybox:1.24
RUN echo "hello-world!!!!" > /myfile.txt
CMD ["cat", "/myfile.txt"]
...
$ docker run volume-in-build
hello-world!!!!

但是,如果我对一个存储在`volume`上的文件做同样的事,就不会起作用。
FROM busybox:1.24
VOLUME /data
RUN echo "hello-world!!!!" > /data/myfile.txt
CMD ["cat", "/data/myfile.txt"]
...
$ docker run volume-in-build
cat: can't open '/data/myfile.txt': No such file or directory

一个有趣的问题是:如果你前面的任何一个层次声明了一个`VOLUME`(也可能是几个`FROMS`)依然会遇到同样的问题。因此,最好留意一下父类镜像都声明了什么`volume`。如果遇到问题,请使用`docker inspect`检查。
#结论
理解怎样写好一个`Dockerfile`将会是一个漫长的路程,它会带你理解`Docker`是如何工作的,同时也帮助你建立你的基础架构。理解Docker缓存会为你节省好多等待构建完成的时间!

原文链接:9 Common Dockerfile Mistakes (翻译:陈晏娥 校对:田浩浩

===========================================

译者介绍
陈晏娥,鞍钢集团矿业公司信息开发中心运维高级工程师,专注虚拟化技术。

DockOne技术分享(三十七):玩转Docker镜像和镜像构建

徐新坤 发表了文章 • 4 个评论 • 11546 次浏览 • 2015-12-09 10:31 • 来自相关话题

【编者的话】本次分享从个人的角度,讲述对于Docker镜像和镜像构建的一些实践经验。主要内容包括利用Docker Hub进行在线编译,下载镜像,dind的实践,对于镜像的一些思考等。 @Container容器技术大会将于2016年1月 ...查看全部
【编者的话】本次分享从个人的角度,讲述对于Docker镜像和镜像构建的一些实践经验。主要内容包括利用Docker Hub进行在线编译,下载镜像,dind的实践,对于镜像的一些思考等。

@Container容器技术大会将于2016年1月24日在北京举行,来自爱奇艺、微博、腾讯、去哪儿网、美团云、京东、蘑菇街、惠普、暴走漫画等知名公司的技术负责人将分享他们的容器应用案例。
前言
本次分享主要是从个人实践的角度,讲述本人对于Docker镜像的一些玩法和体会。本文中大部分的内容都还处于实验的阶段,未经过大规模生产的实践。特此说明。思虑不全或者偏颇之处,还请大家指正。

镜像应该算是Docker的核心价值之一。镜像由多层组成。那么对于一个层来说,就有了两个角度来看待。一个角度是把这层当做一个独立的单位来看,那么这一个层其实主要是包含了文件和配置两个部分。另一个角度则是把这一层和它的所有父层结合起来看,那么这个整体则是代表了一个完整的镜像。

本文所述的Docker镜像,主要是指的从Dockerfile构建出来的镜像。

现在已经有了Docker Hub等多家公有容器服务供应商,为我们提供了非常便捷的镜像构建服务。我们不再需要在本地运行docker build而是可以借用他们的服务实现方便的镜像构建。下文中以Docker Hub为例,介绍一些非常规的用法。各位在实践中可以使用国内的多家容器服务提供商,如DaoCloud等。
Docker Hub之在线编译
众所周知,Docker镜像可以用来描述一个APP的runtime。比如我们构建一个Tomcat的镜像,镜像里包含了运行Tomcat的环境以及依赖。但是我们再细看,其实Docker镜像不仅仅是一个runtime,而是提供了一个环境,一个软件栈。从这个角度上来说,镜像不仅仅可以用来提供APP进行运行,还可以提供诸如编译的环境。

用Docker来进行编译,这个应该来说不是什么新奇玩法。因为Docker源码的编译就是通过这种方式来获得的。Docker有对应的Dockerfile。可以利用这个来完成代码的编译。

这里我举个例子。这里有一个写的Dockerfile。test.c是一个输出hello world的c语言源文件。
	FROM centos:centos6
RUN yum install -y gcc
ADD test.c /
RUN gcc /test.c

构建这个镜像,由于最后一步是编译命令gcc/test.c,所以编译过程会在Docker Hub上进行执行。

我们可以通过编写Dockerfile,使得整个编译过程都托管在Docker Hub上。如果我们提交了新的代码,需要重新编译,那么只需要重新构建镜像即可。
镜像下载
在v1版本中,Docker Client是串行下载镜像的各层。对于docker pull的过程进行分析,可以看到Docker Client总共有这样几个步骤:

* /v1/repositories/{repository}/tags/{tag} 获取tag的id,
* /v1/images/{tag_id}/ancestry 获取tag的各层的id
* /v1/images/{layer_id}/json 依次获取各层对应的配置文件json
* /v1/images/{layer_id}/layer 依次获取各层对应的镜像数据layer

Docker Hub的镜像数据,并不是在自己的服务器中存储,而是使用的亚马逊的s3服务。因此在调用/v1/images/{layer_id}/layer接口,拉取镜像的layer数据时,会返回302,将请求重定向到亚马逊的s3服务上进行下载。

为了方便下载,我自己写了个小程序,使用HTTP协议即可完全模拟Docker Client的整个过程。自己写的好处在于你可以依次获取tag的ID,各层的ID,以及所有层的配置,进而一次性将所有层对应的镜像数据存储在亚马逊的s3地址获取到,然后可以进行并行下载。如果单层下载失败,只需要重新下载这一层即可。当所有的层在本地下载完毕后。然后打成tar包,再使用Docker Client进行load即可。

对于上文中所说的在线编译,那么我们其实只关心编译出来的相关文件。如刚刚的举例,我们其实只需要获取镜像的最后一层就可以了。那么使用自己写的工具,可以仅仅把最后一层下载下来。下载下来的tar包进行解包,就可以直接获取出编译结果,即编译过程生成的相关文件了。Docker Hub就成为了我们的一个强大的在线编译器。

注:这里说的镜像下载过程是针对的Registry v1版本。Docker Hub在不久之后即将全面结束v1的服务。目前国内的几家容器服务提供商还可以支持v1。该方法同样有效。v2的协议和代码我还没学习,后面研究之后再同大家分享。
镜像层合并
镜像层合并这个话题一直是一个有争议的话题。过长的Dockefile会导致一个冗长的镜像层数。而因为镜像层数过多(比如十几层,几十层),可能会带来的性能和稳定性上的担忧也不无道理,但是似乎Docker社区一直不认为这是一个重要的问题。所以基本上对于镜像层合并的PR最后都被拒了。但是这不影响我们在这里讨论他的实现。

我为Dockerfile增加了两个指令。TAG和COMPRESS。

TAG功能类似于`docker build -t`的参数。不过`build -t`只能给Dockerfile中的最后一层镜像打上tag。新增加的TAG指令可以在build生成的中间层也用标签记录下来。比如
	FROM centos:centos6
RUN yum install -y sshd
TAG sshd:latest
ADD test /
CMD /bin/bash

这个TAG功能相当于使用下面的Dockerfile生成了这样的一个镜像,并打上了sshd:latest的标签。
	FROM centos:centos6
RUN yum install -y sshd

COMPRESS功能实现了一个镜像多层合并的功能。比如下面这个Dockerfile:
	FROM centos:centos6
RUN yum install -y sshd
ADD test /
CMD /bin/bash
COMPRESS centos:centos6

我们知道这里假设`RUN yum install -y sshd`,ADD test /, CMD /bin/bash生成的镜像层为a、b、c。那么COMPRESS的功能目标就是将新增的a、b、c的文件和配置合并为一个新的层d,并设置层d的父亲为镜像centos:centos6。层d的配置文件可以直接使用层c的配置文件。合并的难点在于如何计算层d的文件。

这里有两种做法,一种是把层a、b、c中的文件按照合并的规则合并起来。合并的规则包括子层和父层共有的文件则使用子层的,没有交叉的文件则全部做为新添加的。这种方法效率较低,在需要合并的层数过多的时候,会极为耗时。

另外一种思路则较为简单,不需要考虑中间总共有多少层。直接比较centos:centos6镜像和c镜像(c镜像是指由c和其所有父层组成的镜像),将两者的所有文件做比较,两者的diff结果即为新层d。

最终,我采用了后者作为COMPRESS的实现。镜像的合并缩减了层数,但是弊端在于将生成镜像的Dockerfile信息也消除了(使用Dockerfile生成的镜像,可以通过docker history进行回溯)。
dind
dind(Docker in Docker),顾名思义就是在容器里面启动一个Docker Daemon。然后使用后者再启动容器。dind是一种比较高级的玩法,从另一个角度来说也是一种有一定风险的玩法。dind巧妙的利用了Docker的嵌套的能力,但是令人颇为担心的是底层graph driver在嵌套后的性能和稳定性。所以dind我并不推荐作为容器的运行环境来使用(RancherOS其实是使用了这种方式的),但是使用其作为构建镜像的环境,可以进行实践。毕竟构建失败的后果没有运行时崩溃的后果那么严重。

之所以会用到dind,是因为如果用于镜像构建,那么直接使用多个物理机,未免比较浪费。因为构建并不是随时都会发生的。而使用dind的方式,只需在需要的时候申请多个容器,然后再在其上进行构建操作。在不需要时候就可以及时释放容器资源,更加灵活。

制作dind的镜像需要一个CentOS的镜像(其他暂未实践过,fedora/ubuntu也都可以做),和一个wrapdocker的文件。wrapdocker的主要作用是容器启动后为Docker Daemon运行时准备所需的环境。

因为容器启动后,Docker还需要一些环境才能启动daemon。比如在CentOS下,需要wrapdocker把cgroup等准备好。使用CentOS的镜像创建一个容器后,安装Docker等Docker需要的组件后,然后把wrapdocker ADD进去。并把wrapdocker添加为ENTRYPOINT或者CMD。然后将容器commit成为镜像,就获得了一个dind的镜像。使用dind的镜像时需要使用privileged赋予权限,就可以使用了。

熟悉Docker源码的同学应该知道,dind其实并不陌生。在Docker项目里,就有这样一个dind的文件。这个dind文件其实就是一个wrapdocker文件。在Docker进行集成测试时,需要使用该文件,协助准备环境以便在容器内部启动一个Daemon来完成集成测试。

如果对于dind有兴趣,可以参考jpetazzo中的Dockerfile和wrapdocker,构建自己的dind镜像。

dind中Docker的使用跟普通Docker一样。不再赘述。
关于镜像的思考
Docker镜像由若干层组成。而其中的每一层是由文件和配置组成的。如果把层与层之间的父子关系,看做一种时间上的先后关系,那么Docker镜像其实与Git十分的相像。那么从理论上来说,Git的若干功能,比如merge、reset、rebase功能其实我们都可以在Docker的构建过程中予以实现。比如上文中的COMPRESS功能,就类似于Git的merge。理论上,Docker镜像其实也可以拥有Git般强大的功能。从这点上来说,Docker镜像的灵活性就远高于KVM之类的镜像。

在这里,不得不抱怨几句。Docker的维护者们对于dockerfile或者说Docker的构建过程并没有给予非常积极的态度,予以改善。当然这也可能是由于他们的更多的关注点集中在了runC、libnetwork、Orchestration上。所以没有更多的人力来完善Docker构建的工具,而是寄希望于社区能自己增加其他的tool来丰富Docker的构建过程。

所以很多时候,docker build的功能并不尽如人意。比如一直呼声很高的Docker镜像压缩功能,几经讨论,终于无果而终。又比如在build过程中,使用--net参数来使得可以控制build过程中容器使用的网络。该讨论从今年的一月份开始讨论,至今仍未定论结贴。大家可以去强势围观。地址在这里

这里特别说一下,在CentOS 6下,dind不能使用网桥(centos7可以支持),所以在CentOS 6下使用dind,进行docker build,需要指定网络--net=host的方式。

所以很多功能并不能等待Docker自己去完善,只好自己动手开发。其实熟悉了Docker源码后,关于docker build这方面的开发难度并不是很大。可以自己去实现。读一下孙宏亮同学的《Docker源码分析》,会很快上手。
Q&A
Q:京东私有云是基于OpenStack+Docker吗,网络和存储的解决方案是什么?

A:是的。私有云网络使用的是VLAN。并没有使用租户隔离,主要保证效率。存储使用的是京东自己的存储。



Q:那个镜像压缩,有什么好处?

A: 镜像压缩或者说合并,主要是减少层数,减少担忧。其实目前看,好处并不明显。因为层数过多带来的更多的是担忧,但没有确凿证据表明会影响稳定。



Q:在线编译应用广泛吗?我们一般可能更关注最后的结果。有很多代码都是先在本地编译,成功后,再发布到镜像中的。

A:这个玩法应该说并不广泛。主要是我自己玩的时候,不想自己去拉镜像的全部层,只关注编译结果。所以这样玩



Q:对于Docker镜像的存储京东是使用什么方式实现的分布式文件系统京东Docker上有使用吗能否介绍下?

A:镜像存储使用的是官方的registry。v1版本。registry后端是京东自研的JFS存储。



Q:你之前提到了“镜像的合并缩减了层数,但是弊端在于将生成镜像的Dockerfile信息也消除了(使用Dockerfile生成的镜像,可以通过docker history进行回溯)。”那如果使用了Compress之后,应该如何进行回溯?还是说需要舍弃这部分功能?

A:是的,确实没办法回溯了,所以要舍弃了。不过反过来想,其实如果Dockerfile的ADD和COPY之类的功能,就算能回溯,其实意义也不大。所以我认为保存Dockerfile更有意义。



Q:为什么不采用将要执行的命令做成脚本,直接add进去执行这种,也能减少层数?

A:这种方法也是可行的。只是Dockerfile更显式一些。同理,其实只要你做好镜像,直接export出去,就可以得到所有文件了。再配上配置文件。这样整个就只有一层了。




Q:我平时在,测试的时候并没-有压缩过,也不知道,压缩会带来什么风险,但是,看你刚才说有可能会带来一定的风险。 你们遇到过么?

A:因为我们的镜像都做过合并层,所以层数并不多。不合并会带来什么风险,其实更多的是出于性能和稳定性上的担忧。这种担忧可能是多余的。但是我们宁愿选择谨慎一些。



Q:镜像的合并方面怎么样能方便的减小镜像的大小,我做的镜像有些都在1G以上?

A:减少镜像大小主要还是靠去除不必要的文件。合并只能减少冗余文件,如果每层的文件都不相同,合并并不会缩小镜像的大小。



Q:网络这个使用VLAN能说详细一些吗,是每个容器都有一个和宿主机同网段的真实的物理IP吗?

A:是的。每个容器都有一个真实的IP。跟宿主机网段不同。是单独的容器网络。这个可以参考neutron中的Vlan实现。



Q:还有,把镜像压缩我也觉,但是像你那样把父镜像整个合并成新镜像这点我觉得有点问题,毕竟大家玩容器时都是在基础镜像上添加东西,你把常用的镜像为了压缩生成一个一次性的镜像,以后再使用基础镜像做其他业务时那不还得重新下载基础镜像?

A:镜像合并其实主要还是为了获得一个基础镜像。然后大家在基础镜像上添加东西。基础镜像相对来说,不会轻易改变。



Q:在你们的实践中,大规模部署容器时,每个节点都会从Registry节点下载镜像,给网络带来的压力大吗?

A:我们做了一些优化。首先,大部分业务使用的镜像会提前推送到每个Docker节点上。即使节点没有,Registry后端接的是京东的JFS,通过优化,临时去下载的时候可以直接从JFS去拿镜像数据。所以网络压力并不大。



Q:镜像压缩或者合并之后,镜像的层次减少了,但每层镜像不是变大了吗,这对于发布不是会占用带宽降低效率吗?

A:这个问题跟上个差不多。合并主要是为基础镜像使用的。



Q:你们怎么看待OpenStack和Docker的关系?在京东未来会长期两个并存吗?现在两个架构的发展速度和研发力量对比如何?

A:OpenStack和Docker并不矛盾。私有云采用nova docker的结合更多的是迎合用户习于使用VM的习惯。Magnum也在快速发展中。所以我相信二者都有存在的价值和发展的必要。



Q:关于dockfile的优化,你们有没有什么好的建议或者经验?

A:似乎也没多少新的建议。参考DockOne的相关文章。Dockerfile之优化经验浅谈大家在写 dockerfile 时有啥最佳实践?希望得到大家的建议



Q:比如创建一个rabbitmq镜像,需要安装很多依赖包,最后编译,最后生成的镜像1.3G,像这种情况,在创建镜像的时候能否减少镜像的大小呢?

A:并没有什么好的办法来减少。可能需要一定的人工或者工具去分析不需要的文件,来减少镜像的大小。



Q:Docker是如何进行自动更新的,自己搭建的镜像仓库,如何更新新版本的镜像?

A:Docker我们固定了一个版本。如果没出大面积的严重问题,几乎不会更新。目前来看,运行稳定。所以也没有更新必要。新版本的Docker提供的如网络等,暂时我们还不会大面积跟进使用。自己的镜像仓库,如果要更新新版本镜像,push进去就可以了。



Q:一个困扰我比较久的问题,如果镜像间存在依赖关系,基础镜像发生改变后其他镜像你们是跟着更新的呢?

A:在内部私有云中,一般大家使用的都是一个做好的base镜像。这里面就有一个问题,一旦这个base镜像需要打补丁,影响面比较大。首先很多base的子镜像会受到影响。另一方面,就是要考虑已经在使用基于base或者base子镜像的节点。前者我的方案是直接在base镜像中的layer,把需要打补丁的文件加入进去,重新打包放回。对于后者,目前还没想到很好的方法解决。



Q:在运行容器的时候,1、应用里面的日志或者配置文件,使用本地映射是不是好点,我是考虑到方便查看日志或者修改配置;2、创建的数据库镜像,在运行容器的时候把数据文件是不是映射到本地更好些呢?

A:日志我们的确是使用的本地映射。而且有的业务方狂写日志不加约束。所以我们给本地映射做了个LVM,挂给容器。做了容量上的限制。配置的话,现在是有一个内部的部署系统会帮他们部署配置。数据库的话是一个道理,也是映射到本地。也有一部分接入了云硬盘。



Q:Docker中,每层镜像打标签那我觉的很奇怪,当pull一个镜像或生成一个容器时,它如何找到你所命名的镜像层?

A:并不是给每层都打标签,而是你根据你的需要来给某一层打标签。至于标签内容,需要自己来进行控制。



Q:关于Compress的实现有些疑问,是不是在实现的过程中,只考虑最后的镜像和前一层的diff,还是说要逐层做diff?

A:是只考虑最后的镜像和你要合并到的父层镜像做diff。这样只要做一次diff,就可以获得中间的所有文件变化了。



Q:wrapdocker文件的工作原理是什么?

A:这个工作原理主要是准备一些Docker启动必要的环境。比如在CentOS下,需要wrapdocker把cgroups等准备好等。你可以参考下wrapdocker里面的代码。



Q:容器运行在物理机上,与OpenStack平台虚拟机是同一套管理系统?如何与容器的集群系统整合?

A:是同一套系统,都是用nova。虚拟机KVM和容器主要是镜像类型不同。在nova调度的时候,会根据镜像类型调度到KVM或者Docker节点进行创建。



Q:在一台物理机上运行Docker的数量是否有限定 还是看运行的应用来决定?

A:没有特别做限定。主要还是业务方去申请的。业务方习惯用大内存,多CPU的。那这个物理机上创建的容器数就少些。大致这样。



Q:想了解一下,你们对镜像的tag是怎么管理的?根据什么来打的?对于旧的镜像你们是丢弃还是像Git保存代码一样一直保留在仓库呢?

A:tag由各个用户来定。不同的用户在不同的Repository里。镜像tag自己管理。不过我们更希望他们能够更加规范一些,比如用git的版本号来打tag。
旧的镜像如果失去了tag(新的镜像抢夺了该tag),则旧镜像会被删除。不过不是立即,也是定期清理,主要减少存储量。因为毕竟不需要存储那么多的版本。



===========================
以上内容根据2015年12月8日晚微信群分享内容整理。分享人:徐新坤,京东商城云平台南京研发中心JDOS团队研发工程师,从2014年初开始从事Docker的研发,主要负责Docker在京东落地的相关开发和维护工作。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesx,进群参与,您有想听的话题可以给我们留言。

DockOne技术分享(一):Dockerfile与Docker构建流程解读

徐新坤 发表了文章 • 0 个评论 • 28124 次浏览 • 2015-04-29 09:48 • 来自相关话题

【编者的话】本次讨论主要对docker build的源码流程进行了梳理和解读,并分享了在制作Dockerfile过程中的一些实践经验,包括如何调试、优化和build中的一些要点。另外,还针对现有Dockerfile的不足进行了简要说明,并分享了对于Docker ...查看全部
【编者的话】本次讨论主要对docker build的源码流程进行了梳理和解读,并分享了在制作Dockerfile过程中的一些实践经验,包括如何调试、优化和build中的一些要点。另外,还针对现有Dockerfile的不足进行了简要说明,并分享了对于Dockerfile的一些理解。

## 听众
这次的分享主要面向有一定Docker基础的。我希望你已经:

  • 用过Docker,熟悉`docker commit`命令
  • 自己动手编写过Dockerfile
  • 自己动手build过一个镜像,有亲身的体验
我主要分享一些现在网上或者文档中没有的东西,包括我的理解和一些实践,有误之处也请大家指正。好了,正文开始:## Dockerfile Dockerfile其实可以看做一个命令集。每行均为一条命令。每行的第一个单词,就是命令command。后面的字符串是该命令所要接收的参数。比如ENTRYPOINT /bin/bash。ENTRYPOINT命令的作用就是将后面的参数设置为镜像的entrypoint。至于现有命令的含义,这里不再详述。DockOne上有很多的介绍。## Docker构建(docker build)docker build的流程`docker build`的流程(这部分代码基本都在`docker/builder`中)[list=1]
  • 提取Dockerfile(evaluator.go/RUN)。
  • 将Dockerfile按行进行分析(parser/parser.go/Parse) Dockerfile,每行第一个单词,如CMD、FROM等,这个叫做command。根据command,将之后的字符串用对应的数据结构进行接收。
  • 根据分析的command,在dispatchers.go中选择对应的函数进行处理(dispatchers.go)。
  • 处理完所有的命令,如果需要打标签,则给最后的镜像打上tag,结束。
  • 在这里,我举一个例子来说明一下在第4步命令的执行过程。以CMD命令为例:
    func cmd(b *Builder, args []string, attributes map[string]bool, original string) error {   cmdSlice := handleJsonArgs(args, attributes)   if !attributes["json"] {      cmdSlice = append([]string{"/bin/sh", "-c"}, cmdSlice...)   }   b.Config.Cmd = runconfig.NewCommand(cmdSlice...)   if err := b.commit("", b.Config.Cmd, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {      return err   }   if len(args) != 0 {      b.cmdSet = true   }   return nil}
    可以看到,b.Config.Cmd = runconfig.NewCommand(cmdSlice...)就是根据传入的CMD,更新了Builder里面的Config。然后进行b.commit。Builder这里的commit大致含义其实与docker/daemon的commit功能大同小异。不过这里commit是包含了以下的一个完整过程(参见internals.go/commit):[list=1]
  • 根据Config,create一个container出来。
  • 然后将这个container通过commit(这个commit是指的docker的commit,与docker commit的命令是相同的)得到一个新的镜像。
  • 不仅仅是CMD命令,几乎所有的命令(除了FROM外),在最后都是使用b.commit来产生一个新的镜像的。
  • 所以这会导致的结果就是,Dockerfile里每一行,最后都会变为镜像中的一层。几乎是有多少有效行,就有多少层。Dockerfile逆向通过`docker history image`可以看到该镜像的历史来源。即使没有Dockerfile,也可以通过history来逆向产生Dockerfile。
    [root@jd ~]# docker history 2d8IMAGE               CREATED             CREATED BY                                      SIZE2d80e15fcfdb        8 days ago          /bin/sh -c #(nop) COPY dir:86faa820e8bf5dcc06   16.29 MB0f601e909d72        8 days ago          /bin/sh -c #(nop) ENTRYPOINT [hack/dind]        0 B68aed19c5994        8 days ago          /bin/sh -c set -x                               && git clone https://githu   3.693 MBebc6ef15552b        8 days ago          /bin/sh -c #(nop) ENV TOMLV_COMMIT=9baf8a8a9f   0 Bfe22e308201a        8 days ago          /bin/sh -c set -x                               && git clone -b v1.0.1 htt   5.834 MBf514c504c9b1        8 days ago          /bin/sh -c #(nop) COPY dir:d9a19910e57f47cb3b   3.114 MBe4e3ec8edf1a        8 days ago          /bin/sh -c ./contrib/download-frozen-image.sh   1.155 MB6250561532fa        8 days ago          /bin/sh -c #(nop) COPY file:9679abce578bcaa2c   3.73 kB...
    例如0f601e909d72就是由ENTRYPOINT [hack/dind]产生。这里的信息展示的不完全,可以通过`docker inspect -f {{.ContainerConfig.Cmd}} layer`来看某一层产生的具体信息。## 如何做DockerfileDockerfile调试Dockerfile更多的像一个脚本,类似于安装脚本。特别是大篇幅的脚本,想一次写成是比较有难度的。免不了进行一些调试。调试时最好利用Dockerfile的cache功能,可以大幅度节约调试的时间。举个例子,如果我现在有一个Dockerfile。但是我发现。我还需要再开几个端口,或者再安装其他的软件。这个时候最好不要直接修改已经有的Dockerfile的内容。而是在后面追加命令。这样再build的时候,可以利用已有的cache。Dockerfile优化调试过后的Dockerfile当然可以作为最终的Dockerfile,提供给用户。但是调试的Dockerfile的缺点就是层数可能过多,而且不易越多。所以最好进行一定的优化和整理。经过整理的Dockerfile生成出来的镜像可以使得层数更少,条理更清晰,也可以更好的复用。DockerOne里有一篇文章写得很好,可以参考。这里有两点要强调:
    • 尽量生成一个base:这样便于版本的迭代和作为公用镜像。
    • 清晰的注释:有一些注释会帮助别人理解这些命令的目的
    Dockerfile自动build有了Dockerfile,很多人都是在本地build。其实这个是相当耗时的。这个工作其实完全可以交给registry.hub.docker.com来完成。具体的做法就是:[list=1]
  • 把你的Dockerfile上传到GitHub上。
  • 进入到registry.hub.docker.com的自己的账户中,选择Automated Build。
  • 然后就可以build了。
  • 根据你的Dockerfile内容大小,build时长不确定。但是应该算是比较快了。docker源码的Dockerfile在我本地build了一个多小时。但是registry.hub.docker.com只用了半小时左右。大约是因为外国的月亮比较圆吧。build完成后,可以在线查看版本信息等。本地需要的话,可以直接pull下来。国内有多家公司提供了registry.hub.docker.com的Mirror服务,可以直接从国内的源中pull下来。速度快很多。## Dockerfile的不足 [list=1]
  • 层数过多:过多行的Dockerfile
  • 不能清理volume等配置:volume、expose等多个参数只能单向增加。不能删除。比如在某个镜像层加入了VOLUME /var/lib/docker。那么在该镜像之后的所有层将继承这一属性。
  • IMPORT功能

  • ## 其他

    现在我们回过头来看Docker的分层的另一个可能的用途。

    Docker的镜像可以看做是一个软件栈。那么其中有多个软件组成。好了,那么我们是不是可以考虑让软件进行自由叠加呢?

    比如:从CentOS镜像上安装了Python形成镜像A,从CentOS镜像上安装了Apache形成镜像B。如果用户想从CentOS上形成一个既有Python又有Apache的镜像,如何做呢?

    我想有两种方式,一种是dockerfile的import。我们可以基于镜像A,然后import安装Apache的Dockerfile,从而得到目标镜像。

    另外一种是可以直接引入,就是基于镜像A,然后我们直接把B的最后一层(假设B安装apache只形成了一层),搬到镜像A的子层上,不是也可以得到目标镜像么?

    以上主要是我分享的一些内容。大家可以一起来讨论。

    ===========================

    以上内容根据2015年4月28日晚微信群分享内容整理。分享人徐新坤,京东商城云平台南京研发中心开发工程师。从事京东定制OpenStack开发维护。2014年开始从事Docker的研究和开发。接下来,DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加我微信(liyingjiesx)参与

    Dockerfile最佳实践(一)

    田浩浩 发表了文章 • 7 个评论 • 68252 次浏览 • 2015-01-08 14:17 • 来自相关话题

    【编者的话】本文是Docker入门教程第三章-DockerFile的进阶篇,作者主要介绍了缓存、标签、端口以及CMD与ENTRYPOINT的最佳用法,并通过案例分析了注意事项,比如我们应该使用常用且不变的Dockerfile开头、通过`-t`标记来构建镜像、勿 ...查看全部
    【编者的话】本文是Docker入门教程第三章-DockerFile的进阶篇,作者主要介绍了缓存、标签、端口以及CMD与ENTRYPOINT的最佳用法,并通过案例分析了注意事项,比如我们应该使用常用且不变的Dockerfile开头、通过`-t`标记来构建镜像、勿在Dockerfile映射公有端口等等。

    Dockerfile使用简单的语法来构建镜像。下面是一些建议和技巧以帮助你使用Dockerfile。

    ##1、使用缓存
    Dockerfile的每条指令都会将结果提交为新的镜像,下一个指令将会基于上一步指令的镜像的基础上构建,如果一个镜像存在相同的父镜像和指令(除了`ADD`),Docker将会使用镜像而不是执行该指令,即缓存。

    为了有效地利用缓存,你需要保持你的Dockerfile一致,并且尽量在末尾修改。我所有的Dockerfile的前五行都是这样的:

    FROM ubuntu
    MAINTAINER Michael Crosby
    RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
    RUN apt-get update
    RUN apt-get upgrade -y

    更改`MAINTAINER`指令会使Docker强制执行`RUN`指令来更新apt,而不是使用缓存。

    所以,我们应该使用常用且不变的Dockerfile开始(译者注:上面的例子)指令来利用缓存。

    ##2、使用标签
    除非你正在用Docker做实验,否则你应当通过`-t`选项来```docker build```新的镜像以便于标记构建的镜像。一个简单的可读标签将帮助你管理每个创建的镜像。

    docker build -t="crosbymichael/sentry" .

    注意,始终通过`-t`标记来构建镜像。

    #3、公开端口
    两个Docker的核心概念是可重复和可移植。镜像应该可以运行在任何主机上并且运行尽可能多的次数。在Dockerfile中你有能力映射私有和公有端口,但是你永远不要通过Dockerfile映射公有端口。通过映射公有端口到主机上,你将只能运行一个容器化应用程序实例。(译者注:运行多个端口不就冲突啦)

    #private and public mapping
    EXPOSE 80:8080

    #private only
    EXPOSE 80

    如果镜像的使用者关心容器公有映射了哪个公有端口,他们可以在运行镜像时通过`-p`参数设置,否则,Docker会自动为容器分配端口。

    切勿在Dockerfile映射公有端口。

    ##4、CMD与ENTRYPOINT的语法
    `CMD`和`ENTRYPOINT`指令都非常简单,但它们都有一个隐藏的容易出错的“功能”,如果你不知道的话可能会在这里踩坑,这些指令支持两种不同的语法。

    CMD /bin/echo
    #or
    CMD ["/bin/echo"]

    这看起来好像没什么问题,但仔细一看其实两种方式差距很大。如果你使用第二个语法:`CMD`(或`ENTRYPOINT`)是一个数组,它执行的命令完全像你期望的那样。如果使用第一种语法,Docker会在你的命令前面加上`/bin/sh -c`,我记得一直都是这样。

    如果你不知道Docker修改了`CMD`命令,在命令前加上`/bin/sh -c`可能会导致一些意想不到的问题以及难以理解的功能。因此,在使用这两个指令时你应当使用数组语法,因为数组语法会确切地执行你打算执行的命令。

    使用CMD和ENTRYPOINT时,请务必使用数组语法。

    ##5、CMD和ENTRYPOINT 结合使用更好
    `docker run`命令中的参数都会传递给`ENTRYPOINT`指令,而不用担心它被覆盖(跟`CMD`不同)。当与`CMD`一起使用时`ENTRYPOINT`的表现会更好。让我们来研究一下我的Rethinkdb Dockerfile,看看如何使用它。

    #Dockerfile for Rethinkdb
    #http://www.rethinkdb.com/

    FROM ubuntu

    MAINTAINER Michael Crosby

    RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
    RUN apt-get update
    RUN apt-get upgrade -y

    RUN apt-get install -y python-software-properties
    RUN add-apt-repository ppa:rethinkdb/ppa
    RUN apt-get update
    RUN apt-get install -y rethinkdb

    #Rethinkdb process
    EXPOSE 28015
    #Rethinkdb admin console
    EXPOSE 8080

    #Create the /rethinkdb_data dir structure
    RUN /usr/bin/rethinkdb create

    ENTRYPOINT ["/usr/bin/rethinkdb"]

    CMD ["--help"]

    这是Docker化Rethinkdb的所有配置文件。在开始我们有标准的5行来确保基础镜像是最新的、端口的公开等。当`ENTRYPOINT`指令出现时,我们知道每次运行该镜像,在`docker run`过程中传递的所有参数将成为`ENTRYPOINT`(`/usr/bin/rethinkdb`)的参数。

    在Dockerfile中我还设置了一个默认`CMD`参数`--help`。这样做是为了`docker run`期间如果没有参数的传递,rethinkdb将会给用户显示默认的帮助文档。这是你所期望的与rethinkdb交互相同的功能。

    docker run crosbymichael/rethinkdb

    输出

    Running 'rethinkdb' will create a new data directory or use an existing one,
    and serve as a RethinkDB cluster node.
    File path options:
    -d [ --directory ] path specify directory to store data and metadata
    --io-threads n how many simultaneous I/O operations can happen
    at the same time

    Machine name options:
    -n [ --machine-name ] arg the name for this machine (as will appear in
    the metadata). If not specified, it will be
    randomly chosen from a short list of names.

    Network options:
    --bind {all | addr} add the address of a local interface to listen
    on when accepting connections; loopback
    addresses are enabled by default
    --cluster-port port port for receiving connections from other nodes
    --driver-port port port for rethinkdb protocol client drivers
    -o [ --port-offset ] offset all ports used locally will have this value
    added
    -j [ --join ] host:port host and port of a rethinkdb node to connect to
    .................

    现在,让我们带上`--bind all`参数来运行容器。

    docker run crosbymichael/rethinkdb --bind all

    输出

    info: Running rethinkdb 1.7.1-0ubuntu1~precise (GCC 4.6.3)...
    info: Running on Linux 3.2.0-45-virtual x86_64
    info: Loading data from directory /rethinkdb_data
    warn: Could not turn off filesystem caching for database file: "/rethinkdb_data/metadata" (Is the file located on a filesystem that doesn't support direct I/O (e.g. some encrypted or journaled file systems)?) This can cause performance problems.
    warn: Could not turn off filesystem caching for database file: "/rethinkdb_data/auth_metadata" (Is the file located on a filesystem that doesn't support direct I/O (e.g. some encrypted or journaled file systems)?) This can cause performance problems.
    info: Listening for intracluster connections on port 29015
    info: Listening for client driver connections on port 28015
    info: Listening for administrative HTTP connections on port 8080
    info: Listening on addresses: 127.0.0.1, 172.16.42.13
    info: Server ready
    info: Someone asked for the nonwhitelisted file /js/handlebars.runtime-1.0.0.beta.6.js, if this should be accessible add it to the whitelist.

    就这样,一个全面的可以访问db和管理控制台的Rethinkdb实例就运行起来了,你可以用与镜像交互一样的方式来与其交互。虽然简单小巧但它的功能非常强大。

    CMD和ENTRYPOINT 结合在一起使用更好。

    我希望这篇文章可以帮助你使用Dockerfiles以及构建镜像。Dockerfile是Docker的重要一部分,无论你是构建或是使用镜像,它都非常简单而且使用方便。我打算投入更多的时间来提供一个完整的、功能强大但简单的解决方案来使用Dockerfile构建Docker镜像。

    原文链接:Dockerfile Best Practices - take 1 (翻译:田浩浩 校对:李颖杰)

    ===========================
    译者介绍
    田浩浩悉尼大学USYD硕士研究生,目前在珠海从事Android应用开发工作。业余时间专注Docker的学习与研究,希望通过DockerOne把最新最优秀的译文贡献给大家,与读者一起畅游Docker的海洋。

    创建了一个有ssh服务的容器,如何ssh登录后,可以获取到Dockerfile中的环境变量呢?

    回复

    徐新坤 回复了问题 • 2 人关注 • 1 个回复 • 2214 次浏览 • 2019-03-14 17:42 • 来自相关话题

    dockerfile编译openresty时候configure报错

    回复

    李扯火 发起了问题 • 1 人关注 • 0 个回复 • 2057 次浏览 • 2017-05-26 11:12 • 来自相关话题

    大家在写 dockerfile 时有啥最佳实践?希望得到大家的建议。

    回复

    炮灰程序猿 回复了问题 • 9 人关注 • 6 个回复 • 6570 次浏览 • 2017-05-18 00:51 • 来自相关话题

    Windows上该如何制作Dockefile

    回复

    coagent 回复了问题 • 2 人关注 • 1 个回复 • 2081 次浏览 • 2017-03-01 11:13 • 来自相关话题

    Dockerfile VOLUME挂载的文件夹为空

    回复

    ayto 发起了问题 • 1 人关注 • 0 个回复 • 3252 次浏览 • 2016-12-12 17:35 • 来自相关话题

    关于dockerfile的EXPOSE问题

    回复

    徐磊 回复了问题 • 3 人关注 • 2 个回复 • 3373 次浏览 • 2016-10-08 21:23 • 来自相关话题

    dockerfile build 的镜像不能启动

    回复

    谭叔叔 回复了问题 • 2 人关注 • 2 个回复 • 7116 次浏览 • 2016-07-18 15:56 • 来自相关话题

    求大神分享tengine或者nginx可以合并请求的dockerfile或者镜像

    回复

    dockerlove123 发起了问题 • 1 人关注 • 0 个回复 • 2658 次浏览 • 2016-06-17 01:03 • 来自相关话题

    请教下关于dockerfile制作镜像ADD命令问题

    回复

    zhangke909 发起了问题 • 2 人关注 • 0 个回复 • 2462 次浏览 • 2016-05-26 09:50 • 来自相关话题

    Dockerfile构建问题

    回复

    vikbert 回复了问题 • 3 人关注 • 2 个回复 • 3617 次浏览 • 2016-04-03 23:28 • 来自相关话题

    容器化之路:谁偷走了我的构建时间

    CCE_SWR 发表了文章 • 0 个评论 • 614 次浏览 • 2019-04-11 15:10 • 来自相关话题

    随着全面云时代到来,很多公司都走上了容器化道路,老刘所在的公司也不例外。作为一家初创型的互联网公司,容器化的确带来了很多便捷,也降低了公司成本,不过老刘却有一个苦恼,以前每天和他一起下班的小王自从公司上云以后每天都比他早下班一个小时,大家手头上的活都差不多,讲 ...查看全部
    随着全面云时代到来,很多公司都走上了容器化道路,老刘所在的公司也不例外。作为一家初创型的互联网公司,容器化的确带来了很多便捷,也降低了公司成本,不过老刘却有一个苦恼,以前每天和他一起下班的小王自从公司上云以后每天都比他早下班一个小时,大家手头上的活都差不多,讲道理不应该呀,经过多番试探、跟踪、调查,终于让老刘发现了秘密的所在。

    作为一个开发,每天总少不了要出N个测试版本进行调试,容器化以后每次出版本都需要打成镜像,老刘发现每次他做一个镜像都要20分钟,而小王只要10分钟,对比来对比去只有这个东西不一样!


    0411_1.jpg


    Storage-Dirver到底是何方神圣?为什么能够导致构建时间上的差异?现在让我们来一窥究竟。

    在回答这个问题之前我们需要先回答三个问题——什么是镜像?什么是镜像构建?什么是storage-driver?

    什么是镜像?

    说到镜像就绕不开容器,我们先看一张来自官方对镜像和容器解释的图片:


    0411_2.jpg


    看完以后是不是更疑惑了,我们可以这样简单粗暴的去理解,镜像就是一堆只读层的堆叠。那只读层里到底是什么呢,另外一个简单粗暴的解释:里边就是放了一堆被改动的文件。这个解释在不同的storage-driver下不一定准确但是我们可以先这样简单去理解。

    那不对呀,执行容器的时候明明是可以去修改删除容器里的文件的,都是只读的话怎么去修改呢?实际上我们运行容器的时候是在那一堆只读层的顶上再增加了一个读写层,所有的操作都是在这个读写层里进行的,当需要修改一个文件的时候我们会将需要修改的文件从底层拷贝到读写层再进行修改。那如果是删除呢,我们不是没有办法删除底层的文件么?没错,确实没有办法删除,但只需要在上层把这个文件隐藏起来,就可以达到删除的效果。按照官方说法,这就是Docker的写时复制策略。

    为了加深大家对镜像层的理解我们来举个栗子,用下面的Dockerfile构建一个etcd镜像:


    0411_3.jpg


    构建完成以后生成了如下的层文件:


    0411_4.jpg


    每次进入容器的时候都感觉仿佛进入了一台虚机,里面包含linux的各个系统目录。那是不是有一层目录里包含了所有的linux系统目录呢?

    bingo答对!在最底层的层目录的确包含了linux的所有的系统目录文件。


    0411_5.jpg


    上述Dockerfile中有这样一步操作

    ADD . /go/src/github.com/coreos/etcd

    将外面目录的文件拷到了镜像中,那这一层镜像里究竟保存了什么呢?


    0411_6.jpg


    打开发现里面就只有

    /go/src/github.com/coreos/etcd这个目录,目录下存放了拷贝进来的文件。

    到这里是不是有种管中窥豹的感觉,接下来我们再来了解什么是镜像构建,这样基本上能够窥其全貌了。

    什么是镜像构建?

    通过第一节的内容我们知道了镜像是由一堆层目录组成的,每个层目录里放着这一层修改的文件,镜像构建简单的说就是制作和生成镜像层的过程,那这一过程是如何实现的呢?以下图流程为例:


    0411_7.jpg


    Docker Daemon首先利用基础镜像ubuntu:14.04创建了一个容器环境,通过第一节的内容我们知道容器的最上层是一个读写层,在这一层我们是可以写入修改的,Docker Daemon首先执行了RUN apt-update get命令,执行完成以后,通过Docker的commit操作将这个读写层的内容保存成一个只读的镜像层文件。接下来再在这一层的基础上继续执行 ADD run.sh命令,执行完成后继续commit成一个镜像层文件,如此反复直到将所有的Dockerfile都命令都被提交后,镜像也就做好了。



    这里我们就能解释为什么etcd的某个层目录里只有一个go目录了,因为构建的过程是逐层提交的,每一层里只会保存这一层操作所涉及改动的文件。

    这样看来镜像构建就是一个反复按照Dockerfile启动容器执行命令并保存成只读文件的过程,那为什么速度会不一样呢?接下来就得说到storage-driver了。

    什么是storage-driver?

    再来回顾一下这张图:


    0411_8.jpg


    之前我们已经知道了,镜像是由一个个的层目录叠加起来的,容器运行时只是在上面再增加一个读写层,同时还有写时复制策略保证在最顶层能够修改底层的文件内容,那这些原理是怎么实现的呢?就是靠storage-driver!

    简单介绍三种常用的storage-driver:

    1. AUFS

    AUFS通过联合挂载的方式将多个层文件堆叠起来,形成一个统一的整体提供统一视图,当在读写层进行读写的时,先在本层查找文件是否存在,如果没有则一层一层的往下找。aufs的操作都是基于文件的,需要修改一个文件时无论大小都会将整个文件从只读层拷贝到读写层,因此如果需要修改的文件过大,会导致容器执行速度变慢,docker官方给出的建议是通过挂载的方式将大文件挂载进来而不是放在镜像层中。


    0411_9.jpg


    1. OverlayFS

    OverlayFS可以认为是AUFS的升级版本,容器运行时镜像层的文件是通过硬链接的方式组成一个下层目录,而容器层则是工作在上层目录,上层目录是可读写的,下层目录是只读的,由于大量的采用了硬链接的方式,导致OverlayFS会可能会出现inode耗尽的情况,后续Overlay2对这一问题进行了优化,且性能上得到了很大的提升,不过Overlay2也有和AUFS有同样的弊端——对大文件的操作速度比较慢。


    0411_10.jpg


    1. DeviceMapper

    DeviceMapper和前两种Storage-driver在实现上存在很大的差异。首先DeviceMapper的每一层保存的是上一层的快照,其次DeviceMapper对数据的操作不再是基于文件的而是基于数据块的。

    下图是devicemapper在容器层读取文件的过程:


    0411_11.jpg


    首先在容器层的快照中找到该文件指向下层文件的指针。

    再从下层0xf33位置指针指向的数据块中读取的数据到容器的存储区

    最后将数据返回app。



    在写入数据时还需要根据数据的大小先申请1~N个64K的容器快照,用于保存拷贝的块数据。



    DeviceMapper的块操作看上去很美,实际上存在很多问题,比如频繁操作较小文件时需要不停地从资源池中分配数据库并映射到容器中,这样效率会变得很低,且DeviceMapper每次镜像运行时都需要拷贝所有的镜像层信息到内存中,当启动多个镜像时会占用很大的内存空间。

    针对不同的storage-driver我们用上述etcd的dockerfile进行了一组构建测试


    0411_1.jpg



    注:该数据因dockerfile以及操作系统、文件系统、网络环境的不同测试结果可能会存在较大差异

    我们发现在该实验场景下DevivceMapper在时间上明显会逊于AUFS和Overlay2,而AUFS和Overlay2基本相当,当然该数据仅能作为一个参考,实际构建还受到具体的Dockerfile内容以及操作系统、文件系统、网络环境等多方面的影响,那要怎么样才能尽量让构建时间最短提升我们的工作效率呢?

    且看下回分解!

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

    themind 发表了文章 • 0 个评论 • 3599 次浏览 • 2018-06-18 17:51 • 来自相关话题

    #1. Dockerfile文件和核心指令 在Kubernetes中运行容器的前提是已存在构建好的镜像文件,而通过Dockerfile文件构建镜像是最好方式。Dockerfile是一个文本文件,在此文件中的可以设置各种指令,以通过do ...查看全部
    #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 [:] [AS ] 

    ##1.2 RUN:设置构建镜像时执行的命令
    RUN命令有两种格式,下面是shell格式的RUN命令,在Linux中RUN的默认命令是/bin/sh;在Windows中默认命令为cmd /S /C:
    RUN 

    下面是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 命令行参数将会被追加到exec格式的ENTRYPOINT所有元素之后,并将会覆盖使用CMD指定的所有元素。这就允许江参数传递到入口点,例如,docker run
    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指令组合的命令执行情况:
    1.png

    ##1.5 ENV:设置环境变量
    Env指令通过<键>和<值>对设置环境变量。此值将在环境中用于生成阶段中的所有后续指令,并且也可以在许多情况下被替换为内联。

    “Env”指令有两种形式。第一种形式,即ENV < value >,将一个变量设置为一个值。第一个空间之后的整个字符串将被处理为“<值>”,包括空白字符。
    ENV  

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

    例如:
    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中的中拷贝文件、目录,并将它们添加到镜像文件系统的中。在指令中能够设置多个,--chown仅仅在构建Linux容器镜像时起作用,ADD指令有两种格式:
    ADD [--chown=:] ... 

    下面的ADD指令格式可以运行源和目标路径包含空格。
    ADD [--chown=:] ["",... ""]

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

    是容器一个绝对路径,或者是一个相对于WORKDIR的相对路径,
    ADD test relativeDir/          # 添加"test"到容器中`WORKDIR`/relativeDir/
    ADD test /absoluteDir/ # 添加"test"到容器中的/absoluteDir/

    ADD指令遵循下面的规则:

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

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

    COPY [--chown=:] ... 

    当目录中存在空格时,请使用下面的格式:
    COPY [--chown=:] ["",... ""]

    ##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  [/...]

    “EXPOSE”指令实际上并不发布端口,它在构建镜像的人员和运行容器的人员之间起着文档告知的作用。要在运行容器时实际发布端口,则需要通过在docker run命令使用-p和-P来发布和映射一个或者多个端口。
    ##1.10 LABEL:设置镜像的元数据信息
    LABEL指令拥有为镜像添加一些描述的元数据。LABEL是一系列的键值对,它的格式如下:
    LABEL = = = ...

    下面是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 命令选项
    11.jpg

    ##2.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构建镜像的合法表达:
    2.png

    ##2.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
    2. Dockerfile reference
    3. Best practices for writing Dockerfiles

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

    Dockerfile 中的 multi-stage(多阶段构建)

    博云BoCloud 发表了文章 • 0 个评论 • 1625 次浏览 • 2018-06-06 11:04 • 来自相关话题

    在应用了容器技术的软件开发过程中,控制容器镜像的大小可是一件费时费力的事情。如果我们构建的镜像既是编译软件的环境,又是软件最终的运行环境,这是很难控制镜像大小的。所以常见的配置模式为:分别为软件的编译环境和运行环境提供不同的容器镜像。 ...查看全部
    在应用了容器技术的软件开发过程中,控制容器镜像的大小可是一件费时费力的事情。如果我们构建的镜像既是编译软件的环境,又是软件最终的运行环境,这是很难控制镜像大小的。所以常见的配置模式为:分别为软件的编译环境和运行环境提供不同的容器镜像。

    比如为编译环境提供一个 Dockerfile.build,用它构建的镜像包含了编译软件需要的所有内容,比如代码、SDK、工具等等。同时为软件的运行环境提供另外一个单独的 Dockerfile,它从 Dockerfile.build 中获得编译好的软件,用它构建的镜像只包含运行软件所必须的内容。这种情况被称为构造者模式(builder pattern),本文将介绍如何通过 Dockerfile 中的 multi-stage 来解决构造者模式带来的问题。


    常见的容器镜像构建过程

    比如我们创建了一个 GO 语言编写了一个检查页面中超级链接的程序 app.go(请从 sparkdev 获取本文相关的代码):

    package main
    import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/url"
    "os"
    "strings"
    "golang.org/x/net/html"
    )
    type scrapeDataStore struct {
    Internal int `json:"internal"`
    External int `json:"external"`
    }
    func isInternal(parsedLink [i]url.URL, siteUrl [/i]url.URL, link string) bool {
    return parsedLink.Host == siteUrl.Host || strings.Index(link, "#") == 0 ||
    len(parsedLink.Host) == 0
    }
    func main() {
    urlIn := os.Getenv("url")
    if len(urlIn) == 0 {
    urlIn = "https://www.cnblogs.com/"
    }
    resp, err := http.Get(urlIn)
    scrapeData := &scrapeDataStore{}
    tokenizer := html.NewTokenizer(resp.Body)
    end := false
    for {
    tt := tokenizer.Next()
    switch {
    case tt == html.StartTagToken:
    token := tokenizer.Token()
    switch token.Data {
    case "a":
    for _, attr := range token.Attr {
    if attr.Key == "href" {
    link := attr.Val
    parsedLink, parseLinkErr := url.Parse(link)
    if parseLinkErr == nil {
    if isInternal(parsedLink, siteUrl, link) {
    scrapeData.Internal++
    } else {
    scrapeData.External++
    }
    }
    if parseLinkErr != nil {
    fmt.Println("Can't parse: " + token.Data)
    }
    }
    }
    break
    }
    case tt == html.ErrorToken:
    end = true
    break
    }
    if end {
    break
    }
    }
    data, _ := json.Marshal(&scrapeData)
    fmt.Println(string(data))
    }

    下面我们通过容器来构建它,并把它部署到生产型的容器镜像中。

    首先构建编译应用程序的镜像:

    FROM golang:1.7.3
    WORKDIR /go/src/github.com/sparkdevo/href-counter/
    RUN go get -d -v golang.org/x/net/html
    COPY app.go .
    RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .


    把上面的内容保存到 Dockerfile.build 文件中。

    接着把构建好的应用程序部署到生产环境用的镜像中:

    FROM alpine:latest  
    RUN apk --no-cache add ca-certificates
    WORKDIR /root/
    COPY app .
    CMD ["./app"]


    把上面的内容保存到 Dockerfile 文件中。

    最后需要使用一个脚本把整个构建过程整合起来:

    #!/bin/sh
    echo Building sparkdevo/href-counter:build # 构建编译应用程序的镜像
    docker build --no-cache -t sparkdevo/href-counter:build . -f Dockerfile.build # 创建应用程序
    docker create --name extract sparkdevo/href-counter:build
    # 拷贝编译好的应用程序
    docker cp extract:/go/src/github.com/sparkdevo/href-counter/app ./app docker rm -f extractecho Building sparkdevo/href-counter:latest # 构建运行应用程序的镜像
    docker build --no-cache -t sparkdevo/href-counter:latest .


    把上面的内容保存到 build.sh 文件中。这个脚本会先创建出一个容器来构建应用程序,然后再创建最终运行应用程序的镜像。


    把 app.go、Dockerfile.build、Dockerfile 和 build.sh 放在同一个目录下,然后进入这个目录执行 build.sh 脚本进行构建。构建后的容器镜像大小:


    微信图片_20180606110034.jpg



    从上图中我们可以观察到,用于编译应用程序的容器镜像大小接近 700M,而用于生产环境的容器镜像只有 10.3 M,这样的大小在网络间传输的效率是很高的。

    运行下面的命令可以检查我们构建的容器是否可以正常的工作:

    $ docker run -e url=https://www.cnblogs.com/ sparkdevo/href-counter:latest
    $ docker run -e url=http://www.cnblogs.com/sparkdev/ sparkdevo/href-counter:latest


    OK,我们写的程序正确的统计了博客园首页和笔者的首页中超级链接的情况。


    微信图片_20180606110040.jpg



    采用上面的构建过程,我们需要维护两个 Dockerfile 文件和一个脚本文件 build.sh。能不能简化一些呢? 下面我们看看 docker 针对这种情况提供的解决方案:multi-stage。


    在 Dockerfile 中使用 multi-stage


    multi-stage 允许我们在 Dockerfile 中完成类似前面 build.sh 脚本中的功能,每个 stage 可以理解为构建一个容器镜像,后面的 stage 可以引用前面 stage 中创建的镜像。所以我们可以使用下面单个的 Dockerfile 文件实现前面的需求:

    FROM golang:1.7.3
    WORKDIR /go/src/github.com/sparkdevo/href-counter/
    RUN go get -d -v golang.org/x/net/html
    COPY app.go .
    RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
    FROM alpine:latest
    RUN apk --no-cache add ca-certificates
    WORKDIR /root/
    COPY --from=0 /go/src/github.com/sparkdevo/href-counter/app .
    CMD ["./app"]


    把上面的内容保存到文件 Dockerfile.multi 中。这个 Dockerfile 文件的特点是同时存在多个 FROM 指令,每个 FROM 指令代表一个 stage 的开始部分。我们可以把一个 stage 的产物拷贝到另一个 stage 中。本例中的第一个 stage 完成了应用程序的构建,内容和前面的 Dockerfile.build 是一样的。第二个 stage 中的 COPY 指令通过 --from=0 引用了第一个 stage ,并把应用程序拷贝到了当前 stage 中。接下来让我们编译新的镜像:

    $ docker build --no-cache -t sparkdevo/href-counter:multi . -f Dockerfile.multi


    这次使用 href-counter:multi 镜像运行应用:

    $ docker run -e url=https://www.cnblogs.com/ sparkdevo/href-counter:multi$ docker run -e url=http://www.cnblogs.com/sparkdev/ sparkdevo/href-counter:multi



    微信图片_20180606110044.jpg



    结果和之前是一样的。那么新生成的镜像有没有特别之处呢:


    微信图片_20180606110047.jpg



    好吧,从上图我们可以看到,除了 sparkdevo/href-counter:multi 镜像,还生成了一个匿名的镜像。因此,所谓的 multi-stage 不过时多个 Dockerfile 的语法糖罢了。但是这个语法糖还好很诱人的,现在我们维护一个结构简洁的 Dockerfile 文件就可以了!

    使用命名的 stage

    在上面的例子中我们通过 --from=0 引用了 Dockerfile 中第一个 stage,这样的做法会让 Dockerfile 变得不容易阅读。其实我们是可以为 stage 命名的,然后就可以通过名称来引用 stage 了。下面是改造后的 Dockerfile.mult 文件:

    FROM golang:1.7.3 as builder
    WORKDIR /go/src/github.com/sparkdevo/href-counter/
    RUN go get -d -v golang.org/x/net/html
    COPY app.go .
    RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
    FROM alpine:latest
    RUN apk --no-cache add ca-certificates
    WORKDIR /root/
    COPY --from=builder /go/src/github.com/sparkdevo/href-counter/app .
    CMD ["./app"]


    我们把第一个 stage 使用 as 语法命名为 builder,然后在后面的 stage 中通过名称 builder 进行引用 --from=builder。通过使用命名的 stage, Dockerfile 更容易阅读了。


    总结

    Dockerfile 中的 multi-stage 虽然只是些语法糖,但它确实为我们带来了很多便利。尤其是减轻了 Dockerfile 维护者的负担(要知道实际生产中的 Dockerfile 可不像 demo 中的这么简单)。需要注意的是旧版本的 docker 是不支持 multi-stage 的,只有 17.05 以及之后的版本才开始支持。

    容器和应用程序:扩展、重构或重建?

    Rancher 发表了文章 • 1 个评论 • 2076 次浏览 • 2017-03-20 10:33 • 来自相关话题

    技术领域是不断变化的,因此,任何应用程序都可能在很短时间内面临过时甚至淘汰,更新换代的速度之快给人的感觉越来越强烈,我们如何使传统应用程序保持活力不落伍?工程师想的可能是从头开始重建传统应用程序,这与公司的业务目标和产品时间表通常是相悖的。如果现阶段正在运行的 ...查看全部
    技术领域是不断变化的,因此,任何应用程序都可能在很短时间内面临过时甚至淘汰,更新换代的速度之快给人的感觉越来越强烈,我们如何使传统应用程序保持活力不落伍?工程师想的可能是从头开始重建传统应用程序,这与公司的业务目标和产品时间表通常是相悖的。如果现阶段正在运行的应用程序是正常工作的,这时候你很难找到正当而充分的理由让技术人员花六个月重写应用程序。代码债似乎注定意味着失败。

    众所周知,产品开发向来都不是非黑即白那么简单,必须要权衡各方妥协折衷进行,虽然完全重写的可行性不大,但应用程序现代化的长远利益仍然值得重视。虽然许多组织尚未能构建全新的云本地应用程序,但通过使用一些技术比如Docker等容器技术,仍然能够实现传统应用程序的现代化。

    这些现代化技术最终可以归纳为三种类别:扩展,重构和重建。在开始介绍它们之前,让我们先来谈谈关于Dockerfile的一些基础知识。

    Dockerfile基础知识

    对于初学者来说,Docker是一个容器化平台,它包含了基本上可以安装在服务器上的所有东西,即“在一个完整的文件系统中包含一个软件运行所需的一切:代码,运行时,系统工具,系统库”, 而且没有虚拟化平台的开销。

    虽然容器的优点和缺点不在本文的讨论范围之内,但还是不得不提,Docker的最大优点之一即只需几行代码就能够快速轻松地启动轻量级、可重复的服务器环境。这种配置是通过一个名为Dockerfile的文件完成的,Dockerfile本质上是Docker用来构建容器镜像的蓝图。在这里,Dockerfile启动了一个简单的基于Python的Web服务器以供参考:


    # Use the python:2.7 base image
    FROM python:2.7

    # Expose port 80 internally to Docker process
    EXPOSE 80

    # Set /code to the working directory for the following commands
    WORKDIR /code

    # Copy all files in current directory to the /code directory
    ADD . /code

    # Create the index.html file in the /code directory
    RUN touch index.html

    # Start the python web server
    CMD python index.py


    这个例子比较简单,但已经很能说明关于Dockerfile一些基础知识,涵盖扩展预先存在的镜像、暴露端口以及运行命令和服务。只要基础源代码架构设计合理,此时只需几个指令就可以启动非常强大的微服务。

    应用程序现代化

    从根本上说,传统应用程序容器化并不困难,困难在于并不是每个应用程序都是建构在容器化的基础上。Docker有一个临时文件系统,这意味着容器内的存储并不持久。如果不采取一些特定措施,保存在Docker容器中的任何文件都可能丢失。此外,并行化是应用程序容器化的面临另一个难题,因为Docker的一个最大优点就在于它能快速适应日益增长的流量需求,这些应用程序需要能够与多个实例并行运行。

    综上所述,为使传统应用程序容器化,有以下几种路径:扩展、重构或者重建。哪种方法最适合,则完全取决于组织的需求和资源。

    #扩展

    一般来说,扩展非容器化应用程序的已有功能在这几种办法中最为简便,但如果处理不好,所做的更改可能会导致技术债显著增加。利用容器技术扩展传统应用程序的最好办法是通过微服务和API。虽然传统应用程序本身并没有被容器化,为使产品实现现代化,可将新特性从基于Docker的微服务中隔离,同时开发遗留代码,易于将来重构或重建。

    从高层面来说,对于那些在不久的将来很可能变得落后或必须经历重建的应用程序而言,扩展是很好的选择——不过代码库越老,为适应Docker平台,应用程序的某些部分就越需要彻底重构。

    #重构

    但有时,通过微服务或API扩展应用程序是不实际甚至不可行的。无论是欠缺要添加的新功能,还是通过扩展添加新功能很困难,重构旧代码库的某些部分都可能是必要的。将当前应用程序的各个现有功能从容器化的微服务中隔离出来,就能轻松完成重构了。例如,将整个社交网络重构到Docker化的应用程序可能是不切实际的,但通过退出运行用户搜索引擎,就能够将各个组件作为单独的Docker容器隔离。

    重构传统应用程序另一途径是用于写入日志、用户文件等内容的存储机制。在Docker中运行应用程序的最大障碍之一是临时文件系统。这种情况可以通过几种方式进行处理,最常见的是通过使用基于云的存储方法,如AmazonS3或Google云存储。通过重构文件存储方法以利用这些平台,应用程序可以很容易地在Docker容器中运行而不丢失任何数据。

    #重建

    当传统应用程序无法支持多个运行的实例时,不从头重建的话,可能无法添加Docker支持。传统应用程序服务周期可以很长,但如果应用程序的架构和设计决策在初始阶段就不够合理的话,则可能影响将来对应用程序的有效重构。意识到即将发生的阻碍对于识别生产率风险至关重要。

    大体来说,利用容器技术实现传统应用程序的现代化并没有硬性规则。至于哪种才是最佳决策则要视产品需求和业务需求而定。但是,要想确保应用程序稳定运行而不损失生产力,充分了解哪些决策会如何影响组织长期运行,是至关重要的。

    原文来源:Rancher Labs

    .NET程序在Linux容器中的演变

    龙影随风 发表了文章 • 0 个评论 • 2731 次浏览 • 2017-03-17 13:54 • 来自相关话题

    【编者的话】Linux容器技术已被开发人员所熟知,现在.NET程序可以跑在Docker容器中,这为以Windows中心的开发人员带来了好处。 【上海站|3天烧脑式微服务架构训练营】培训内容包括:DevOps、微服务、Spring Cl ...查看全部
    【编者的话】Linux容器技术已被开发人员所熟知,现在.NET程序可以跑在Docker容器中,这为以Windows中心的开发人员带来了好处。

    【上海站|3天烧脑式微服务架构训练营】培训内容包括:DevOps、微服务、Spring Cloud、Eureka、Ribbon、Feign、Hystrix、Zuul、Spring Cloud Config、Spring Cloud Sleuth等。

    本文将首先讨论镜像的构建时间和启动时间,接着会将一个简单的.NET程序运行在基于容器的应用上,然后观察镜像大小的变化,最终缩短镜像的构建和加载时间。此外,代码优化是本文的另一个主题。

    现在,.NET开发人员可以无障碍地使用如Docker这样的Linux容器,那么让我们来尝试如何以正确的方式配置一个容器。

    可能,文章的标题改成“Linux容器开发人员的演变”会更好。由于.NET可在Linux(以及Windows和macOS)上运行,所以整个世界的Linux容器和微服务已经开放给了.NET开发人员。

    有着大量的开发人员,长期的运行记录和优异性能指标的.NET,现在给以Windows为中心的开发人员提供了一个使用Linux容器的机会。

    虽然在Linux容器中尝试运行.NET代码是诱人的,同时也会产生一些细微差别,但是这样做是不会错的。你可以很容易地将一些.NET代码推送到镜像中。

    毕竟,一切都发生的这么快,一定都很好。 对不对?

    事实并非如此。让.NET代码运行在Linux容器中并不是一件简单的事情,但请记住:“先让它工作,然后让它工作得很快。”

    在下面的例子中,上文说的“很快”指的是构建镜像所需的时间,启动镜像所需的时间和镜像内部代码的性能。本文将首先讨论镜像的构建时间和启动时间,接着会将一个简单的.NET程序运行在基于容器的应用上,然后观察镜像大小的变化,最终缩短镜像的构建和加载时间。此外,代码优化是本文的另一个主题。
    # 短暂的停留
    考虑一个非常简单的微服务示例,它只给出一个“Hello world”类型的HTTP响应。也就是说,当在浏览器中填写URL,你就会得到一个包括主机名的Web页面。

    我们可从这个代码库中下载源码,并制作第一个Dockerfile(Dockerfile.attempt1),接着使用以下命令构建镜像:
    #  docker  build  -t  attempt1   -f   Dockerfile.attempt1   .

    然后在容器中运行镜像:
    #  docker  run   -d   -p   5000:5000   --name   attempt1   attempt1

    将浏览器的URL指向主机的IP地址,情况如下:
    01.png

    # 数字
    第一次构建镜像,一共耗时95秒。其中,下载红帽企业Linux(简称RHEL)镜像与安装.NET SDK,这些文件一共490MB。最终,镜像大小为659MB。

    一般而言,镜像的后续构建将更快,因为Docker化的镜像已经在主机上可用。改变源码后,我们再次运行构建。这一次构建镜像,大约耗时50秒,得到了相同大小的镜像,也是659MB。

    镜像的大小很重要。因为镜像使用操作系统的存储空间,虽然空间便宜,但它仍然是有限的商品。当定期使用容器时,我们很容易忽略过时的镜像,然而它仍然在占用磁盘。如果你不注意的话,磁盘空间将很快用尽。

    如何使镜像尽可能的小?
    #移除镜像不需要的部分
    使用命令`dotnet restore --no-cache`可以消除任何缓存,这样镜像的大小下降到608.6MB,减少了50.6 MB,同比缩小超过7%。
    #在构建镜像之前构建应用
    应用是在容器中运行镜像时构建.NET程序的。这耗时大约1.6秒——虽然时间不长,但却是在浪费时间。

    在恢复之前插入的`dotnet build`命令,并在构建镜像之前构建应用,这样的话容器将会更快地启动。这个结果可在Dockerfile.attempt3中实现。

    与此同时,镜像大小却增加到610.2MB,而我们还得运行`dotnet build`,虽然现在花这个时间,但却可在每次启动容器时受益。
    # 运行Dotnet Publish命令
    因为容器是一个运行时环境,那我们为什么不使用`dotnet publish`命令发布代码,然后把代码放入镜像呢?如果这样做的话,我们就没必要在镜像中安装.NET程序了。毕竟,我们需要的是一个可在任何地方独立运行的应用。

    使用dotnet发布代码,会减少镜像大小和缩短容器启动时间。更改project.json文件,注释掉下图中红框的内容,这告诉编译器此文件为一个平台构建。您可以在下图中看到它:
    02.jpg

    接下来,我们使用`dotnet publish -c Release -r rheh.7.2-x64`发布代码,这会把所有的编译文件和运行时文件,放入一个文件夹,我们把此文件夹复制到镜像中。

    因为我们不再需要安装.NET程序,只要一个包含RHEL文件的基础镜像即可,这样就减少了镜像的大小。这是Dockerfile的第四次迭代——Dockerfile.attempt4:
    FROM registry.access.redhat.com/rhel7
    RUN yum install -y libunwind
    RUN yum install -y libicu
    ADD bin/Release/netcoreapp1.0/rhel.7.2-x64/publish/. /opt/app-root/src/
    WORKDIR /opt/app-root/src/
    EXPOSE 5000
    CMD ["/bin/bash", "-c", "/opt/app-root/src/dotnet_docker_msa"]

    请注意,`yum install`命令将安装一些.NET需要的依赖文件,然后运行`docker build `命令,最终生成一个694.6MB的镜像。
    # 谁需要缓存?
    多次运行`yum install`命令,前一次操作将为后一次构建缓存。如果在每个`yum install`命令之后,我们立即清除缓存,效果将会很好。下面是Dockerfile的第五次迭代———Dockerfile.attempt5:
    FROM registry.access.redhat.com/rhel7
    RUN yum install -y libunwind && yum clean all
    RUN yum install -y libicu && yum clean all
    ADD bin/Release/netcoreapp1.0/rhel.7.2-x64/publish/. /opt/app-root/src/
    WORKDIR /opt/app-root/src/
    EXPOSE 5000
    CMD ["/bin/bash", "-c", "/opt/app-root/src/dotnet_docker_msa"]

    基于Dockerfile.attempt5构建的镜像,其大小减少到293.7MB,这比第一次构建缩小了55%。
    # 堆叠命令
    对Dockerfile做最后更改,我们需要堆叠`yum install`命令,具体内容如下所示:
    FROM registry.access.redhat.com/rhel7
    `RUN yum install -y libunwind libicu && yum clean all
    `ADD bin/Release/netcoreapp1.0/rhel.7.2-x64/publish/. /opt/app-root/src/
    `WORKDIR /opt/app-root/src/
    `EXPOSE 5000
    `CMD ["/bin/bash", "-c", "/opt/app-root/src/dotnet_docker_msa"]

    最终得到的镜像大小为257.5MB,这比第一次构建缩小了60%。

    下面是各个Dockerfile构建的镜像大小对比图:
    03.jpg

    # 总结
    在探索新技术与新模式时,我们不能将早期的结果与最优做法相混淆。虽然早期的成功会给我们带来兴奋和鼓励,但它也可能使我们丧失进步的动力。勤奋,然后不断尝试,并且始终接受改进的建议,会帮助我们走的更远。

    原文链接:The Evolution of a Linux Container(译者:Jack)

    ===========================================
    译者介绍
    Jack,开源软件爱好者,研究方向是云计算PaaS平台与深度学习,现积极活跃于Docker,Kubernetes,Tensorflow社区。

    Dockerfile实践优化建议

    ylzhang 发表了文章 • 0 个评论 • 7078 次浏览 • 2017-01-20 18:36 • 来自相关话题

    【编者的话】Dockerfile是一种被Docker程序解释的脚本,Dockerfile由一条一条的指令组成,每条指令对应Linux下面的一条命令。Docker程序将这些Dockerfile指令翻译真正的Linux命令。类似于Makefile,Dockerfi ...查看全部
    【编者的话】Dockerfile是一种被Docker程序解释的脚本,Dockerfile由一条一条的指令组成,每条指令对应Linux下面的一条命令。Docker程序将这些Dockerfile指令翻译真正的Linux命令。类似于Makefile,Dockerfile有自己书写格式和支持的命令,Docker程序解决这些命令间的依赖关系。下面是resin.io关于Dockerfile编写经验和建议的总结。

    上个月,Docker发起了Docker Global Mentor Week 2016,旨在帮助开发者用户提高各项技术水平。在resin.io技术栈中,Docker是一个关键的技术之一,而且我们也积攒了很多与Docker关联的最佳实践经验、注意事项、以及提高resin.io开发经验的小技巧。Docker本身已经有很多优秀的实践范例,但并不是所有的场景都在resin.io使用。根据Global Mentor Week的议题精神,在这篇博客中我们整理了关于resin.io应用程序和硬件设备使用Docker场景的一些常见问题。

    文章主要分为两个部分:1,必须在实践中使用的; 2,提示部分,建议使用可以提高代码质量和经验,但是并非时强制的。
    # 必须使用部分
    以下这些实践经验能在开发中为您缩减痛苦过程。
    ## 固定软件版本
    固定所有依赖的版本是实现良好实践最佳途径。这包括基本映象,从GitHub中提取的代码,代码依赖的库等等。通过版本控制,您可以简化应用程序已知的工作版本。

    如果没有版本控制,您的组件很容易改变,导致以前工作的Dockerfile不能再构建。

    您可以在resin.io官方Docker Hub拉取基础映象最新的可用版本,可以依据基础映象的Tags查询选择。例如,使用`resin/raspberrypi3-debian`关键字搜索列出映象,按照更新日期排序版本的新旧,应当选择当时最新的jessie-20161119版本而不是jessie版本。
    FROM resin/raspberrypi3-debian:jessie-20161119

    基础映象的架构会发现变化(这种情况极少,但是也是存在的),而使用日期标记排序,就可以标识处稳定可用的最新映象版本。(这样对于Docker来说,他们就一直可以下载可用版本)

    一个棘手的事情是固定操作系统中使用包安装器安装的软件的版本问题,再Debian中,运行apt-get安装特定的版本信息,例如:
    RUN apt-get update && \  
    apt-get install -yq --no-install-recommends \
    i2c-tools=3.1.1-1 \
    ...

    Debian软件包Alpine软件包Fedora软件包及其各自的软件包管理器也是如此。 如果你已经安装了大量的软件包,这需要花更多的时间设置版本信息,但是从长远来看它是值得的。

    通常,您将从版本控制(例如从git / GitHub)安装软件,在这种情况下,没有理由不使用由唯一ID(如git的hash / SHA)定义的特定提交或标签 。 下面是一个如何使用git检出代码的特定标记版本的示例:
    # Can use tag or commit hash to set MRAAVERSION
    ENV MRAAVERSION v1.3.0
    RUN git clone https://github.com/intel-iot-devkit/mraa.git && \
    cd mraa && \
    git checkout -b build ${MRAAVERSION} && \
    ...

    最终,安装版本都来自于不管任何库申请都是固定的版本,不论使用了requirements.txt(Python管理安装模块),package.json(Node.js管理安装模块),Cargo.toml(Rust管理安装模块),或者是其他语言的管理的安装包管理器,这样就总是固定版本(或者是经常锁定冻结)依赖版本号或者唯一提交。
    ## 自我清理
    普遍来讲,加快计算机程序最好的方式之一是消除不必要计算(做的更少)。通常来讲,软件部署也是如此,加快部署和更新的最佳方式不发送不需要的代码。所以,自身来讲从容器中清除不必要的代码,可以提高效率。

    什么是不需要的代码? 最常见的是,它们是保存在包管理器中的临时文件或者是在Dockerfile中构建和安装的软件源代码。
    在包管理器之后清理的方式取决于在您的基本映像中使用的分发方式。 在Debian和Raspbian的情况下使用的是apt-get,Docker已经有很多建议Dockerfile中使用apt-get。 最后,完成安装步骤,删除临时信息,如下:
    RUN apt-get update && \  
    apt-get install -yq --no-install-recommends \
    \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

    上面的最后一行通过apt-get rm删除了设备上不需要的的临时文件。

    如果你使用Alpine Linux,apk包管理工具有一个方便的--no-cache选项:
    RUN apk add --no-cache 

    Fedora系统中,dnf包管理器可以通过apt-get简单处理:
    RUN dnf makecache && \  
    dnf install -y \
    \
    && dnf clean all && rm -rf /var/cache/dnf/*

    清除已安装软件的源代码通常非常简单,只需删除在生成过程的早期步骤中创建的目录即可。 为了保持上面的MRAA示例,通过git checkout后通过这个方式执行清理:
    ENV MRAAVERSION v1.3.0  
    RUN git clone https://github.com/intel-iot-devkit/mraa.git && \
    cd mraa && \
    git checkout -b build ${MRAAVERSION} && \

    make install && \
    cd .. && rm -rf mraa

    还要确保所有的清理语句在同一个部分运行,否则它们将看起来清除,但最终仍然存在于Docker容器成为残留。
    ## 组合运行语句
    上面的最后一个注释引导必须要做,这就要把逻辑上属于一起操作步骤的语句合并进入Dockerfile中,这样以避免类似缓存和不必要地使用磁盘空间有关常见问题。 首先,由于缓存,您可能会有意外的构建结果。 如果您的apt-get更新步骤是从apt-get install 步骤单独运行的,则前者可能会被缓存,并且不会在您期望更新的时候更新。 如果你分离你的git克隆和实际构建,类似的事情也可能发生。其次,在单独的后续RUN步骤中删除的文件保留在最终容器中,但不可访问(残留)。

    Docker文档有更多的注释和建议说明
    # 推荐的实现
    强烈推荐以下做法,通常情况下这是从优秀到卓越的经验,但是并不一定是一个瓶颈。
    ## 整理 Dockerfile 语句
    Docker尝试缓存您的Dockerfile中尚未更改的所有步骤,但如果更改任何语句,将重做其后的所有步骤。 您可以在构建过程中节省相当多的时间,只要有可能,就尽可能按照最不可能更改的顺序编写Dockerfile。 例如,一般设置,如设置工作目录,启用initsystem,设置维护应该更早发生。
    MAINTAINER Awesome Developer   
    WORKDIR /usr/src/app
    ENV INITSYSTEM on

    这些语句执行之后,可以使用操作系统的软件包管理器安装软件,然后编译依赖关系,启用系统服务和其他设置等。 例如,在Dockerfile末尾的这一部分执行,你应该安装Python:
    COPY requirements.txt ./  
    RUN pip install -r requirements.txt

    或者 Node.js 依赖.
    COPY package.json ./  
    RUN npm install

    复制应用程序源代码的做法放应该在最后,这也是最常用的做法。我们可以适用复制命令:
    COPY . ./

    这样就可以加快构建部署的过程,而且Dockerfile文件的可读性强!上面的例子只供参考,逻辑上实现可以依赖域当前应用程序的部分。
    ## 使用 .dockerignore
    接着上一步,我们总是定义一个.dockerignore文件,这个文件是用来区分源码中那些是非必须的设备文件,那些不用经过Copy ../拷贝步骤。忽略的文件可是是README.md或者其他的文件,或者是图片、文件、其他不需要要求应用程序的功能而又必须放在一个库或者其他原因的文件。
    ## 使用启动脚本
    创建和调试比较大型的项目,我们还有一个建议:不要直接使用CMD命令运行,建议时用一个开始脚本,每次调用运行:
    CMD ["bash", "start.sh"]

    然后在start.sh脚本文件中你可以使用python app.py之类的命令启动、运行你的应用程序。这样的优势是可以很方便扩展运行脚本文件,增加调试功能,而不用在CMD中一步一步执行。

    核心代码发布之前你想增加一些调试信息?仅仅增加几条你想要的几条测试逻辑?

    另一方面,你可以使用Resin sync可以提高我们的开发进度。Resin sync可以直接拷贝源代码到正在运行的设备中实现更新(不用重新编译Dockerfile文件),然后重启容器加载新的配置即可。然而,这些只有在Docker容器没有缓存的情况下才能生效,例如通过cmd直接重定向的。
    ## 创建 Non-Root User
    Docker的默认设置中,容器中的代码都是通过Root账户运行的。作为一个良好的预防性安全实践,建议创建一个Non-Root用户,授予它只需要尽可能多的特权即可。

    例如:
    RUN useradd --user-group --shell /bin/false resin  
    USER resin

    上面命令是创建一个resin用户,后续的步骤中都使用这个用户。Docker docs上有更详细的说明,或者参考这个博客
    # 总结
    通过检查我们的编译优化文档Docker最佳实践经验文档(那些已经应用),我们可以更进一步的了解。你可能也想看看Dockerfile Linter的一些优化建议。

    原文链接:Dockerfile Tips and Tricks (翻译:ylzhang)

    九个编写Dockerfiles的常见错误

    cyeaaa 发表了文章 • 0 个评论 • 10445 次浏览 • 2016-06-16 16:36 • 来自相关话题

    【编者的话】我们每天基于Dockerfiles工作;所有运行的代码都来自一系列的Dockerfiles。这篇文章将会讨论编写Dockerfile时人们经常犯的错误以及如何改进。对于Docker专家说,这篇文章里的许多技巧可能会非常明显进而会得到很多的认同。但是 ...查看全部
    【编者的话】我们每天基于Dockerfiles工作;所有运行的代码都来自一系列的Dockerfiles。这篇文章将会讨论编写Dockerfile时人们经常犯的错误以及如何改进。对于Docker专家说,这篇文章里的许多技巧可能会非常明显进而会得到很多的认同。但是对于初级到中级开发者,该文章将会是一份很有用的指南,它有助于理清以及加速你们的工作流程。
    #1. 执行 apt-get
    执行`apt-get install`是每一个Dockerfile都有的东西之一。你需要安装一些外部的包来运行代码。但使用`apt-get`相应地会带来一些问题。

    一个是运行`apt-get upgrade` 会更新所有包到最新版本 —— 不能这样做的理由是它会妨碍Dockerfile构建的持久与一致性。

    另一个是在不同的行之间运行`apt-get update`与`apt-get install`命令。不能这样做的原因是,只有`apt-get update`的代码会在构建过程中被缓存,而且你需要运行`apt-get install`命令的时候不会每次都被执行。因此,你需要将`apt-get update`跟所要安装的包都在同一行执行,来确保它们正确的更新。

    在以下 Golang Dockerfile中`apt-install`命令就是一个不错的例子:
    # From https://github.com/docker-library/golang
    RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    g++ \
    gcc \
    libc6-dev \
    make \
    && rm -rf /var/lib/apt/lists/*

    #2. 使用ADD而非COPY
    `ADD`与`COPY`是完全不同的命令。`COPY`是这两个中最简单的,它只是从主机复制一份文件或者目录到镜像里。`ADD`同样可以这么做,但是它还有更神奇的功能,像解压TAR文件或从远程URLs获取文件。为了降低Dockerfile的复杂度以及防止意外的操作,最好用`COPY`来复制文件。
    FROM busybox:1.24

    ADD example.tar.gz /add #解压缩文件到add目录
    COPY example.tar.gz /copy #直接复制文件

    #3. 在一行内添加整个应用目录
    明确代码的哪些部分以及什么时候应该放在构建镜像内或许是最重要的事了,它可以显著加快构建速度。

    Dockerfile里经常会看到如下这些内容:
    # !!! ANTIPATTERN !!!
    COPY ./my-app/ /home/app/
    RUN npm install # or RUN pip install or RUN bundle install
    # !!! ANTIPATTERN !!!

    这就意味着每次修改文件之后都需要重新构建那行以下的所有东西。多数情况下(包括上面的例子),它意味着重新安装应用依赖。为了尽可能地使用Docker的缓存,首先复制所有安装依赖所需要的文件,然后执行命令安装这些依赖。在复制剩余文件(这一步尽可能放到最后一行)之前先做这两个步骤,会使代码的变更被快速的重建。
    COPY ./my-app/package.json /home/app/package.json # Node/npm packages
    WORKDIR /home/app/
    RUN npm install
    # 或许还要安装python依赖?
    COPY ./my-app/requirements.txt /home/app/requirements.txt
    RUN pip install -r requirements.txt
    COPY ./my-app/ /home/app/

    这样做会确保构建尽可能快的执行。
    #4. 使用:latest标签
    许多Dockerfiles在开头都使用`FROM node:latest`模板,用来从Docker registry拉取最新的镜像。简单地说,使用`latest`标签的镜像意味着如果这个镜像得到更新,那么Dockerfile的构建可能会突然中断。弄清这件事可能会非常难,因为Dockerfile的维护者实际上并没做任何修改。为了防止这种情况,只需要确保镜像使用特定的标签(例如:`node:6.2.1`)。这样就可以确保Dockerfile的一致性。
    #5. 构建镜像时使用外部服务
    很多人会忽视构建Docker镜像与运行一个Docker容器的区别。在构建镜像时,Docker读取Dockerfile里的命令并创建镜像。在依赖或代码修改之前,镜像是保持不变以及可重复使用的。这个过程完全独立于其它容器。需要与其它容器或服务(如数据库)进行交互则会在容器运行的时候发生。

    举一个例子,执行数据库迁移。很多人试图在构建镜像时执行此操作。这样做会导致许多问题。首先,在构建时数据库可能不可用,因为它可能没建在它将要运行的服务器上。其次,你可能想使用同一个镜像来连接不同的数据库(在开发或生产环境中),在这种情况下,如果它在构建过程中,迁移是不能进行的。
    # !!! ANTIPATTERN !!!
    COPY /YOUR-PROJECT /YOUR-PROJECT
    RUN python manage.py migrate
    # 尝试迁移数据,但是并不能
    CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
    # !!! ANTIPATTERN !!!

    #6. 在Dockerfile开始部分加入EXPOSE和ENV
    EXPOSE和ENV是廉价的执行命令。如果你破坏它们的缓存,几乎瞬时就可以重建。所以,最好尽可能晚地声明这些命令。在构建过程中应该直到需要的时候才声明ENV。如果在构建的时候不需要他们,那么应该在Dockerfile的末尾附加`EXPOSE`。

    再次查看Golang的Dockerfile,你会看到,所有`ENVS`都是在使用前声明的,并且在最后声明其余的:
    ENV GOLANG_VERSION 1.7beta1
    ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
    ENV GOLANG_DOWNLOAD_SHA256 a55e718935e2be1d5b920ed262fd06885d2d7fc4eab7722aa02c205d80532e3b
    RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
    && echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
    && tar -C /usr/local -xzf golang.tar.gz \
    && rm golang.tar.gz
    ENV GOPATH /go
    ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH

    如需修改`ENV GOPATH`或`ENV PATH`,镜像几乎会马上重建成功。
    #7. 多个FROM声明
    尝试使用多个`FROM`声明来将不同的镜像组合到一起,这样不会起任何作用。Docker仅使用最后一个`FROM`并且忽略前面所有的。

    所以如果你有这样的Dockerfile:
    # !!! ANTIPATTERN !!!
    FROM node:6.2.1
    FROM python:3.5
    CMD ["sleep", "infinity"]
    # !!! ANTIPATTERN !!!

    那么`docker exec`进入运行的容器中,会得到下面的结果:
    $ docker exec -it d86fcf0775d3 bash
    root@d86fcf0775d3:/# which python
    /usr/local/bin/python
    root@d86fcf0775d3:/# which node
    root@d86fcf0775d3:/#

    这其实是GitHub上的一个问题:合并不同的镜像,但它看起来不会很快就增加的功能。
    #8. 多个服务运行在同一个容器内
    这可能是了解Docker的开发者遇到的最大问题。而公认的最佳实践是:每个不同的服务,包括应用,应该在它自己的容器中运行。在一个Docker镜像里面加入多个服务非常容易,但是有一定的负面影响。

    首先,横向扩展应用会变得很困难。其次,额外的依赖和层次会使镜像构建变慢。最终,增大了Dockerfile的编写、维护以及调试难度。

    当然,像所有的技术建议一样,你需要用你的最佳判断。如果想快速安装一个`Django`+`Nginx`的应用的开发环境,那么让它们运行在同一个容器里面,同时生产环境中有一个不同的Dockerfile,让他们分开运行,是合理可行的。
    #9. 在构建过程中使用VOLUME
    `Volume`是在运行容器时候加入的,而不是构建的时候。与第五个误区类似,在构建过程中不应该与你声明的`volume`有交互。相反地,你只是在运行容器的时候使用它。例如,如果在以下构建过程中创建文件并且在运行那个镜像时候使用它,一切正常:
    FROM busybox:1.24
    RUN echo "hello-world!!!!" > /myfile.txt
    CMD ["cat", "/myfile.txt"]
    ...
    $ docker run volume-in-build
    hello-world!!!!

    但是,如果我对一个存储在`volume`上的文件做同样的事,就不会起作用。
    FROM busybox:1.24
    VOLUME /data
    RUN echo "hello-world!!!!" > /data/myfile.txt
    CMD ["cat", "/data/myfile.txt"]
    ...
    $ docker run volume-in-build
    cat: can't open '/data/myfile.txt': No such file or directory

    一个有趣的问题是:如果你前面的任何一个层次声明了一个`VOLUME`(也可能是几个`FROMS`)依然会遇到同样的问题。因此,最好留意一下父类镜像都声明了什么`volume`。如果遇到问题,请使用`docker inspect`检查。
    #结论
    理解怎样写好一个`Dockerfile`将会是一个漫长的路程,它会带你理解`Docker`是如何工作的,同时也帮助你建立你的基础架构。理解Docker缓存会为你节省好多等待构建完成的时间!

    原文链接:9 Common Dockerfile Mistakes (翻译:陈晏娥 校对:田浩浩

    ===========================================

    译者介绍
    陈晏娥,鞍钢集团矿业公司信息开发中心运维高级工程师,专注虚拟化技术。

    DockOne技术分享(三十七):玩转Docker镜像和镜像构建

    徐新坤 发表了文章 • 4 个评论 • 11546 次浏览 • 2015-12-09 10:31 • 来自相关话题

    【编者的话】本次分享从个人的角度,讲述对于Docker镜像和镜像构建的一些实践经验。主要内容包括利用Docker Hub进行在线编译,下载镜像,dind的实践,对于镜像的一些思考等。 @Container容器技术大会将于2016年1月 ...查看全部
    【编者的话】本次分享从个人的角度,讲述对于Docker镜像和镜像构建的一些实践经验。主要内容包括利用Docker Hub进行在线编译,下载镜像,dind的实践,对于镜像的一些思考等。

    @Container容器技术大会将于2016年1月24日在北京举行,来自爱奇艺、微博、腾讯、去哪儿网、美团云、京东、蘑菇街、惠普、暴走漫画等知名公司的技术负责人将分享他们的容器应用案例。
    前言
    本次分享主要是从个人实践的角度,讲述本人对于Docker镜像的一些玩法和体会。本文中大部分的内容都还处于实验的阶段,未经过大规模生产的实践。特此说明。思虑不全或者偏颇之处,还请大家指正。

    镜像应该算是Docker的核心价值之一。镜像由多层组成。那么对于一个层来说,就有了两个角度来看待。一个角度是把这层当做一个独立的单位来看,那么这一个层其实主要是包含了文件和配置两个部分。另一个角度则是把这一层和它的所有父层结合起来看,那么这个整体则是代表了一个完整的镜像。

    本文所述的Docker镜像,主要是指的从Dockerfile构建出来的镜像。

    现在已经有了Docker Hub等多家公有容器服务供应商,为我们提供了非常便捷的镜像构建服务。我们不再需要在本地运行docker build而是可以借用他们的服务实现方便的镜像构建。下文中以Docker Hub为例,介绍一些非常规的用法。各位在实践中可以使用国内的多家容器服务提供商,如DaoCloud等。
    Docker Hub之在线编译
    众所周知,Docker镜像可以用来描述一个APP的runtime。比如我们构建一个Tomcat的镜像,镜像里包含了运行Tomcat的环境以及依赖。但是我们再细看,其实Docker镜像不仅仅是一个runtime,而是提供了一个环境,一个软件栈。从这个角度上来说,镜像不仅仅可以用来提供APP进行运行,还可以提供诸如编译的环境。

    用Docker来进行编译,这个应该来说不是什么新奇玩法。因为Docker源码的编译就是通过这种方式来获得的。Docker有对应的Dockerfile。可以利用这个来完成代码的编译。

    这里我举个例子。这里有一个写的Dockerfile。test.c是一个输出hello world的c语言源文件。
    	FROM centos:centos6
    RUN yum install -y gcc
    ADD test.c /
    RUN gcc /test.c

    构建这个镜像,由于最后一步是编译命令gcc/test.c,所以编译过程会在Docker Hub上进行执行。

    我们可以通过编写Dockerfile,使得整个编译过程都托管在Docker Hub上。如果我们提交了新的代码,需要重新编译,那么只需要重新构建镜像即可。
    镜像下载
    在v1版本中,Docker Client是串行下载镜像的各层。对于docker pull的过程进行分析,可以看到Docker Client总共有这样几个步骤:

    * /v1/repositories/{repository}/tags/{tag} 获取tag的id,
    * /v1/images/{tag_id}/ancestry 获取tag的各层的id
    * /v1/images/{layer_id}/json 依次获取各层对应的配置文件json
    * /v1/images/{layer_id}/layer 依次获取各层对应的镜像数据layer

    Docker Hub的镜像数据,并不是在自己的服务器中存储,而是使用的亚马逊的s3服务。因此在调用/v1/images/{layer_id}/layer接口,拉取镜像的layer数据时,会返回302,将请求重定向到亚马逊的s3服务上进行下载。

    为了方便下载,我自己写了个小程序,使用HTTP协议即可完全模拟Docker Client的整个过程。自己写的好处在于你可以依次获取tag的ID,各层的ID,以及所有层的配置,进而一次性将所有层对应的镜像数据存储在亚马逊的s3地址获取到,然后可以进行并行下载。如果单层下载失败,只需要重新下载这一层即可。当所有的层在本地下载完毕后。然后打成tar包,再使用Docker Client进行load即可。

    对于上文中所说的在线编译,那么我们其实只关心编译出来的相关文件。如刚刚的举例,我们其实只需要获取镜像的最后一层就可以了。那么使用自己写的工具,可以仅仅把最后一层下载下来。下载下来的tar包进行解包,就可以直接获取出编译结果,即编译过程生成的相关文件了。Docker Hub就成为了我们的一个强大的在线编译器。

    注:这里说的镜像下载过程是针对的Registry v1版本。Docker Hub在不久之后即将全面结束v1的服务。目前国内的几家容器服务提供商还可以支持v1。该方法同样有效。v2的协议和代码我还没学习,后面研究之后再同大家分享。
    镜像层合并
    镜像层合并这个话题一直是一个有争议的话题。过长的Dockefile会导致一个冗长的镜像层数。而因为镜像层数过多(比如十几层,几十层),可能会带来的性能和稳定性上的担忧也不无道理,但是似乎Docker社区一直不认为这是一个重要的问题。所以基本上对于镜像层合并的PR最后都被拒了。但是这不影响我们在这里讨论他的实现。

    我为Dockerfile增加了两个指令。TAG和COMPRESS。

    TAG功能类似于`docker build -t`的参数。不过`build -t`只能给Dockerfile中的最后一层镜像打上tag。新增加的TAG指令可以在build生成的中间层也用标签记录下来。比如
    	FROM centos:centos6
    RUN yum install -y sshd
    TAG sshd:latest
    ADD test /
    CMD /bin/bash

    这个TAG功能相当于使用下面的Dockerfile生成了这样的一个镜像,并打上了sshd:latest的标签。
    	FROM centos:centos6
    RUN yum install -y sshd

    COMPRESS功能实现了一个镜像多层合并的功能。比如下面这个Dockerfile:
    	FROM centos:centos6
    RUN yum install -y sshd
    ADD test /
    CMD /bin/bash
    COMPRESS centos:centos6

    我们知道这里假设`RUN yum install -y sshd`,ADD test /, CMD /bin/bash生成的镜像层为a、b、c。那么COMPRESS的功能目标就是将新增的a、b、c的文件和配置合并为一个新的层d,并设置层d的父亲为镜像centos:centos6。层d的配置文件可以直接使用层c的配置文件。合并的难点在于如何计算层d的文件。

    这里有两种做法,一种是把层a、b、c中的文件按照合并的规则合并起来。合并的规则包括子层和父层共有的文件则使用子层的,没有交叉的文件则全部做为新添加的。这种方法效率较低,在需要合并的层数过多的时候,会极为耗时。

    另外一种思路则较为简单,不需要考虑中间总共有多少层。直接比较centos:centos6镜像和c镜像(c镜像是指由c和其所有父层组成的镜像),将两者的所有文件做比较,两者的diff结果即为新层d。

    最终,我采用了后者作为COMPRESS的实现。镜像的合并缩减了层数,但是弊端在于将生成镜像的Dockerfile信息也消除了(使用Dockerfile生成的镜像,可以通过docker history进行回溯)。
    dind
    dind(Docker in Docker),顾名思义就是在容器里面启动一个Docker Daemon。然后使用后者再启动容器。dind是一种比较高级的玩法,从另一个角度来说也是一种有一定风险的玩法。dind巧妙的利用了Docker的嵌套的能力,但是令人颇为担心的是底层graph driver在嵌套后的性能和稳定性。所以dind我并不推荐作为容器的运行环境来使用(RancherOS其实是使用了这种方式的),但是使用其作为构建镜像的环境,可以进行实践。毕竟构建失败的后果没有运行时崩溃的后果那么严重。

    之所以会用到dind,是因为如果用于镜像构建,那么直接使用多个物理机,未免比较浪费。因为构建并不是随时都会发生的。而使用dind的方式,只需在需要的时候申请多个容器,然后再在其上进行构建操作。在不需要时候就可以及时释放容器资源,更加灵活。

    制作dind的镜像需要一个CentOS的镜像(其他暂未实践过,fedora/ubuntu也都可以做),和一个wrapdocker的文件。wrapdocker的主要作用是容器启动后为Docker Daemon运行时准备所需的环境。

    因为容器启动后,Docker还需要一些环境才能启动daemon。比如在CentOS下,需要wrapdocker把cgroup等准备好。使用CentOS的镜像创建一个容器后,安装Docker等Docker需要的组件后,然后把wrapdocker ADD进去。并把wrapdocker添加为ENTRYPOINT或者CMD。然后将容器commit成为镜像,就获得了一个dind的镜像。使用dind的镜像时需要使用privileged赋予权限,就可以使用了。

    熟悉Docker源码的同学应该知道,dind其实并不陌生。在Docker项目里,就有这样一个dind的文件。这个dind文件其实就是一个wrapdocker文件。在Docker进行集成测试时,需要使用该文件,协助准备环境以便在容器内部启动一个Daemon来完成集成测试。

    如果对于dind有兴趣,可以参考jpetazzo中的Dockerfile和wrapdocker,构建自己的dind镜像。

    dind中Docker的使用跟普通Docker一样。不再赘述。
    关于镜像的思考
    Docker镜像由若干层组成。而其中的每一层是由文件和配置组成的。如果把层与层之间的父子关系,看做一种时间上的先后关系,那么Docker镜像其实与Git十分的相像。那么从理论上来说,Git的若干功能,比如merge、reset、rebase功能其实我们都可以在Docker的构建过程中予以实现。比如上文中的COMPRESS功能,就类似于Git的merge。理论上,Docker镜像其实也可以拥有Git般强大的功能。从这点上来说,Docker镜像的灵活性就远高于KVM之类的镜像。

    在这里,不得不抱怨几句。Docker的维护者们对于dockerfile或者说Docker的构建过程并没有给予非常积极的态度,予以改善。当然这也可能是由于他们的更多的关注点集中在了runC、libnetwork、Orchestration上。所以没有更多的人力来完善Docker构建的工具,而是寄希望于社区能自己增加其他的tool来丰富Docker的构建过程。

    所以很多时候,docker build的功能并不尽如人意。比如一直呼声很高的Docker镜像压缩功能,几经讨论,终于无果而终。又比如在build过程中,使用--net参数来使得可以控制build过程中容器使用的网络。该讨论从今年的一月份开始讨论,至今仍未定论结贴。大家可以去强势围观。地址在这里

    这里特别说一下,在CentOS 6下,dind不能使用网桥(centos7可以支持),所以在CentOS 6下使用dind,进行docker build,需要指定网络--net=host的方式。

    所以很多功能并不能等待Docker自己去完善,只好自己动手开发。其实熟悉了Docker源码后,关于docker build这方面的开发难度并不是很大。可以自己去实现。读一下孙宏亮同学的《Docker源码分析》,会很快上手。
    Q&A
    Q:京东私有云是基于OpenStack+Docker吗,网络和存储的解决方案是什么?

    A:是的。私有云网络使用的是VLAN。并没有使用租户隔离,主要保证效率。存储使用的是京东自己的存储。



    Q:那个镜像压缩,有什么好处?

    A: 镜像压缩或者说合并,主要是减少层数,减少担忧。其实目前看,好处并不明显。因为层数过多带来的更多的是担忧,但没有确凿证据表明会影响稳定。



    Q:在线编译应用广泛吗?我们一般可能更关注最后的结果。有很多代码都是先在本地编译,成功后,再发布到镜像中的。

    A:这个玩法应该说并不广泛。主要是我自己玩的时候,不想自己去拉镜像的全部层,只关注编译结果。所以这样玩



    Q:对于Docker镜像的存储京东是使用什么方式实现的分布式文件系统京东Docker上有使用吗能否介绍下?

    A:镜像存储使用的是官方的registry。v1版本。registry后端是京东自研的JFS存储。



    Q:你之前提到了“镜像的合并缩减了层数,但是弊端在于将生成镜像的Dockerfile信息也消除了(使用Dockerfile生成的镜像,可以通过docker history进行回溯)。”那如果使用了Compress之后,应该如何进行回溯?还是说需要舍弃这部分功能?

    A:是的,确实没办法回溯了,所以要舍弃了。不过反过来想,其实如果Dockerfile的ADD和COPY之类的功能,就算能回溯,其实意义也不大。所以我认为保存Dockerfile更有意义。



    Q:为什么不采用将要执行的命令做成脚本,直接add进去执行这种,也能减少层数?

    A:这种方法也是可行的。只是Dockerfile更显式一些。同理,其实只要你做好镜像,直接export出去,就可以得到所有文件了。再配上配置文件。这样整个就只有一层了。




    Q:我平时在,测试的时候并没-有压缩过,也不知道,压缩会带来什么风险,但是,看你刚才说有可能会带来一定的风险。 你们遇到过么?

    A:因为我们的镜像都做过合并层,所以层数并不多。不合并会带来什么风险,其实更多的是出于性能和稳定性上的担忧。这种担忧可能是多余的。但是我们宁愿选择谨慎一些。



    Q:镜像的合并方面怎么样能方便的减小镜像的大小,我做的镜像有些都在1G以上?

    A:减少镜像大小主要还是靠去除不必要的文件。合并只能减少冗余文件,如果每层的文件都不相同,合并并不会缩小镜像的大小。



    Q:网络这个使用VLAN能说详细一些吗,是每个容器都有一个和宿主机同网段的真实的物理IP吗?

    A:是的。每个容器都有一个真实的IP。跟宿主机网段不同。是单独的容器网络。这个可以参考neutron中的Vlan实现。



    Q:还有,把镜像压缩我也觉,但是像你那样把父镜像整个合并成新镜像这点我觉得有点问题,毕竟大家玩容器时都是在基础镜像上添加东西,你把常用的镜像为了压缩生成一个一次性的镜像,以后再使用基础镜像做其他业务时那不还得重新下载基础镜像?

    A:镜像合并其实主要还是为了获得一个基础镜像。然后大家在基础镜像上添加东西。基础镜像相对来说,不会轻易改变。



    Q:在你们的实践中,大规模部署容器时,每个节点都会从Registry节点下载镜像,给网络带来的压力大吗?

    A:我们做了一些优化。首先,大部分业务使用的镜像会提前推送到每个Docker节点上。即使节点没有,Registry后端接的是京东的JFS,通过优化,临时去下载的时候可以直接从JFS去拿镜像数据。所以网络压力并不大。



    Q:镜像压缩或者合并之后,镜像的层次减少了,但每层镜像不是变大了吗,这对于发布不是会占用带宽降低效率吗?

    A:这个问题跟上个差不多。合并主要是为基础镜像使用的。



    Q:你们怎么看待OpenStack和Docker的关系?在京东未来会长期两个并存吗?现在两个架构的发展速度和研发力量对比如何?

    A:OpenStack和Docker并不矛盾。私有云采用nova docker的结合更多的是迎合用户习于使用VM的习惯。Magnum也在快速发展中。所以我相信二者都有存在的价值和发展的必要。



    Q:关于dockfile的优化,你们有没有什么好的建议或者经验?

    A:似乎也没多少新的建议。参考DockOne的相关文章。Dockerfile之优化经验浅谈大家在写 dockerfile 时有啥最佳实践?希望得到大家的建议



    Q:比如创建一个rabbitmq镜像,需要安装很多依赖包,最后编译,最后生成的镜像1.3G,像这种情况,在创建镜像的时候能否减少镜像的大小呢?

    A:并没有什么好的办法来减少。可能需要一定的人工或者工具去分析不需要的文件,来减少镜像的大小。



    Q:Docker是如何进行自动更新的,自己搭建的镜像仓库,如何更新新版本的镜像?

    A:Docker我们固定了一个版本。如果没出大面积的严重问题,几乎不会更新。目前来看,运行稳定。所以也没有更新必要。新版本的Docker提供的如网络等,暂时我们还不会大面积跟进使用。自己的镜像仓库,如果要更新新版本镜像,push进去就可以了。



    Q:一个困扰我比较久的问题,如果镜像间存在依赖关系,基础镜像发生改变后其他镜像你们是跟着更新的呢?

    A:在内部私有云中,一般大家使用的都是一个做好的base镜像。这里面就有一个问题,一旦这个base镜像需要打补丁,影响面比较大。首先很多base的子镜像会受到影响。另一方面,就是要考虑已经在使用基于base或者base子镜像的节点。前者我的方案是直接在base镜像中的layer,把需要打补丁的文件加入进去,重新打包放回。对于后者,目前还没想到很好的方法解决。



    Q:在运行容器的时候,1、应用里面的日志或者配置文件,使用本地映射是不是好点,我是考虑到方便查看日志或者修改配置;2、创建的数据库镜像,在运行容器的时候把数据文件是不是映射到本地更好些呢?

    A:日志我们的确是使用的本地映射。而且有的业务方狂写日志不加约束。所以我们给本地映射做了个LVM,挂给容器。做了容量上的限制。配置的话,现在是有一个内部的部署系统会帮他们部署配置。数据库的话是一个道理,也是映射到本地。也有一部分接入了云硬盘。



    Q:Docker中,每层镜像打标签那我觉的很奇怪,当pull一个镜像或生成一个容器时,它如何找到你所命名的镜像层?

    A:并不是给每层都打标签,而是你根据你的需要来给某一层打标签。至于标签内容,需要自己来进行控制。



    Q:关于Compress的实现有些疑问,是不是在实现的过程中,只考虑最后的镜像和前一层的diff,还是说要逐层做diff?

    A:是只考虑最后的镜像和你要合并到的父层镜像做diff。这样只要做一次diff,就可以获得中间的所有文件变化了。



    Q:wrapdocker文件的工作原理是什么?

    A:这个工作原理主要是准备一些Docker启动必要的环境。比如在CentOS下,需要wrapdocker把cgroups等准备好等。你可以参考下wrapdocker里面的代码。



    Q:容器运行在物理机上,与OpenStack平台虚拟机是同一套管理系统?如何与容器的集群系统整合?

    A:是同一套系统,都是用nova。虚拟机KVM和容器主要是镜像类型不同。在nova调度的时候,会根据镜像类型调度到KVM或者Docker节点进行创建。



    Q:在一台物理机上运行Docker的数量是否有限定 还是看运行的应用来决定?

    A:没有特别做限定。主要还是业务方去申请的。业务方习惯用大内存,多CPU的。那这个物理机上创建的容器数就少些。大致这样。



    Q:想了解一下,你们对镜像的tag是怎么管理的?根据什么来打的?对于旧的镜像你们是丢弃还是像Git保存代码一样一直保留在仓库呢?

    A:tag由各个用户来定。不同的用户在不同的Repository里。镜像tag自己管理。不过我们更希望他们能够更加规范一些,比如用git的版本号来打tag。
    旧的镜像如果失去了tag(新的镜像抢夺了该tag),则旧镜像会被删除。不过不是立即,也是定期清理,主要减少存储量。因为毕竟不需要存储那么多的版本。



    ===========================
    以上内容根据2015年12月8日晚微信群分享内容整理。分享人:徐新坤,京东商城云平台南京研发中心JDOS团队研发工程师,从2014年初开始从事Docker的研发,主要负责Docker在京东落地的相关开发和维护工作。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesx,进群参与,您有想听的话题可以给我们留言。

    推荐一个在线 Dockerfile 语法检查优化工具

    oilbeater 发表了文章 • 3 个评论 • 6388 次浏览 • 2015-11-26 10:57 • 来自相关话题

    http://dockerfile-linter.com/ 看上去还不错,建议也很靠谱 要使用 tag,但不要 latest。Debian 挺好的,不要总 Ubuntu。apt-get update 在前,r ...查看全部
    http://dockerfile-linter.com/

    看上去还不错,建议也很靠谱

    • 要使用 tag,但不要 latest。
    • Debian 挺好的,不要总 Ubuntu。
    • apt-get update 在前,rm -rf /var/lib/apt/lists/* 在后。
    • yum install,不忘 yum clean。
    • 多 RUN 要合并,来减少层数。
    • 无用的软件,不要乱安装。
    • COPY 放最后,缓存很开心。
    • 善用 dockerignore,不浪费传输。
    • 不忘 MAINTAINER,这都是我的。

    dockerfilelinter.jpg


    Docker 逗你玩儿 --- 在线 Docker 镜像征集活动

    我是王永和 发表了文章 • 0 个评论 • 3688 次浏览 • 2015-07-07 10:24 • 来自相关话题

    今年的6月17日, Git@OSC 携手灵雀云支持 Docker 项目一键部署【相关链接】,为 Git@OSC 打造全新一站式极速持续交付体验,让 OSChina 用户享受到新一代的基于 Docker 镜像的演示平台。 ...查看全部
    今年的6月17日, Git@OSC 携手灵雀云支持 Docker 项目一键部署【相关链接】,为 Git@OSC 打造全新一站式极速持续交付体验,让 OSChina 用户享受到新一代的基于 Docker 镜像的演示平台。
    102149_RxkU_943418.jpg

    开源软件的世界如同一个取之不尽的宝藏,提供了成千上万款拿来即可使用的软件/系统,尤其是基础软件,如博客平台 Wordpress、代码托管平台 gitlab、内容管理系统 Joomla、网站商店系统 ECShop、客户关系管理系统 SugarCRM …… 试想如果将这些基础的应用系统/软件做成一个个Docker镜像,并分享出来,方便更多的用户拿来即用,岂不是一件普惠大众的美事?

    开源精神离不开“创新“和”分享“,我们号召各位开源热心人士,将这些基础应用做成一个个docker镜像,并将之展示和分享出来,供更多的人学习、使用,甚至参与进来
    #活动前提
    创建的Docker镜像必须是一些基础、通用的软件,更多软件,可在开源中国的软件库进行检索:http://www.oschina.net/project
    #活动步骤
    1. 代码托管到 git@osc 上,在 Readme 文件上详细描述应用的相关内容以及使用方法;
    2. 将应用部署到项目演示平台并 run 起来;
    3. 将项目代码的页面地址私信发给 @小编辑

    #评选方法及奖品
    作为活动的发起方,我们联手 灵雀云, 为大家准备了一些小礼品,按私信提交时间的先后,对每一个应用进行验证后得到排名,入围者按照先后顺序从奖品列表里头挑选自己喜爱的奖品。本次活动奖品有限,只发给前200名的作者,略表心意,还望海涵,欢迎商家和个人提供奖品赞助(联系 wyh#oschina.net):

    * 开源马克杯 共 50 个
    * 小米手环 共 5 个
    * 手机自拍杆 50 个
    * 灵雀云 T-shirt 50 件
    * JetBrains 溜溜球 45个

    07082102_krB0_(1).png

    对于优秀作品,我们将会在git@osc平台上重点宣传和推广。
    本次活动所有解释权归OSCHINA.NET官方所有。

    欢迎访问: https://git.oschina.net/