Dockerfiles构建镜像最佳实践

Dockerfiles构建镜像最佳实践

浏览:12

Docker 能从Dockerfile读取指令自动构建docker镜像,Dockerfile类似于Makefile,都是一种文本文件,按照构建镜像顺序组织包含所有的命令。Dockerfile采用特定格式和特定的指令集合。我们可以通过阅读 Dockerfile参考页面来了解基础命令的使用。如果您是编写Dockerfile的新手,建议您从这里开始。

本文档覆盖最佳的实例和Docker,Inc和致力于创建易用高效Dockerfile的Docker社区推荐的方法,强烈推荐大家准寻这些建议(事实上,如果我们想要创建一个官方镜像,我们必须采用这些实践经验)。

我们可以看到这些经验和建议在buildpack-deps下Dockerfile的作用。

一般准则和建议

容器应该是短暂的

由Dockerfile定义的镜像产生的容器应该尽可能的短暂的。此处短暂的意思是容器可以被停止、销毁并且可以经过最小的安装和配置就可以重新创建和部署。

使用.dockerignore文件

这个文件的作用帮助我们在过滤一些文件来提高我们创建镜像的效率,支持模式匹配规则类似与.gitignore文件。通过阅读 .dockerignore file来了解如何使用。

避免安装不必要的包

为了减小复杂度、依赖、文件大小和创建的时间,我们应该避免安装额外或者不必要的包。例如,我们不必在一个数据库镜像中包含一个文本编辑器。

一个容器仅运行一个进程

在几乎所有的情况下,我们应该一个容器运行一个进程。

多个容器中的解耦应用更加容易横向扩展和复用容器。如果一个服务依赖另一个容器,使用 容器连接[1]。

 

保证镜像的层数的最小化

我们需要了解可读性和最小化层数的平衡关系。对于层数的控制要有策略并且谨慎。

对于多行参数要做字典序排序

只要有可能,通过对多行参数进行字母数字排序来缓解后续变更。这将帮助你避免重复的包并且更容易更新。这将对PRs来说更容易阅读和复审。在反斜线( \ )前添加一个空格是哥好习惯。

 
RUN apt-get update && apt-get install -y \

 

bzr \

 

cvs \

 

git \

 

mercurial \

 

subversion

 

build的缓存

我们在创建镜像过程中,Docker将按照Dockerfile指定步骤执行每个指令。因为每个指令都会被检查,Docker将会查找已经存在的镜像的缓存看是否可以复用,而不是重复创建一个新的镜像。我们可以在docker build命令中使用--no-cache=true选项。

然而,如果我们允许Docker使用它的缓存,这对于我们理解它何时查找一个匹配镜像非常有帮助。匹配的基础规则如下:

开始于一个已经存在于缓存中的基础镜像,下一个指令将会比较所有集成此镜像的子镜像来查看是否有相同的指令。如果没有,这个缓存是无效的。

在大多数情况下,简单的将Dockerfile的指令与任一一个子镜像比较是足够的。然而一些指令需要更多一些的检查和说明。

 

对于ADD和COPY指令,镜像中的文件内容被检查并且为每个文件计算生成一个校验和。文件的最后修改时间和最后访问时间不被添加到校验和中。在查找缓存时,将会使用此校验和进行比较。如果文件被修改缓存就会失效(例如内容或者元数据)。

 

除了ADD和COPY指令,缓存检查不会查看容器中的文件来决定缓存的匹配。例如,RUN apt-get -y update命令更新容器中的文件,并不会影响到它的缓存内容。这种情况下仅使用命令字符串进行匹配。

 

一旦缓存无效了,所有随后的Dockerfile命令将会生成新的镜像而且不会使用到缓存内容。

Dockerfile 指令介绍

以下提供了一些建议,它会帮助我们编写更好的Dockerfile。

FROM

尽可能使用官方仓库作为基础镜像。推荐使用Debian image因为它是严格控制的并且占用存储比较小(150MB左右),目前仍在全量分发。

RUN

