Docker从入门到精通——编写 Dockerfile 的最佳实践
编写 Dockerfile 的最佳实践
本文档涵盖了构建高效映像的推荐最佳实践和方法。
Dockerfile
Docker 通过从一个包含构建给定镜像所需的所有命令的文本文件中读取指令来自动构建镜像。Dockerfile
遵循特定格式和指令集,您可以在 Dockerfile 参考 中找到。
一个 Docker 镜像由只读层组成,每个层代表一个 Dockerfile 指令。这些层是堆叠的,每一层都是前一层变化的增量。考虑一下Dockerfile
:
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py
每条指令创建一个层:
-
FROM
从ubuntu:18.04
Docker 映像创建一个层。COPY
从 Docker 客户端的当前目录添加文件。RUN
使用make
.CMD
指定要在容器中运行的命令。
当你运行一个镜像并生成一个容器时,你会在底层之上添加一个新的可写层(“容器层”)。对正在运行的容器所做的所有更改,例如写入新文件、修改现有文件和删除文件,都会写入此可写容器层。
有关映像层(以及 Docker 如何构建和存储映像)的更多信息,请参阅 关于存储驱动程序。
一、一般指南和建议
1、创建临时容器
您定义的 image Dockerfile
应该生成尽可能短暂的容器。“临时”是指容器可以停止和销毁,然后用绝对最小的设置和配置重建和替换。
请参阅 Twelve-factor 应用程序方法下的流程, 以了解以这种无状态方式运行容器的动机。
2、了解上下文
当您发出 docker build
命令时,当前工作目录称为构建上下文。默认情况下,假定 Dockerfile 位于此处,但您可以使用文件标志 (-f
) 指定不同的位置。无论 Dockerfile
实际位于何处,当前目录中文件和目录的所有递归内容都会作为构建上下文发送到 Docker 守护进程。
构建上下文示例
为构建上下文创建一个目录并
cd
进入其中。将“hello”写入一个名为的文本文件hello
并创建一个在其上运行的 Dockerfilecat
。从构建上下文 (.
) 中构建映像:mkdir myproject && cd myproject echo "hello" > hello echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile docker build -t helloapp:v1 .移动
Dockerfile
并hello
进入单独的目录并构建图像的第二个版本(不依赖于上次构建的缓存)。用于-f
指向 Dockerfile 并指定构建上下文的目录:mkdir -p dockerfiles context mv Dockerfile dockerfiles && mv hello context docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context
无意中包含构建映像不需要的文件会导致更大的构建上下文和更大的映像大小。这会增加构建镜像的时间、拉取和推送镜像的时间以及容器运行时的大小。要查看您的构建上下文有多大,请在构建时查找如下消息Dockerfile
:
Sending build context to Docker daemon 187.8MB
3、管道 Dockerfile stdin
Docker 能够通过管道,通过 Dockerfile
本地或远程构建上下文来构建映像。在不将 Dockerfile 写入磁盘的情况下执行一次性构建,或者在生成 Dockerfile 且不应在之后持续存在的情况下,管道直通可能很有用。stdin
Dockerfile
stdin
Dockerfile
为方便起见,本节中的示例使用此处的文档,但可以使用 任何提供
Dockerfile
on的方法。stdin
例如,以下命令是等效的:
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<<EOF FROM busybox RUN echo "hello world" EOF
您可以用您喜欢的方法或最适合您的用例的方法替换这些示例。
1)使用标准输入中的 Dockerfile 构建映像,而不发送构建上下文
使用此语法使用 Dockerfile
from 构建图像 stdin
,而不发送其他文件作为构建上下文。连字符 (-
) 占据的位置,并指示 Docker 从目录而不是目录PATH
读取构建上下文(仅包含 a Dockerfile
) :stdin
docker build [OPTIONS] -
以下示例使用通过 的 构建Dockerfile
图像 stdin
。没有文件作为构建上下文发送到守护进程。
docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF
Dockerfile
在不需要将文件复制到映像中并提高构建速度的情况下,省略构建上下文可能很有用,因为没有文件发送到守护程序。
如果您想通过从构建上下文中排除某些文件来提高构建速度,请参阅使用 .dockerignore 排除。
注意:
尝试构建使用此语法COPY
或ADD
将失败的 Dockerfile。以下示例说明了这一点:
# create a directory to work in mkdir example cd example # create an example file touch somefile.txt docker build -t myimage:latest -<<EOF FROM busybox COPY somefile.txt ./ RUN cat /somefile.txt EOF # observe that the build fails ... Step 2/3 : COPY somefile.txt ./ COPY failed: stat /var/lib/docker/tmp/docker-builder249218248/somefile.txt: no such file or directory
2)使用来自 stdin 的 Dockerfile 从本地构建上下文构建
使用此语法使用本地文件系统上的文件构建映像,但使用 Dockerfile
from stdin
. 该语法使用-f
(or --file
) 选项指定 Dockerfile
要使用的,使用连字符 (-
) 作为文件名来指示 Docker 读取 Dockerfile
from stdin
:
docker build [OPTIONS] -f- PATH
下面的示例使用当前目录 ( .
) 作为构建上下文,并使用通过Dockerfile
here document 传递的 a 构建图像。stdin
# create a directory to work in
mkdir example
cd example
# create an example file
touch somefile.txt
# build an image using the current directory as context, and a Dockerfile passed through stdin
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt ./
RUN cat /somefile.txt
EOF
3)使用来自标准输入的 Dockerfile 从远程构建上下文构建
使用此语法使用来自远程存储库的文件构建映像git
,使用Dockerfile
from stdin
。该语法使用-f
(or --file
) 选项指定Dockerfile
要使用的,使用连字符 ( -
) 作为文件名来指示 Docker 读取Dockerfile
from stdin
:
docker build [OPTIONS] -f- PATH
Dockerfile
在您想要从不包含 a 的存储库构建映像的情况下,或者如果您想要使用自定义构建 Dockerfile
而不维护您自己的存储库分支,此语法可能很有用。
下面的示例使用Dockerfile
from构建图像,并从GitHub 上的“hello-world”Git 存储库stdin
添加hello.c
文件。
docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF FROM busybox COPY hello.c ./ EOF
Under the hood
当使用远程 Git 存储库作为构建上下文构建映像时,Docker
git clone
在本地机器上执行一个存储库,并将这些文件作为构建上下文发送到守护进程。此功能需要git
安装在您运行docker build
命令的主机上。
3、使用 .dockerignore 排除
要排除与构建无关的文件(不重新构建源存储库),请使用.dockerignore
文件。此文件支持类似于.gitignore
文件的排除模式。有关创建的信息,请参阅 .dockerignore 文件。
4、使用多阶段构建
多阶段构建允许您大幅减小最终 image 的大小,而无需努力减少中间层和文件的数量。
由于镜像是在构建过程的最后阶段构建的,因此您可以通过 利用构建缓存 来最小化镜像层。
例如,如果您的构建包含多个层,您可以将它们从不经常更改(以确保构建缓存可重用)排序到更频繁更改:
-
-
安装构建应用程序所需的工具
-
安装或更新库依赖项
-
生成您的应用程序
-
Go 应用程序的 Dockerfile 可能如下所示:
# syntax=docker/dockerfile:1 FROM golang:1.16-alpine AS build # Install tools required for project # Run `docker build --no-cache .` to update dependencies RUN apk add --no-cache git RUN go get github.com/golang/dep/cmd/dep # List project dependencies with Gopkg.toml and Gopkg.lock # These layers are only re-built when Gopkg files are updated COPY Gopkg.lock Gopkg.toml /go/src/project/ WORKDIR /go/src/project/ # Install library dependencies RUN dep ensure -vendor-only # Copy the entire project and build it # This layer is rebuilt when a file changes in the project directory COPY . /go/src/project/ RUN go build -o /bin/project # This results in a single layer image FROM scratch COPY --from=build /bin/project /bin/project ENTRYPOINT ["/bin/project"] CMD ["--help"]
5、不要安装不必要的包
为了减少复杂性、依赖关系、文件大小和构建时间,请避免安装额外或不必要的软件包,因为它们可能“很高兴拥有”。例如,您不需要在数据库图像中包含文本编辑器。
6、解耦应用程序
每个容器应该只有一个关注点。将应用程序解耦到多个容器中,可以更轻松地水平扩展和重用容器。例如,一个 Web 应用程序堆栈可能由三个独立的容器组成,每个容器都有自己独特的图像,以分离的方式管理 Web 应用程序、数据库和内存缓存。
将每个容器限制为一个进程是一个很好的经验法则,但这不是一个硬性规定。例如,不仅可以 使用 init 进程生成容器,某些程序可能会自行生成其他进程。例如,Celery可以产生多个工作进程,而Apache可以为每个请求创建一个进程。
使用您的最佳判断来保持容器尽可能清洁和模块化。如果容器相互依赖,您可以使用Docker 容器网络 来确保这些容器可以通信。
7、尽量减少层数
在旧版本的 Docker 中,尽量减少镜像中的层数以确保它们的性能非常重要。添加了以下功能以减少此限制:
-
只有说明
RUN
,COPY
,ADD
创建图层。其他指令创建临时中间图像,并且不增加构建的大小。 -
在可能的情况下,使用 多阶段构建,并且只将您需要的工件复制到最终 image 中。这允许您在中间构建阶段包含工具和调试信息,而不会增加最终映像的大小。
8、对多行参数进行排序
只要有可能,通过按字母数字排序多行参数来简化以后的更改。这有助于避免重复包并使列表更容易更新。这也使 PR 更容易阅读和审查。在反斜杠 (\) 前添加一个空格\
也有帮助。
Here’s an example from the buildpack-deps
image:
RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion \ && rm -rf /var/lib/apt/lists/*
9、利用构建缓存
构建映像时,Docker 会逐步执行 Dockerfile 中的指令,并按照指定的顺序执行每个指令。在检查每条指令时,Docker 在其缓存中查找可以重用的现有映像,而不是创建新的(重复的)映像。
如果您根本不想使用缓存,可以使用命令 --no-cache=true
上的选项。但是,如果您确实让 Docker 使用它的缓存,那么了解它何时可以找到匹配的图像以及何时不能找到匹配的图像非常重要。 Docker 遵循的基本规则概述如下:
-
从已经在缓存中的父图像开始,将下一条指令与从该基础图像派生的所有子图像进行比较,以查看其中一个是否是使用完全相同的指令构建的。如果不是,则缓存无效。
-
在大多数情况下,只需将 中的指令
Dockerfile
与其中一个子图像进行比较就足够了。但是,某些说明需要更多的检查和解释。 -
对于
ADD
和COPY
指令,检查图像中文件的内容并为每个文件计算校验和。这些校验和中不考虑文件的最后修改时间和最后访问时间。在缓存查找期间,将校验和与现有图像中的校验和进行比较。如果文件中有任何更改,例如内容和元数据,则缓存无效。 -
除了
ADD
andCOPY
命令,缓存检查不会查看容器中的文件来确定缓存匹配。例如,在处理RUN apt-get -y update
命令时,不会检查容器中更新的文件以确定是否存在缓存命中。在这种情况下,只有命令字符串本身用于查找匹配项。
一旦缓存失效,所有后续 Dockerfile
命令都会生成新的图像并且缓存不会被使用。