首页 > 代码库 > 编写Dockerfiles的最佳做法

编写Dockerfiles的最佳做法

编写Dockerfiles的最佳做法


Docker可以通过从Dockerfile包含所有命令的文本文件中读取 指令,自动构建图像,以便构建给定图像所需的顺序。Dockerfile坚持一个具体的格式,并使用一组具体的说明。您可以在Dockerfile参考页面上了解基础知识 。如果你是新来Dockerfile的,你应该从那里开始。

本文档介绍了Docker,Inc.和Docker社区推荐的最佳做法和方法,以创建易于使用,有效 Dockerfile的。我们强烈建议您遵循这些建议(实际上,如果您正在创建官方图片,则必须遵守这些做法)。

您可以在buildpack-depsDockerfile中看到许多这些做法和建议。

注意:有关这里提到的任何Dockerfile命令的更详细的解释,请访问Dockerfile参考页面。

一般准则和建议

容器应该是短暂的

由您Dockerfile定义的图像生成的容器应尽可能短暂。通过“短暂的”,我们意味着它可以被停止和破坏,一个新的建立和建立,绝对最小的设置和配置。您可能需要查看“12因子”应用程序方法的“ 过程”部分,了解以无国籍方式运行容器的动机。

使用.dockerignore文件

在大多数情况下,最好将每个Dockerfile放在一个空目录中。然后,仅添加构建Dockerfile所需的文件。为了增加构建的性能,您可以通过向该.dockerignore目录添加文件来排除文件和目录。此文件支持类似于.gitignore文件的排除模式。有关创建的信息,请参阅.dockerignore文件。

避免安装不必要的包

为了减少复杂性,依赖关系,文件大小和构建时间,您应该避免安装额外的或不必要的软件包,因为它们可能“很好”。例如,您不需要在数据库中包含文本编辑器图片。

每个容器应该只有一个问题

将应用程序解耦到多个容器中可以更轻松地水平扩展和重新使用容器。例如,Web应用程序堆栈可能由三个独立的容器组成,每个容器具有自己独特的映像,以解耦的方式管理Web应用程序,数据库和内存中缓存。

你可能听说应该有“每个集装箱一个进程”。虽然这个口头禅意图很好,但并不一定每个容器只能有一个操作系统进程。除了现在可以使用init进程产生容器的事实之外,一些程序可能会自行产生其他进程。例如,芹菜可以产生多个工作进程,或阿帕奇可以创建每个请求的过程。虽然“每个集装箱的一个过程”通常是一个很好的经验法则,但这不是一个艰难和快速的规则。尽可能地判断容器是否干净,模块化。

如果容器依赖于彼此,则可以使用Docker容器网络 来确保这些容器可以通信。

最小化层数

您需要找到可用性(从而长期可维护性)之间的平衡,Dockerfile并最大限度地减少其使用的层数。对您使用的层数进行战略和谨慎。

排序多行参数

只要有可能,通过以字母数字排序多行参数来缓解以后的变化。这将帮助您避免重复的包,并使列表更容易更新。这也使得PR更容易阅读和审查。在反斜杠(\)之前添加一个空格也有帮助。

这是buildpack-deps图像的一个例子:

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

构建缓存

在构建映像的过程中,Docker将按照指定Dockerfile的顺序执行每个指令。随着每条指令的检查,Docker将在其缓存中查找可重用的现有映像,而不是创建一个新的(重复)映像。如果您不想使用缓存,您可以使用--no-cache=true 该docker build命令的选项。