为了使得Dockerfile更可读、更容易理解和维护,尽量将单行长的复杂的RUN语句拆分为用反斜线( \ )分隔的多行。

apt-get

RUN apt-get经常用来安装包,但他有一些陷阱需要小心。

我们要避免运行RUN apt-get upgrade或者dist-upgrade命令,因为很多基础镜像“必要”的包不应该在容器中更新。如果一个包过期了,我们需要联系它的维护者。如果我们知道有一个特殊包foo需要更新,使用apt-get install -y foo来自动更新即可。

 

我们经常结合update和install 在同一个RUN语句中,如下:

 

   
RUN apt-get update && apt-get install -y \

 

package-bar \

 

package-baz \

 

package-foo

 

单独使用apt-get update会引起缓存问题,并且随后的apt-get install指令会失败。例如如下的Dockerfile内容:

 FROM ubuntu:14.04

 

 RUN apt-get update

 

 RUN apt-get install -y curl

 

创建镜像后,所有层都在Docker的缓存中。假如你后来修改了apt-get install通过添加额外包 :

 
  FROM ubuntu:14.04

 

  RUN apt-get update

 

  RUN apt-get install -y curl nginx

 

结果RUN apt-get update不会被执行,我们的创建结果可能会获得一个过期版本的curl和nginx包。

使用如下命令就可以确保每次安装的都是最新版本的包。这个技术以cache busting(缓存失效)著称。我们也可以通过指定包版本来达到同样的效果。这种方法命名为穿钉版本。如下示例:

 RUN apt-get update && apt-get install -y \

 

 package-bar \

 

 package-baz \

 

 package-foo=1.3.*

 

 

穿钉版本技术强迫创建镜像获取指定版本,这额技术可以减少因依赖包的意外变更导致的失败率。

以下是一个建议使用的RUN命令示例:

 RUN apt-get update && apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \
&& rm -rf /var/lib/apt/lists/*

s3cmd命令指定为1.1.* 版本。如果以前镜像使用的是一个老版本,这就会导致一个缓存无效,执行apt-get update命令然后安装这个新版本。

另外,清理apt缓存并且删除/var/lib/apt/lists可以减小镜像的大小。由于RUN语句开始于apt-get update,包缓存将总是之前的apt-get install被刷新。

 

说明: 官方的Debian和Ubuntu镜像自动运行apt-get clean,所以没有必要明确调用。

CMD

CMD指令应该用于运行镜像中包含的软件,同时可以携带任意的参数。CMD应该总是使用exec格式(CMD["executable", "param1", "param2"...]).因此如果镜像是用来提供一个服务(Apache,Rails,etc),我们应该像这样执行CMD["apache2", "-DFOREGROUND"]。实际上,推荐任何提供服务的基础镜像都使用这种指令的格式。

在大多数情况下,CMD应该提供一个交互的shell(bash,python,perl,etc),例如,CMD["perl","-de0"],CMD["python"],或者CMD["php", "-a"].CMD应该少用于CMD["param","param"]的方式与ENTRYPOINT联合使用,除非我们对ENTRYPOINT的工作原理非常了解了。

EXPOSE

EXPOSE指令指示容器将要监听连接的端口。所以我们应该使用通用的、传统的端口给我们的应用。例如包含Apache的web服务的镜像将使用EXPOSE 80,一个包含MongoDB服务的镜像使用EXPOSE 27017等等。

ENV

为了使得新软件更容易运行,我们常使用ENV更新PATH环境变量,例如ENV PATH /usr/local/nginx/bin:$PATH将要确保CMD["nginx"]可以工作。

ADD or COPY

ADD和COPY功能相似,COPY是首选。因为它比ADD更加透明.COPY仅支持基础的本地文件拷贝到容器中,ADD有些特性(本地tar包解压和远程URL支持) 不会立即显示。ADD最好的应用是本地tar包自动解压到镜像中,如ADD rootfs.tar.xz /。

如果我们在Dockerfile中有多个步骤使用不同文件,逐个COPY这些文件,而不是拷贝所有文件。这样确保每步骤的缓存会还重新更新。

 COPY requirements.txt /tmp/

 

 RUN pip install --requirement /tmp/requirements.txt

 

 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

 

ENTRYPOINT

ENTRYPOINT最好用于设置镜像的主要命令,允许镜像被运行如同它是一个命令一样。

我们来看下这个例子:

ENTRYPOINT ["s3cmd"]

 

CMD ["--help"]

 

使用这个镜像运行下面的命令:

$ docker run s3cmd --help

或者使用右面的参数来执行一个命令:

 $ docker run s3cmd ls s3://mybucket

注意到镜像的名字又可以作为一个它包含的可执行程序的参考,这对于我们理解镜像作用是有帮助的。

ENTRYPOINT指令可以用来与帮助脚本结合使用。如下示例:

#!/bin/bash

 

                          set -e

 

if [ "$1" = 'postgres' ]; then

                          chown -R postgres "$PGDATA"

 

if [ -z "$(ls -A "$PGDATA")" ]; then

                          gosu postgres initdb

 

                          fi

 

exec gosu postgres "$@"

                          fi

 

exec "$@"

说明: 脚本使用了execbash命令,因此运行脚本后的应用进程在容器中的PID为1,这样可以保证进程接收到任何发送给容器的UNIX信号。

这个帮助脚本会被拷贝到容器中,然后通过ENTRYPOINT指令在容器中执行。

 COPY ./docker-entrypoint.sh /

 

 ENTRYPOINT ["/docker-entrypoint.sh"]

 

脚本允许用户使用多种方式进行交互。下面是一种简单的方式:

 $ docker run postgres

也可以在运行时通过传入参数给服务:

 $ docker run postgres postgres --help

最后也可以通过交互的Bash、Perl、python等工具来执行:

 $ docker run --rm -it postgres bash

VOLUME

VOLUME指令应该用于如下内容:任何类型的数据库存储区域、配置存储、容器创建的文件或目录。

推荐VOLUME用于挂载镜像中那些经常变化(易变化的)或者用户可维护的部分。

 

USER

一个服务如果不需要超级权限,建议使用USER切换成非root用户执行。在Dockerfile中创建用户和组的方式如下:

RUN groupadd -r postgres && useradd -r -g postgres postgres

说明: 重建镜像时如果不明确指定UID/GID,我们每次可能得到的UID/GID都不会一样,因此我们应该明确的指定UID/GID。

我们尽量避免安装和使用sudo,因为它有不可预知的TTY和信号转发行为,这会给我们带来更多问题需要解决。我们可以使用gosu替代它。

最后,为了减少层数和复杂度,避免频繁使用USER进行用户切换。

WORKDIR

为了清晰度和可靠性,我们应该确保WORKDIR在镜像中使用绝对路径。我们应该使用WORKDIR而不是使用类似这些指令RUN cd ... && do-something ,这会影响其可读性、问题定位和可维护性。

ONBUILD

ONBUILD指令在执行Dockerfile创建新镜像后会存储到镜像的manifest清单中,我们可以通过docker inspect查看OnBuild的信息。

当我们使用带有ONBUILD触发器的镜像作为基础镜像来创建新镜像时,当Dockerfile执行到FROM时会自动查找OnBuild信息并执行这个触发器命令。成功后继续向下执行下一条指令,失败的话就停止向下执行并中止创建过程。如果成功创建了新的镜像后,这个新镜像中不会继承基础镜像中的OOnBuild触发器内容。参考 Ruby’s ONBUILD variants

从ONBUILD创建的镜像应该单独做一个标记,如ruby:1.9-onbuild 或者ruby:2.0-onbuild。

当把ADD或者COPY加入ONBUILD中时要小心,如果新创建镜像的上下文缺少这些要添加的资源情况会导致创建的失败。因而添加单独的标签可以帮助我们减小这种情况发生的可能,让Dockerfile作者来做决定。

扫描本文章二维码可手机访问: