编写Dockerfiles的最佳做法
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编写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
与其中一个子图像的指令是足够的。但是,某些说明需要更多的检查和解释。对于
ADD
和COPY
指令,检查图像中文件的内容,并为每个文件计算校验和。在这些校验和中不考虑文件的最后修改和最后访问的时间。在缓存查找期间,将校验和与现有映像中的校验和进行比较。如果文件(如内容和元数据)中有任何变化,则缓存无效。除了
ADD
和COPY
命令之外,缓存检查不会查看容器中的文件来确定缓存匹配。例如,当处理RUN apt-get -y update
命令时,将不会检查在容器中更新的文件以确定是否存在高速缓存命中。在这种情况下,只需使用命令字符串本身来查找匹配。
一旦缓存无效,所有后续Dockerfile
命令将生成新的映像,并且高速缓存将不被使用。
Dockerfile指令
您可以在下面找到建议,以便最好地编写可用于其中的各种说明Dockerfile
。
只要有可能,使用当前的官方存储库作为您的图像的基础。我们建议使用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"
有关可接受的标签键和值的指导,请参阅了解对象标签。有关查询标签的信息,请参阅管理对象标签中过滤相关的项目 。
和往常一样,为了使您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 install
RUN
RUN apt-get update && apt-get install -y package-bar package-baz package-foo
apt-get update
在RUN
语句中单独使用会导致缓存问题和后续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
没有运行,你的构建可能会有一个过时的版本curl
和nginx
包。
使用 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
选项。在这种情况下(例如dash
shell,它是基于Debian的映像的默认shell),请考虑使用exec表单RUN
来明确选择一个支持该pipefail
选项的shell 。例如:
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
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
应该很少的方式使用CMD [“param”, “param”]
会同ENTRYPOINT
,除非你和你预期的用户已经非常熟悉如何ENTRYPOINT
工作的。
该EXPOSE
指令指示容器将侦听连接的端口。因此,您应该为应用程序使用通用的传统端口。例如,包含Apache Web服务器EXPOSE 80
的映像将使用,而包含MongoDB的映像将使用EXPOSE 27017
等等。
对于外部访问,您的用户可以执行docker run
一个标志,指示如何将指定的端口映射到他们选择的端口。对于容器链接,Docker为从容器容器返回源(即mysql_PORT_3306_TCP
)的路径提供环境变量。
ENV
为了使新软件更容易运行,您可以使用它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
是优选的。这是因为它比透明度更高ADD
。COPY
只支持将本地文件基本复制到容器中,同时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提取包; 你应该使用curl
或wget
替代。这样,您可以删除在解压后不再需要的文件,而不必在图像中添加另一个图层。例如,你应该避免这样做:
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
对于不需要ADD
tar自动提取功能的其他项目(文件,目录),您应该始终使用COPY
。
最好的用途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 "[email protected]"fiexec "[email protected]"
注:此脚本使用的
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
指令应用于公开您的docker容器创建的任何数据库存储区域,配置存储或文件/文件夹。强烈建议您使用图像VOLUME
的任何可变和/或用户可维修的部分。
用户
如果一个服务可以没有权限运行,可以使用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 / Ubuntuadduser
包装器不支持该--no-log-init
标志,应该避免。
您应避免安装或使用,sudo
因为它具有不可预测的TTY和信号转发行为,可能会导致比解决问题更多的问题。如果您绝对需要类似的功能sudo
(例如,以root用户身份初始化守护程序,但以非root身份运行),则可以使用 “gosu”。
最后,为了减少层次和复杂性,请避免频繁地进行USER
切换。
WORKDIR
为了清楚和可靠,您应该始终为您的绝对路径 WORKDIR
。此外,您应该使用,WORKDIR
而不是增加的指令,如RUN cd … && do-something
难以阅读,排除故障和维护。
ONBUILD
一个ONBUILD
命令在当前Dockerfile
构建完成后执行。 ONBUILD
在导出FROM
当前图像的任何子图像中执行。将该ONBUILD
命令视为父母Dockerfile
给予孩子的指示Dockerfile
。
Docker构建ONBUILD
在子节点的任何命令之前执行命令 Dockerfile
。
ONBUILD
对于将要构建FROM
给定图像的图像很有用。例如,您将使用ONBUILD
一个语言堆栈映像构建在该语言中编写的任意用户软件 Dockerfile
,您可以在Ruby的ONBUILD
变体中看到。
建立的图像ONBUILD
应该有一个单独的标签,例如: ruby:1.9-onbuild
或ruby:2.0-onbuild
。
把时要小心,ADD
或COPY
在ONBUILD
。如果新版本的上下文缺少添加的资源,“onbuild”映像将会严重失败。如上所述,添加单独的标签将有助于通过允许Dockerfile
作者做出选择来缓解这一点。
我们的公共号
以上是关于编写Dockerfiles的最佳做法的主要内容,如果未能解决你的问题,请参考以下文章