首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

编写Dockerfiles(引擎)的最佳实践 | Best practices for writing Dockerfiles (Engine)

Docker可以通过从Dockerfile,一个文本文件,包含所有命令,按照顺序,需要生成给定的图像。DockerfileS坚持特定的格式,并使用一组特定的指令。您可以学习Dockerfile引用一页。如果你刚开始写作Dockerfile你应该从那儿开始。

本文档涵盖Docker公司和Docker社区推荐的最佳做法和方法,以创建易于使用、有效的产品。Dockerfile我们强烈建议您遵循这些建议%28事实上,如果您正在创建一个官方形象,您坚持这些实践%29。

注意:有关此处提到的任何Dockerfile命令的更详细说明,请访问Dockerfile引用一页。

一般准则和建议

容器应该是短暂的

由图像生成的容器Dockerfile定义应该尽可能的短暂。所谓“短暂”,我们的意思是,它可以被停止和摧毁,一个新的建立和安置的绝对最小的设置和配置。您可能想看看过程12要素应用程序方法中的一节,以了解以这样一种无状态方式运行容器的动机。

用一个。dockerignore文件

在大多数情况下,最好将每个Dockerfile放在一个空目录中。然后,只向该目录添加构建Dockerfile所需的文件。若要提高生成的性能,可以通过添加.dockerignore文件也放在那个目录下。此文件支持类似于.gitignore档案。有关创建一个的信息,请参见.dockerignore文件...

避免安装不必要的软件包

为了减少复杂性、依赖性、文件大小和构建时间,您应该避免仅仅因为“拥有”额外的或不必要的包而安装它们。例如,不需要在数据库映像中包含文本编辑器。

每个容器应该只关心一个问题。

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

您可能听说过“每个容器应该有一个过程”。虽然这个咒语有良好的意图,但不一定每个容器只应该有一个操作系统进程。除了容器现在可以由init进程生成,一些程序可能会自动产生额外的进程。例如,芹菜可以生成多个工作进程,或阿帕奇可能会为每个请求创建一个进程。虽然“每个容器一个进程”通常是一个好的经验法则,但它并不是一个硬和快速的规则。用你最好的判断来保持容器尽可能的干净和模块化。

如果容器相互依赖,则可以使用码头集装箱网络以确保这些容器能够通信。

尽量减少层数

您需要找到可读性(以及长期可维护性)Dockerfile与最小化其使用的层数之间的平衡。对您使用的图层数量保持战略性和谨慎。

排序多行参数

只要有可能,可以通过对多行参数进行字母数字排序来简化以后的更改。这将帮助您避免包的重复,并使列表更容易更新。这也使PRs更容易阅读和审查。在反斜杠%28之前添加空格\%29也有帮助。

下面是一个来自buildpack-deps图像*

代码语言:javascript
复制
RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion

构建缓存

在构建图像码头的过程中,您将逐步了解Dockerfile按照指定的顺序执行每个。在检查每条指令时,Docker将在其缓存中寻找一个可以重用的现有映像,而不是创建一个新的%28重复%29映像。如果您根本不想使用缓存,则可以使用--no-cache=true选项的docker build命令。

但是,如果您确实让Docker使用它的缓存,那么非常重要的是要了解它什么时候会,并且不会找到匹配的映像。码头工人将遵循的基本规则概述如下:

  • 从缓存中已经存在的父映像开始,将下一条指令与从该基本映像派生的所有子映像进行比较,以查看其中一个是使用完全相同的指令生成的。否则,缓存将失效。
  • 在大多数情况下,只需比较Dockerfile其中一个孩子的图像就足够了。然而,某些指示需要更多的检查和解释。
  • ADDCOPY说明,检查图像中文件%28s%29的内容,并计算每个文件的校验和。在这些校验和中不考虑文件%28s%29的最后修改和最后访问次数。在缓存查找过程中,将校验和与现有图像中的校验和进行比较。如果文件%28s%29中有任何更改,如内容和元数据,则缓存无效。
  • 除了ADDCOPY命令时,缓存检查将不会查看容器中的文件以确定缓存匹配。例如,当处理RUN apt-get -y update命令不会检查容器中更新的文件以确定是否存在缓存命中。在这种情况下,仅使用命令字符串本身来查找匹配项。

一旦缓存失效,所有后续Dockerfile命令将生成新图像,缓存将不被使用。

Dockerfile指令

下面,您将找到关于编写各种可用说明的最佳方法的建议,以便在Dockerfile...

FROM指令的Dockerfile引用

只要有可能,使用当前的官方存储库作为你形象的基础。我们推荐Debian图像因为它是非常严格控制和保持最小的%28目前低于150 MB%29,同时仍然是一个完整的发行版。

标签

理解对象标签

您可以向图像中添加标签,以帮助按项目组织图像、记录许可信息、帮助自动化或其他原因。对于每个标签,添加一行以LABEL和一个或多个键值对。下面的示例显示了不同的可接受格式。解释性评论包括内联。

*如果字符串包含空格,则必须引用它这些空间必须逃掉。如果字符串包含内部引号字符%28"%29,也要逃离他们。

代码语言:javascript
复制
# Set one or more individual labels
LABEL 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 line
LABEL 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 lines
LABEL 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"

见理解对象标签有关可接受的标签键和值的指南。有关查询标签的信息,请参考与管理对象的标签...

运行指令的Dockerfile引用

一如既往,让你Dockerfile更易读,更易理解,更易维护,分裂更长或更复杂RUN语句在用反斜杠分隔的多行上。

贴切

可能是最常见的用例RUNapt-get...RUN apt-get命令,因为它安装包,所以有几个问题需要注意。

你应该避免RUN apt-get upgradedist-upgrade,因为来自父映像的许多“基本”包不会在非特权容器中升级。如果父映像中包含的包过期,则应与其维护人员联系.。如果你知道有一个特别的包裹,foo,需要更新,使用apt-get install -y foo自动更新。

总是结合RUN apt-get update带着apt-get installRUN声明,例如:

代码语言:javascript
复制
    RUN apt-get update && apt-get install -y \
        package-bar \
        package-baz \
        package-foo

使用apt-get update独处RUN语句导致缓存问题和后续事件。apt-get install指令失败。例如,假设您有一个Dockerfile:

代码语言:javascript
复制
    FROM ubuntu:14.04
    RUN apt-get update
    RUN apt-get install -y curl

生成图像后,所有层都在Docker缓存中。假设您稍后修改apt-get install通过添加额外的包:

代码语言:javascript
复制
    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安装最新的包版本,不再进行编码或手动干预。这种技术被称为“缓存破坏”。您还可以通过指定包版本来实现高速缓存破坏。例如,这称为版本钉扎:

代码语言:javascript
复制
    RUN apt-get update && apt-get install -y \
        package-bar \
        package-baz \
        package-foo=1.3.*

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

下面是一个结构良好的RUN说明所有apt-get建议。

代码语言:javascript
复制
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减少图像大小,因为APT缓存没有存储在一个层中。因为RUN语句以apt-get update之前总是刷新包缓存。apt-get install...

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

使用管道

一些RUN命令依赖于使用管道字符%28将一个命令的输出管道输送到另一个命令的能力|%29,如下例所示:

代码语言:javascript
复制
RUN wget -O - https://some.site | wc -l > /number

Docker使用/bin/sh -c解释器,它只计算管道中最后一个操作的退出代码以确定成功。在上面的示例中,此构建步骤成功并生成一个新映像,只要wc -l命令成功,即使wget命令失败。

如果您希望命令由于管道中任何阶段的错误而失败,请预先准备set -o pipefail &&若要确保意外错误阻止生成意外成功,请执行以下操作。例如:

代码语言:javascript
复制
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

*并非所有shell都支持-o pipefail选择。在这种情况下,%28,例如dashshell是基于debian的图像%29上的默认shell,请考虑使用主管形式RUN若要显式选择确实支持pipefail选择。例如:

代码语言:javascript
复制
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]

CMD

CMD指令的Dockerfile引用

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,你会掉进一个可用的壳里,准备好了。CMD应很少以下列方式使用:CMD [“param”, “param”]ENTRYPOINT,除非您和您的预期用户已经非常熟悉ENTRYPOINT起作用了。

暴露

公开指令的Dockerfile引用

EXPOSE指示容器将侦听连接的端口。因此,应用程序应该使用通用的传统端口。例如,包含apache web服务器的映像将使用EXPOSE 80,而包含MongoDB的图像将使用EXPOSE 27017诸若此类

对于外部访问,用户可以执行docker run带有指示如何将指定端口映射到他们选择的端口的标志。对于容器链接,Docker为从收件人容器返回到源%28 ie的路径提供环境变量,MYSQL_PORT_3306_TCP29%。

环境变化

ENV指令的Dockerfile引用

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

ENV指令对于提供特定于您希望容器化的服务所需的环境变量也很有用,例如Postgres的PGDATA...

最后,ENV还可以用来设置常用的版本号,以便更容易维护版本凸起,如下面的示例所示:

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

类似于程序%28中有常量变量,而不是硬编码值%29,这种方法允许您更改单个ENV指令,自动-神奇地碰撞版本的软件在您的容器。

添加或复制

添加指令的Dockerfile引用

复制指令的Dockerfile引用

尽管ADDCOPY在功能上是相似的,一般来说,COPY是首选。那是因为它比ADD...COPY只支持将本地文件基本复制到容器中,而ADD有一些特性%28,如本地只提取焦油和远程URL支持%29,这不是立即明显。因此,对ADD是本地tar文件自动提取到图像中,如ADD rootfs.tar.xz /...

如果你有多重Dockerfile使用与上下文不同的文件的步骤,COPY他们是单独的,而不是一次性的。这将确保每个步骤的生成缓存仅无效%28,如果特殊需要的文件更改,强制步骤重新运行%29。

例如:

代码语言:javascript
复制
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

类的缓存失效减少。RUN步骤,如果你把COPY . /tmp/在它之前。

因为图像大小很重要,所以使用ADD强烈建议从远程URL获取包;您应该使用curlwget相反。这样,您就可以在解压缩后删除不再需要的文件,并且不必在图像中添加另一层。例如,您应该避免这样做:

代码语言:javascript
复制
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

相反,做一些如下的事情:

代码语言:javascript
复制
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

入口点

入口点指令的Dockerfile引用

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

让我们从命令行工具的图像示例开始s3cmd*

代码语言:javascript
复制
ENTRYPOINT ["s3cmd"]
CMD ["--help"]

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

代码语言:javascript
复制
$ docker run s3cmd

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

代码语言:javascript
复制
$ docker run s3cmd ls s3://mybucket

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

ENTRYPOINT指令也可以与辅助脚本结合使用,允许它以类似于上面命令的方式工作,即使启动工具可能需要多个步骤。

例如,邮政总局官方形象使用以下脚本作为其ENTRYPOINT*

代码语言:javascript
复制
#!/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 "$@"

*此脚本使用exec巴什命令因此,最终运行的应用程序将成为容器的PID 1。这允许应用程序接收发送到容器的任何Unix信号。见ENTRYPOINT了解更多细节。

将助手脚本复制到容器中,并通过ENTRYPOINT在集装箱启动时:

代码语言:javascript
复制
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]

此脚本允许用户以多种方式与Postgres交互。

它只需启动Postgres:

代码语言:javascript
复制
$ docker run postgres

或者,它可以用于运行Postgres并将参数传递给服务器:

代码语言:javascript
复制
$ docker run postgres postgres --help

最后,它还可以用来启动一个完全不同的工具,比如Bash:

代码语言:javascript
复制
$ docker run --rm -it postgres bash

体积

卷指令的Dockerfile引用

VOLUME指令应用于公开由停靠器容器创建的任何数据库存储区、配置存储区或文件/文件夹。强烈鼓励您使用VOLUME对于图像中的任何可变和/或用户可用部分。

用户

用户指令的Dockerfile引用

如果服务可以在没有特权的情况下运行,请使用USER若要更改为非根用户,请执行以下操作。首先,在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%28\0%29个字符。通过--no-log-init要用户添加的标志可以解决此问题。Debian/Ubuntuadduser包装器不支持--no-log-init标志,应避免。

您应该避免安装或使用sudo由于它具有不可预知的TTY和信号转发行为,因此可能会导致比它解决的更多的问题。如果您绝对需要类似于sudo%28E.。如果将守护进程初始化为root,但以非root%29的形式运行它,则可以使用“天哪”...

最后,为了减少层数和复杂度,避免切换。USER频繁地来回走动。

WORKDIR

WORKDIR指令的Dockerfile引用

为了清晰可靠,您应该始终使用绝对路径WORKDIR同时,你也应该用WORKDIR而不是像RUN cd … && do-something,它们很难阅读、故障排除和维护。

奥布尔德

ONBUILD指令的Dockerfile引用

ONBUILD命令之后执行Dockerfile构建完成。ONBUILD在派生的任何子映像中执行。FROM当前图像。想想ONBUILD命令作为父级指令。Dockerfile给孩子Dockerfile...

执行Docker生成ONBUILD在子命令之前的命令Dockerfile...

ONBUILD对于将要构建的图像是有用的。FROMONBUILD中生成用该语言编写的任意用户软件的语言堆栈映像。Dockerfile,如你所见鲁比氏ONBUILD变体...

图像ONBUILD应该得到一个单独的标记,例如:ruby:1.9-onbuildruby:2.0-onbuild...

放的时候要小心ADDCOPYONBUILD如果新构建的上下文缺少要添加的资源,“onbuild”映像将灾难性地失败。如上面所建议的那样,添加一个单独的标记将有助于缓解这种情况,因为它允许Dockerfile作者做出选择。

官方存储库示例

这些官方存储库堪称典范Dockerfiles:

追加资源:

  • Dockerfile引用
  • 更多关于基本图像的信息

扫码关注腾讯云开发者

领取腾讯云代金券