本文主要介绍在编写 docker 镜像的时候一些须要注意的事项和推荐的作法。php
虽然 Dockerfile 简化了镜像构建的过程,而且把这个过程能够进行版本控制,可是不正当的
Dockerfile 使用也会致使不少问题: python
但愿读者可以对 docker 镜像有必定的了解,阅读这篇文章至少须要一下前提知识:nginx
当运行 docker build 命令的时候,整个的构建过程是这样的:git
a. 读取 Dockerfile 文件发送到 docker daemon
b. 读取当前目录的全部文件(context),发送到 docker daemon
c. 对 Dockerfile 进行解析,处理成命令加上对应参数的结构
d. 按照顺序循环遍历全部的命令,对每一个命令调用对应的处理函数进行处理
e. 每一个命令(除了 FROM)都会在一个容器执行,执行的结果会生成一个新的镜像,为最后生成的镜像打上标签web
编写 Dockerfile 的一些最佳实践
1.使用统一的 base 镜像sql
有些文章讲优化镜像会提倡使用尽可能小的基础镜像,目前集团操做系统一级提供统一的基础镜像,一些BU也根据本身的技术规范定义了BU级的基础镜像,通常的应用只须要FROM本身BU提供的基础镜像便可,由于基础镜像只须要下载一次能够共享,并不会形成太多的存储空间浪费。它的好处是这些镜像的生态比较完整,方便咱们安装软件,除了问题方便调试。docker
2.动静分离shell
常常变化的内容和基本不会变化的内容要分开,把不怎么变化的内容放在下层,建立出来不一样基础镜像供上层使用。好比能够建立各类语言的基础镜像,这些镜像包含了最基本的语言库,每一个组能够在上面继续构建应用级别的镜像。数据库
3.最小原则:只安装必需的东西apache
不少人构建镜像的时候,都有一种冲动——把可能用到的东西都打包到镜像中。要遏制这种想法,镜像中应该只包含必需的东西,任何能够有也能够没有的东西都不要放到里面。由于镜像的扩展很容易,并且运行容器的时候也很方便地对其进行修改。这样能够保证镜像尽量小,构建的时候尽量快,也保证将来的更快传输、更省网络资源。
4.一个原则:每一个镜像只有一个功能
不要在容器里运行多个不一样功能的进程,每一个镜像中只安装一个应用的软件包和文件,须要交互的程序经过 容器之间的网络进行交流。这样能够保证模块化,不一样的应用能够分开维护和升级,也能减少单个镜像的大小。
5.使用更少的层
虽然看起来把不一样的命令尽可能分开来,写在多个命令中容易阅读和理解。可是这样会致使出现太多的镜像层,而很差管理和分析镜像,并且镜像的层是有限的。尽可能把相关的内容放到同一个层,使用换行符进行分割,这样能够进一步减少镜像大小,而且方便查看镜像历史。
6.减小每层的内容
尽管只安装必须的内容,在这个过程当中也可能会产生额外的内容或者临时文件,咱们要尽可能让每层安装的东西保持最小。
好比使用 --no-install-recommends 参数告诉 apt-get 不要安装推荐的软件包
7.不要在 Dockerfile 中单独修改文件的权限
由于 docker 镜像是分层的,任何修改都会新增一个层,修改文件或者目录权限也是如此。若是有一个命令单独修改大文件或者目录的权限,会把这些文件复制一份,这样很容易致使镜像很大。
解决方案也很简单,要么在添加到 Dockerfile 以前就把文件的权限和用户设置好,要么在容器启动脚本(entrypoint)作这些修改,或者拷贝文件和修改权限放在一块儿作(这样最终也只是增长一层)。
8.利用 cache 来加快构建速度
若是 Docker 发现某个层已经存在了,它会直接使用已经存在的层,而不会从新运行一次。若是你连续运行 docker build 屡次,会发现第二次运行很快就结束了。
不过从 1.10 版本开始,Content Addressable Storage 的引入致使缓存功能的实效,目前引入了 --cache-from 参数能够手动指定一个镜像来使用它的缓存。
9.版本控制和自动构建
最好把 Dockerfile 和对应的应用代码一块儿放到版本控制中,而后可以自动构建镜像。这样的好处是能够追踪各个版本镜像的内容,方便了解不一样镜像有什么区别,对于调试和回滚都有好处。
另外,若是运行镜像的参数或者环境变量不少,也要有对应的文档给予说明,而且文档要随着 Dockerfile 变化而更新,这样任何人都能参考着文档很容易地使用镜像,而不是下载了镜像不知道怎么用。
10.使用一个.dockerignore文件
在大部分状况下,最好的作法是将每个Dockerfile文件放到一个空的文件夹里。接着,把构建Dockerfile所需的文件添加到这个文件下。为了提升构建的效率,你能够在这个文夹下添加一个.dockerignore 文件来排除那些没用的文件和文件夹。这个文件支持相似 .gitignore 文件那样的排除模式。关于如何建立它,能够移步到dockerignore 文件。
Dockerfile 指令介绍:
更多信息请参考《Dockerfile 参考》
FROM :
这个设置基本的镜像,为后续的命令使用,因此应该做为Dockerfile的第一条指令。FROM <image>:<tag>
不管何时,尽量使用BU提供的基础镜像,有利于技术规范化,简化你的Dockerfile。
RUN :
RUN命令会在上面FROM指定的镜像里执行任何命令,而后提交(commit)结果,提交的镜像会在后面继续用到。格式
RUN <command> (the command is run in a shell - `/bin/sh -c`)
通常,为让你的 Dockerfile 更加易读,易懂和便于维护,请将长的或者复杂的 RUN 语句用反斜杠()分割成多行。
RUN 通常都是搭配 apt-get一块儿使用。当使用 apt-get时,这里几个注意事项:
不要在单独一行上使用RUN apt-get update 。 这样会引发缓存问题,若是关联的归档文档被更新了,将会致使后续的 apt-get install 执行失败而没有任何提示。
避免 RUN apt-get upgrade 或dist-upgrade, 由于不少来自基础镜像的“底层”的包将会更新失败,在一个无特权的容器里。若是一个基础包已通过期,你应该通知它的维护人员。若是你知道这里一个特定的包,如 foo,它须要更新,能够直接使用apt-get install -y foo 让它自动更新。
应该这样编写你的指令:
RUN yum update && yum install -y \ package-bar \ package-baz \ package-foo
使用这样方法编写指令,不只让它变得更加易读和可维护,并且,经过包含 apt-get update,确保绕开本地的缓存,安装最新的版本而不须要编写更多的指令和手动的干预。
绕开缓存能够实现包的版本定位(例如:package-foo=1.3.*)。这将强制去检索指定的版本,无论缓存里存储了什么。编写你的 apt-get 代码,这种方法将大大下降的维护难度和减小由未意料的的包而致使失败几率。
例子
下面是一段格式良好的 RUN 指令,它演示了上述的建议。注意最后的包 s3cmd,指定了一个版本 1.1.0*。若是这个镜像以前使用过一个旧的版本,指定的新版将引发 apt-get update 缓存失效,确保一个新的版本被安装(在这个应用场景中,须要这个特性)。
RUN yum update && yum install -y \ aufs-tools \ automake \ btrfs-tools \ build-essential \ curl \ dpkg-sig \ git \ iptables \ libapparmor-dev \ libcap-dev \ libsqlite3-dev \ lxc=1.0* \ mercurial \ parallel \ reprepro \ ruby1.9.1 \ ruby1.9.1-dev \ s3cmd=1.1.0*;yum clean all
使用这种方法编写指令也能够帮助你避免包的重复,由于这样写比下面的写法更加的易读:
RUN yum install -y package-foo && yum install -y package-bar;yum clean all
EXPOSE
EXPOSE 指令指定容器监听的端口。所以,你应该使用通用、惯例的端口到你的应用。例如,一个包含着Apacheweb服务端的镜像将使用80端口,当镜像包含是一个MangoDB应该使用EXPOSE 27017 等等。
为了提供外部访问,你的用户能够执行docker run 带上一个标志,代表如何映射指定的端口到他们选择的端口。为了容器的链接,Docker提供了环境变量来指定接受容器到源容器的路径(如,MYSQL_PORT_3306_TCP)。
ENV
为了方便新安装的软件的运行,你可使用ENV 去更新环境变量PATH 。例如,ENV PATH /usr/local/nginx/bin:$PATH 保证CMD [“nginx”] 能够正常运行。
ENV 指令也能够为容器化的运用提供必需的环境变量,好比,Postgres的 PGDATA。
最后,ENV 也能够用来设置经常使用的版本号,这样,可让版本维护更加容易,正以下面的例子:
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-PG_MAJOR/bin:$PATH
和在编程时定义常亮相似(而不采用硬编码),使用这样方法,你只需修改一个ENV 指令,就能自动更新与之关联的数据。
ADD 或 COPY
虽然 ADD 和COPY 的功能相似,通常而言,推荐使用COPY 。由于它比ADD更加见名知意。COPY 只支持将本地本件拷贝到容器中,虽然ADD 拥有一些功能(例如,抽取本地tar文件内容和支持远程URL),可是这些功能不是很经常使用。所以,ADD 的最佳使用场景是,自动抽取一个本地tar的内容到镜像中,例如:ADD rootfs.tar.xz /。
若是你要执行多个Dockerfile 步骤且使用来自的环境中不一样的文件,分开COPY 它们,而不是一次性的拷贝它们。这样能够确保每一个步骤的构建缓存都是失效的(强制步骤的重作),若是指定须要的的文件更新了。
例如:
COPY requirements.txt /tmp/ RUN pip install /tmp/requirements.txt COPY . /tmp/
这样,RUN 步骤能够增长缓存的命中率,若是你把COPY . /tmp/ 放到它前面,反之。
出于镜像的大小的考虑,使用 ADD 从远程URL提取内容的方法强烈不推荐。你应该使用curl 或 wget 替代。这种方法容许你在提取完内容后,能够删除你不须要的文件。例如,你应该避免这样作:
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all
相反,你应该这样作:
RUN mkdir -p /usr/src/things \ && curl -SL http://example.com/big.tar.xz
| tar -xJC /usr/src/things \ && make -C /usr/src/things all
除了须要从tar文件中提取内容时使用ADD,其余时候,你应该老是使用COPY。
COPY指令是以root身份执行的。但集团pouch在启动应用时会将/home/admin的属主置为admin,因此用户通常不须要额外的指令来处理COPY到/home/admin/目录下的文件属主权限。
ENTRYPOINT
ENTRYPOINT 最佳使用场景是设置镜像的主入口命令,容许镜像好像命令运行同样(使用 CMD 做为默认的标志)。
让咱们启动一个带命令行工具 s3cmd的镜像:
ENTRYPOINT ["s3cmd"] CMD ["--help"]
如今,启动后的镜像与在命令行中执行命令的帮助相似:
$ docker run s3cmd
或在右边添加参数来执行一个命令:
$ docker run s3cmd ls s3://mybucket
这很用,如上所述,能够把镜像的名字当作一个二进制程序来使用。
ENTRYPOINT 指令也能够和一个辅助脚本结合使用,容许它和上述的相似方式运行,即便当启动工具命令超过一行时。
例如,Postgres官方镜像使用下面的脚本做为它的ENTRYPOINT:
#!/bin/bashset -eif [ "PGDATA"if [ -z "
PGDATA")" ]; then
gosu postgres initdb
fiexec gosu postgres "@"
注意:这个脚本使用了exec Bash指令,运行时的应用程序会变成容器的PID 1。这将容许应用能够接收发送到容器的全部Unix信号。 查看ENTRYPOINT 帮助文档得到更多的信息。
将这个辅助脚本拷贝到容器里,经过 ENTRYPOINT 来启动容器:
COPY ./docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"]
这个脚本容许用户使用几种交互的方法启动Postgres:
能够简单的启动Postgres:
$ docker run postgres
或者,可使用它去运行带几个参数的Postgres:
$ docker run postgres postgres --help
最后,它也能够用来启动一个彻底不一样的工具,如,Bash:
$ docker run --rm -it postgres bash
VOLUME
建立一个挂载点用于共享目录。
VOLUME 指令应该用于暴露任何的数据库存储域、配置存储、文件/文件夹,在建立容器的时候。任何易变的或镜像的供用户使用的部分,建议使用VOLUME 。
docker run时会将宿主机的目录挂载到VOLUME目录下,以宿主机某个目录(此镜像独享的目录)覆盖docker容器中的对应目录,使得其中的数据修改在docker重启时仍然能保持;带来另外一个后果是,若是你在Dockerfile中往VOLUME目录中写入了数据(即docker build阶段写入的数据),在启动容器的时候你会发现它不见了(由于它写到编译机上去了)。
USER
指定运行用户。
若是一个服务能够不须要权限就能运行,应该使用 USER 切换到一个非root用户。使用像这种命令 RUN groupadd -r postgres && useradd -r -g postgres postgres能够建立一个用户和用户组。
注意:镜像里的用户和组的UID/GID都是不肯定的,无论它是否被重建。若是这些信息对你很重要,你应该显示的指定一个UID/GID。
你应该避免安装或使用 sudo ,由于这些操做带来不肯定的TTY和信号的转发行为,是一个得不偿失的设置。若是你必须要使用相似 sudo 的功能(例如,在非root用户在初始化一个须要root权限的的守护进程),你可能须要使用“gosu”。
最后,为了减小层和复杂度,不建议频繁的来回切换 USER 。
WORKDIR
更多内容请移步《Dockerfile参考》的WORKDIR部分
为了清晰和可靠,你应该始终为你的WORKDIR指定一个绝度路径。另外,你因该使用WORKDIR 来替代相似RUN cd … && do-something指令,这样能够下降可读性、故障排除难度、维护成本。
CMD
CMD 命令应该用来运行包含软件的镜像,连同任何参数。CMD 应该老是使用这种格式CMD [“executable”, “param1”, “param2”…]。 这样,若是这个镜像承载着一个服务(Apache,Rails等),你能够运行相似CMD ["apache2","-DFOREGROUND"]的指令。 事实上,这种格式的指令,不管那种基于服务的镜像,都值得推荐。
在大多的其余场景里,CMD 应该指定一个交互式的shell (bash, python, perl, 等),例如,CMD ["perl", "-de0"], CMD ["python"], 或 CMD [“php”, “-a”]。 使用这些格式相似你执行docker run -it python,你将进入一个可用的shell中,准备好了。当CMD 和ENTRYPOINT 协同工做时,应该使用 CMD [“param”, “param”] 格式。这种方式尽可能少用,除非你和你的用户对 ENTRYPOINT 实现机制都很了解。
ONBUILD
更多内容请移步《Dockerfile参考》的ONBUILD部分
一个ONBUILD 命令在当前的Dockerfile 构建完成后会被执行。当使用 FROM 为镜像个派生出子镜像时,ONBUILD 也会被执行。也能够简单的理解为,实际上是将父Dockerfile 的ONBUILD 中的指令放到子Dockerfile中。
ONBUILD 命令会先于子Dockerfile中全部命令执行。
ONBUILD 对使用 FROM 基于指定镜像构建颇有帮助。例如, ONBUILD 容许你在 Dockerfile里,基于某种语言栈构建任意的软件镜像,你能够参考Ruby的 ONBUILD 。.
ONBUILD 因该指定一个指定标志(tag),例如:ruby:1.9-onbuild 或 ruby:2.0-onbuild。
当你把 ADD 或 COPY 放到ONBUILD要注意。 若是新的构建环境缺乏要添加的资源,会致使镜像的构建失败。添加一个分隔标签,如条建议同样,以供编写的 Dockerfile 能够选择合适他的构建环境。