但是,如果您确实让Docker使用其缓存,那么了解何时会找到匹配的映像是非常重要的。Docker将遵循的基本规则如下:

  • 从已经在缓存中的基本图像开始,将下一条指令与从该基本图像导出的所有子图像进行比较,以查看其中一条是否使用完全相同的指令构建。如果没有,则缓存无效。

  • 在大多数情况下,简单地比较Dockerfile与其中一个子图像的指令是足够的。但是,某些说明需要更多的检查和解释。

  • 对于ADDCOPY指令,检查图像中文件的内容,并为每个文件计算校验和。在这些校验和中不考虑文件的最后修改和最后访问的时间。在缓存查找期间,将校验和与现有映像中的校验和进行比较。如果文件(如内容和元数据)中有任何变化,则缓存无效。

  • 除了ADDCOPY命令之外,缓存检查不会查看容器中的文件来确定缓存匹配。例如,当处理RUN apt-get -y update命令时,将不会检查在容器中更新的文件以确定是否存在高速缓存命中。在这种情况下,只需使用命令字符串本身来查找匹配。

一旦缓存无效,所有后续Dockerfile命令将生成新的映像,并且高速缓存将不被使用。

Dockerfile指令

您可以在下面找到建议,以便最好地编写可用于其中的各种说明Dockerfile

FROM

用于FROM指令的Docker文件引用

只要有可能,使用当前的官方存储库作为您的图像的基础。我们建议使用Debian镜像, 因为它是非常严格的控制,并保持最小(目前在150 mb),而仍然是一个完整的分布。

标签

了解对象标签

您可以为图像添加标签,以帮助按项目组织图像,记录许可信息,帮助自动化或其他原因。对于每个标签,添加LABEL以一个或多个键值对开头的行。以下示例显示不同的可接受格式。解释性意见包括在内。

注意:如果您的字符串包含空格,则必须使用引号空格必须转义。如果您的字符串包含内部引号字符("),也可以转义它们。

# Set one or more individual labelsLABEL com.example.version="0.0.1-beta"LABEL vendor="ACME Incorporated"LABEL com.example.release-date="2015-02-12"LABEL com.example.version.is-production=""# Set multiple labels on one lineLABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"# Set multiple labels at once, using line-continuation characters to break long linesLABEL vendor=ACME\ Incorporated \      com.example.is-beta= \      com.example.is-production="" \      com.example.version="0.0.1-beta" \      com.example.release-date="2015-02-12"

有关可接受的标签键和值的指导,请参阅了解对象标签。有关查询标签的信息,请参阅管理对象标签中过滤相关的项目 。

Dock指令的Docker文件引用

和往常一样,为了使您Dockerfile更易于阅读,易于理解和可维护,RUN可以用多个行分隔长条或复合语句,并以反斜杠分隔。

APT-GET的

可能最常用的用例RUN是应用程序apt-get。该 RUN apt-get命令,因为它安装软件包,有几个需要注意的问题。

您应该避免RUN apt-get upgrade或者dist-upgrade,基本图像中的许多“基本”软件包将不会在非特权容器内升级。如果基本图像中包含的包装已过期,则应联系其维护者。如果您知道有特定的软件包,foo需要更新,请使用 apt-get install -y foo自动更新。

总是在同一个 语句中结合 RUN apt-get update使用,例如:apt-get installRUN

    RUN apt-get update && apt-get install -y         package-bar         package-baz         package-foo

apt-get updateRUN语句中单独使用会导致缓存问题和后续apt-get install指令失败。例如,说你有一个Docker文件:

    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

Docker将初始和修改的指令看作是相同的,并重新使用先前步骤的缓存。结果apt-get update执行,因为构建使用缓存的版本。因为apt-get update没有运行,你的构建可能会有一个过时的版本curlnginx包。

使用 RUN apt-get update && apt-get install -y确保您的Dockerfile安装最新的软件包版本,无需进一步的编码或手动干预。这种技术被称为“缓存破解”。您还可以通过指定包版本来实现缓存清除。这被称为版本固定,例如:

    RUN apt-get update && apt-get install -y         package-bar         package-baz         package-foo=1.3.*

版本固定强制构建以检索特定版本,而不管缓存中有什么。这种技术还可以减少由于所需软件包中意外的更改导致的故障。

以下是一个RUN格式正确的指导,显示所有apt-get 建议。

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破坏,并确保新版本的安装。在每行上列出包也可以防止包重复中的错误。

另外,当您通过删除/var/lib/apt/lists 减少映像大小来清理apt缓存时,由于apt缓存未存储在图层中。由于 RUN语句开始apt-get update,包缓存将始终在之前刷新apt-get install

注意:Debian和Ubuntu的官方图像自动运行apt-get clean,因此不需要显式调用。

使用管道

一些RUN命令取决于使用管道字符(|)将一个命令的输出管道传输到另一个命令的能力,如以下示例所示:

RUN wget -O - https://some.site | wc -l > /number

Docker使用/bin/sh -c解释器执行这些命令,该解释器仅评估管道中最后一个操作的退出代码以确定成功。在上述示例中,只要wc -l命令成功,即使wget命令失败,此构建步骤也可以成功并生成新映像。

如果您希望命令由于管道中任何阶段的错误而失败set -o pipefail &&,请先确认出现意外的错误会阻止构建过程无意中成功。例如:

RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

注意:并非所有的shell都支持该-o pipefail选项。在这种情况下(例如dashshell,它是基于Debian的映像的默认shell),请考虑使用exec表单RUN 来明确选择一个支持该pipefail选项的shell 。例如:

RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]

CMD

用于CMD指令的Docker文件引用

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应该很少的方式使用CMD [“param”, “param”]会同ENTRYPOINT,除非你和你预期的用户已经非常熟悉如何ENTRYPOINT 工作的。

EXPOSE

用于EXPOSE指令的Docker文件引用

EXPOSE指令指示容器将侦听连接的端口。因此,您应该为应用程序使用通用的传统端口。例如,包含Apache Web服务器EXPOSE 80的映像将使用,而包含MongoDB的映像将使用EXPOSE 27017等等。

对于外部访问,您的用户可以执行docker run一个标志,指示如何将指定的端口映射到他们选择的端口。对于容器链接,Docker为从容器容器返回源(即MYSQL_PORT_3306_TCP)的路径提供环境变量。

ENV

用于ENV指令的Docker文件引用

为了使新软件更容易运行,您可以使用它ENV来更新PATH容器安装的软件的 环境变量。例如,ENV PATH /usr/local/nginx/bin:$PATH将确保CMD [“nginx”] 只是工作。

ENV指令对于提供特定于要集中化的服务所需的环境变量(如Postgres‘s)也很有用 PGDATA

最后,ENV也可以用于设置常用的版本号,以便版本颠覆更容易维护,如以下示例所示:

ENV PG_MAJOR 9.3ENV PG_VERSION 9.3.4RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

类似于在程序中具有常量变量(与硬编码值相反),这种方法允许您更改单个ENV指令以自动神奇地碰撞容器中的软件版本。

添加或复制

用于ADD指令的
Dockerfile引用用于COPY指令的Dockerfile引用

虽然ADD并且COPY在功能上类似,但一般来说COPY 是优选的。这是因为它比透明度更高ADDCOPY只支持将本地文件基本复制到容器中,同时ADD具有一些功能(如本地仅提取和远程URL支持),这些功能并不明显。因此,最好的用途ADD是本地tar文件自动提取到图像中,如图所示ADD rootfs.tar.xz /

如果您有多个Dockerfile步骤可以从上下文中使用不同的文件,那么COPY它们可以单独使用,而不是一次使用。如果特定需要的文件更改,这将确保每个步骤的构建缓存仅被无效(强制重新运行该步骤)。

例如:

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

导致RUN步骤的缓存无效的数量减少,而不是放在 COPY . /tmp/前面。

由于图像尺寸很重要,ADD因此强烈不鼓励使用远程URL提取包; 你应该使用curlwget替代。这样,您可以删除在解压后不再需要的文件,而不必在图像中添加另一个图层。例如,你应该避免这样做:

ADD http://example.com/big.tar.xz /usr/src/things/RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/thingsRUN 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

对于不需要ADDtar自动提取功能的其他项目(文件,目录),您应该始终使用COPY

ENTRYPOINT

用于ENTRYPOINT指令的Dockerfile引用

最好的用途ENTRYPOINT是设置图像的主要命令,允许该图像像该命令一样运行(然后CMD用作默认标志)。

我们从一个命令行工具图像的例子开始s3cmd

ENTRYPOINT ["s3cmd"]CMD ["--help"]

现在可以像这样运行映像来显示命令的帮助:

$ docker run s3cmd

或使用正确的参数执行命令:

$ docker run s3cmd ls s3://mybucket

这是有用的,因为图像名称可以加倍作为二进制文件的参考,如上面的命令所示。

ENTRYPOINT指令也可以与辅助脚本组合使用,允许其以类似于上述命令的方式运行,即使启动该工具可能需要多于一个步骤。

例如,Postgres Official Image 使用以下脚本作为其ENTRYPOINT

#!/bin/bashset -eif [ "$1" = ‘postgres‘ ]; then    chown -R postgres "$PGDATA"
    if [ -z "$(ls -A "$PGDATA")" ]; then        gosu postgres initdb    fi    exec gosu postgres "$@"fiexec "$@"

:此脚本使用的execbash命令 ,使最终运行的应用程序成为容器的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指令的Docker文件引用

VOLUME指令应用于公开您的docker容器创建的任何数据库存储区域,配置存储或文件/文件夹。强烈建议您使用图像VOLUME的任何可变和/或用户可维修的部分。

用户

Docker文件引用USER指令

如果一个服务可以没有权限运行,可以使用USER来更改非root用户。通过创建在用户和组开始Dockerfile喜欢的东西RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres

注意:图像中的用户和组获得非确定性的UID / GID,因为“下一个”UID / GID被分配,而不管图像重建。所以,如果是至关重要的,你应该分配一个显式的UID / GID。

注意:由于 Go存档/ tar包处理稀疏文件中的一个未解决的错误,尝试在Docker容器中创建一个足够大的UID的用户可能会导致磁盘耗尽,因为/var/log/faillog容器层中填充有NUL(\ 0 )字符 将--no-log-init标志传递给useradd可以解决这个问题。Debian / Ubuntu adduser包装器不支持该--no-log-init标志,应该避免。

您应避免安装或使用,sudo因为它具有不可预测的TTY和信号转发行为,可能会导致比解决问题更多的问题。如果您绝对需要类似的功能sudo(例如,以root用户身份初始化守护程序,但以非root身份运行),则可以使用 “gosu”。

最后,为了减少层次和复杂性,请避免频繁地进行USER切换。

WORKDIR

Docker文件参考WORKDIR指令

为了清楚和可靠,您应该始终为您的绝对路径 WORKDIR。此外,您应该使用,WORKDIR而不是增加的指令,如RUN cd … && do-something难以阅读,排除故障和维护。

ONBUILD

用于ONBUILD指令的Dockerfile引用

一个ONBUILD命令在当前Dockerfile构建完成后执行。 ONBUILD在导出FROM当前图像的任何子图像中执行。将该ONBUILD命令视为父母Dockerfile给予孩子的指示Dockerfile

Docker构建ONBUILD在子节点的任何命令之前执行命令 Dockerfile

ONBUILD对于将要构建FROM给定图像的图像很有用。例如,您将使用ONBUILD一个语言堆栈映像构建在该语言中编写的任意用户软件 Dockerfile,您可以在Ruby的ONBUILD变体中看到。

建立的图像ONBUILD应该有一个单独的标签,例如: ruby:1.9-onbuildruby:2.0-onbuild

把时要小心,ADDCOPYONBUILD。如果新版本的上下文缺少添加的资源,“onbuild”映像将会严重失败。如上所述,添加单独的标签将有助于通过允许Dockerfile作者做出选择来缓解这一点

我们的公共号

技术分享

编写Dockerfiles的最佳做法