2021 年 Dockerfile 的 21 个最佳实践
“这是一份精心挑选的 2020 年度 Dockerfile 最佳实践长名单”。
自 2013 年 3 月 20 日 Docker 诞生以来,它就席卷了全球,彻底改变了应用程序在多平台上打包和交付的方式。尽管容器技术在 Docker 时代之前就已经存在,但 Docker 脱颖而出并享誉全球的关键在于,它能够轻松解决容器技术所涉及的大部分底层复杂性,使其能够在所有主流操作系统和平台上轻松使用,并且始终拥有开源社区的强大支持。
就近年来发生的技术变革而言,Docker 一直是我个人最喜欢的。从裸机到虚拟机的转变,在大多数方面都是如此。同样,Docker 出于各种充分的理由用容器取代虚拟机。简而言之,Docker 包含一些基本组件,首先是一个简单的 Dockerfile,这是一个纯文本文件,我们在其中编写(代码),其中包含一组简单的步骤或指令,这些步骤或指令以简单的术语定义了需要做什么以及您希望应用程序包含什么以及它将如何运行。编写 Docker 文件后,我们会从中构建一个映像(将其视为可执行文件),该映像在编译一些代码(Dockerfile)后创建。构建映像后,我们需要启动该映像。启动映像会创建一个容器,它是映像的运行实例,这类似于启动可执行文件,而可执行文件是可执行文件的运行实例。
选择最少的基础镜像
我们在 Docker 中构建的每个自定义镜像都需要构建在现有的基础镜像之上,因此我们需要精心挑选最精简、最紧凑的镜像。市面上有各种轻量级镜像可供选择,包括 Alpine、Busybox 以及其他特定发行版的镜像,例如 Debian、Ubuntu、CentOS,它们都有-slim *或 * -minimal版本可供选择。
在选择基础镜像时,务必选择轻量级且提供所需支持、工具和二进制文件的完美组合。有时,您可能还会遇到这样的问题:选择轻量级镜像需要权衡与应用程序的兼容性,并且缺少运行应用程序所需的依赖项或库。
FROM alpine
WORKDIR /app
COPY package.json /app
RUN npm install
CMD [“node”,“index.js”]
删除缓存包
我们的应用程序要在容器内运行,通常需要运行时环境、依赖项和二进制文件。当我们尝试使用软件包管理器(例如 apt、apk、yum)安装软件包时,通常会先从远程源码库下载软件包到本地,然后再安装。安装软件包后,通常会存储已下载的缓存软件包文件,并占用额外的不必要空间。最佳建议是在安装软件包后删除这些缓存/软件包文件,这可以进一步优化 Docker 镜像。
根据所使用的图像类型,有不同的包管理器,它们具有存储包管理器缓存的默认位置,其中一些列在下面。
**图像/发行版:**Debian / Ubuntu
**软件包管理器:**apt
*缓存位置:* /var/cache/apt/archives
**图片/发行版:**Alpine
**软件包管理器:**apk
*缓存位置: * /var/cache/apk
**图像/发行版:**centos
**软件包管理器:**yum
*缓存位置:* /var/cache/
在下面的示例中,我们将安装 Nginx Web 服务器来提供静态 HTML 网页。在安装 Nginx 软件包的同时,我们还将删除存储在缓存目录特定路径中的缓存包。在本例中,由于我们使用 alpine,因此我们指定了包含包缓存的目录。
FROM alpine
RUN apk add nginx && **rm -rf /var/cache/apt/***
COPY index.html /var/www/html/
EXPOSE 80
CMD [“nginx”,“-g”,“daemon off;”]
对于 alpine 来说,上述解决方案的替代方法是使用–no-cache,这可确保不会为要安装的包存储任何缓存,从而无需手动删除包。
FROM alpine
RUN apk add –no-cache nginx
COPY index.html /var/www/html/
EXPOSE 80
CMD [“nginx”,“-g”,“daemon off;”]
避免多层
哇!这个汉堡真是养眼,上面加了好几层肉饼和奶酪,真是美味又厚重。Docker 镜像和这个汉堡很像,在构建镜像时,Dockerfile 文件中添加的每一层都会让它变得更重。始终建议确保层数尽可能少。
下面是一个 Dockerfile,其中包含安装 Nginx 以及其他所需实用程序的说明。在 Dockerfile 中,每行新指令都构成一个单独的层。
FROM alpine
RUN apk update
RUN apk add curl
RUN apk add nodejs
RUN apk add nginx-1.16.1-r6
RUN apk add nginx-mod-http-geoip2-1.16.1-r6
COPY index.html /var/www/html/
EXPOSE 80
CMD [“nginx”,“-g”,“daemon off;”]
上述 Dockerfile 可以通过链接进行优化,并在需要时有效使用&&和 * *来减少为 Dockerfile 创建的层数。
FROM alpine
RUN apk update && apk add curl nginx nginx-mod-http-geoip2-1.16.1-r6 \
rm -rf /var/cache/apt/*
COPY index.html /var/www/html/
EXPOSE 80
CMD [“nginx”,“-g”,“daemon off;”]
借助链接,我们将大多数层合并在一起,避免创建多个层,这有助于优化 Dockerfile,使汉堡看起来更加美味。
不要忽略 .DOCKERIGNORE
.dockerignore *顾名思义,它是一种快速简便的方法,可以忽略不应分离于 Docker 镜像的文件。类似于 * .gitignore **文件,它会忽略在版本控制下跟踪的文件。在进一步讨论之前,让我们先了解一下 **build-context。在构建 Dockerfile 时,当前工作目录中的所有文件/文件夹都将被复制并用作构建上下文。这里的权衡是,如果我们构建 Dockerfile 的当前工作目录包含 GB 的数据,在这种情况下,它通常会增加不必要的构建时间,那么这就是一个问题,这是否意味着我们必须在构建 Dockerfile 时将 GB 的数据移动到单独的目录中,不行!!,但是我们该如何解决这个问题呢?
.dockerignore可以帮您解决这个问题,它可以用于几个用例,其中一些我已经在下面提到过:
-
忽略不需要成为将要构建的图像一部分的文件和目录。
-
避免意外复制敏感数据。
让我们尝试通过 Dockerfile 的示例来更好地理解这一点,在该示例中,我们将对 nodejs 应用程序进行 dockerize,并使用 .dockerignore忽略在构建此映像时不需要复制的文件/目录。
-
忽略不需要的文件和目录
来自节点:10
工作目录 /nodeapp
复制 package.json ./
运行 npm install
复制 。 。
曝光 8888
命令 [ “节点”, “index.js” ]
在这种情况下,我们选择node:10作为基础镜像,将应用程序设置为我们的 docker 镜像的工作目录,公开端口8888以供外部访问,之后我们复制 package.json,然后使用 npm 安装 package.json 中提到的所有依赖项,这将创建一个 node_modules 目录,其中将包含在其下安装的所有最新依赖项,之后是关键部分,我们将所有内容从当前工作目录复制到 docker 镜像中。通常,在从当前工作目录复制所有内容时,某些文件/目录是不需要的,在本例中是node_modules,因为我们已经使用 npm install 安装了最新的二进制文件。因此,考虑到这一点,我们可以在 .dockerignore 中添加 node_modules,以避免在构建镜像时复制它。
2.避免复制敏感细节。
开发人员不可否认,他们会将 .env 文件、ssh 密钥、证书以及包含敏感信息的文件存储在本地开发环境中(这种情况我经常遇到),虽然这让访问变得容易,但却将整个系统暴露在全新级别的漏洞和安全漏洞之下。然而,无论如何都应该避免这些做法,并且为了防止在包含 Docker 的开发环境中造成进一步的损害,最好的办法就是避免这些文件在我们正在构建的 Docker 镜像中被复制。这可以借助.dockerignore轻松完成,只需指定需要避免被意外复制的文件即可。
理想情况下,我们的.dockerignore文件应该是这样的
node_modules
.env
secrets/
*pem
*.md
在本例中,我们添加了node_modules(如上所述,它不需要)和.env 文件,因为它可能包含敏感信息或特定于本地开发环境的变量,这些变量不应与其他环境(例如暂存环境、生产环境)产生冲突。我们还排除了存储在.*pem * 文件中的所有敏感数据以及 secret 文件夹中的文件,以及 Docker 镜像中通常不需要的 markdown/文档文件。
选择修身款式
在选择基础镜像时,应该选择更轻薄、更精简的基础镜像。它们通常被标记为-slim或-minimal。与默认镜像相比,这些镜像更轻量,占用空间也更小。
以下是精简版与默认版的几个示例。
斩草除根
我们使用 docker 构建的每个镜像都默认 root 用户,这在安全方面很不安全,因此我们称之为“去 root”。大多数情况下,我们不需要镜像的用户是 root,因为我们可以指定一个默认用户,该用户拥有应用程序在容器内运行所需的所有最低权限。
下面是一个图像示例,其中我们没有指定用户,这意味着默认用户是**root**,这就是我们打开全新级别的安全漏洞的地方。
FROM node:10
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 8888
CMD [ “node”, “index.js” ]
现在,由于我们知道默认用户意味着 root,为了避免这种情况,我们可以指定 root 之外的默认用户。
FROM node:10
RUN user add -m nodeapp
USER nodeappuser
RUN whoami
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 8888
CMD [ “node”, “index.js” ]
删除不需要的
在尝试将应用程序docker化时,我们的主要目标是确保应用程序能够在docker容器中成功运行。通常情况下,在选择基础镜像后,会发现镜像附带了许多工具、软件包和实用程序。我们要么选择使用-slim/-minimal版本的镜像,要么选择删除可能不需要的工具和实用程序。
明智地标记
标签是镜像的唯一标识方式。在标记镜像时,我们可以使用任何特定的命名约定,但根据功能、提交或其他更有意义的内容来选择镜像标签才是最佳选择。标签是供最终用户选择使用哪个版本的镜像的。
标记的示例包括使用图像的增量版本对其进行标记或使用 git 版本控制哈希,所有这些都可以集成到您的 CI/CD 管道中,以自动完成标记图像的目的
所以不要使用最新标签
我曾经这样做过,在 docker 镜像被标记为 * : latest *时遇到过很多问题,以下是我不再使用最新标签的几个原因。
myimage:latest类似,因为它没有任何标签,或者说默认的myimage(没有标签)
避免在生产环境中使用 Kubernetes 的最新标签,因为这会使调试和查找可能导致问题的版本变得非常困难。因此,通常建议使用特定版本对镜像进行有意义的标记,以描述发生的更改并在必要时进行回滚。这破坏了使用唯一标签来描述不同版本背后的整个理念。
公共或私人注册处
这里的问题是选择公共图像还是私有图像?
对于不太关心系统整体安全性的小型团队来说,公共图像非常棒,基础且易于使用。
私有镜像的存储增加了一层安全保障,确保只有授权人员才能访问这些镜像。Docker Hub 以及其他一些容器镜像仓库工具都提供了选择公共镜像或私有镜像的选项(不过,在 Docker Hub 的默认免费方案中,您只能选择将 1 个镜像设置为私有镜像)。
保持单身
保持单一,其真正含义是保持单个容器内单个应用程序的理念。这也适用于许多现实世界的事物,软件设计的单一职责原则也适用于 Docker 镜像。一个镜像应该只代表一个应用程序,从而避免整体的复杂性。
采用模块化方法对整个应用程序栈进行 Docker 化通常是一个好习惯,这或许有助于解决可能出现的意外问题。例如:如果我们尝试对一个依赖 MYSQL 作为数据库的应用程序进行 Docker 化,我们不应该将应用程序和数据库都放在一个镜像中,而应该将两个实例拆分成单独的 Docker 镜像。
使用 LINTER
Linter 是一款简单的软件,它可以分析特定语言的代码,然后检测错误,并从编写代码的那一刻起就提供最佳实践建议。每种语言都有一些可用的 Linter。对于 Docker,我们确实提供了一些 Linter 供您选择,其中一些如下所述。
我个人最喜欢的是 Docker Linter,它是一个 vscode 扩展,可以在你使用时指示警告或语法错误。
不要储存秘密
就像在现实生活中一样,永远不要泄露你的秘密。Dockerfile 也一样,永远不要存储明文用户名、密码或其他敏感信息,以免被泄露。为了避免机密信息被存储,请使用.dockerignore 文件,以防止意外复制可能包含敏感信息的文件。
避免硬编码
虽然这个原则不仅适用于 Dockerfile,也适用于一般的软件设计,但通常不建议在 Dockerfile 中硬编码值。最好的例子是,与其硬编码可能更新或需要更改的特定软件版本,不如在构建时使用 ARGS 动态传递这些值。
* ARGS * — 是 Dockerfile 中的一个关键字,允许我们在构建时动态地将值传递给 Dockerfile。
为了更好地理解这一点,我们举一个例子。
ARG VERSION
FROM node:$VERSION
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 8888
CMD [ “node”, “index.js” ]
使用动态值来传递和构建图像。
docker build -t testimage –build-arg VERSION=10 .
docker build -t testimage –build-arg VERSION=9 .
通过这种技术,我们可以动态地决定要选择的基础镜像的版本,而不是对其进行硬编码并在运行时传递版本的值
避免使用调试工具
在构建镜像时,我们经常会添加一些调试工具,例如 curl、ping、netstat、telnet 和其他网络实用程序,这会进一步增加镜像的整体大小。最好避免在 Dockerfile 中添加这些调试工具,而只在运行时真正需要时才安装它们。
添加元数据
LABEL是 Dockerfile 中的一个关键字,它添加有关 Dockerfile 的元数据详细信息。
LABEL 允许将基于文本的元数据详细信息添加到 Dockerfile,从而提供更详细的信息。LABEL 可用于在 Dockerfile 中添加维护者姓名和电子邮件地址的详细信息。在下面的示例中,我们添加了维护者以及镜像版本的详细信息。
FROM node:10
**LABEL version=“1.0” maintainer=“Chris Rego <cXXXXXXo@gmail.com>”**
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 8888
CMD [ “node”, “index.js” ]
使用漏洞检查
漏洞检查!
最近特斯拉 Kubernetes 基础设施遭受攻击,让所有人都意识到,从裸机到虚拟机,再到容器的迁移,永远无法修复那些经常被遗留的安全漏洞。其实,在 Docker 化和应用的过程中,可以遵循一些安全方面的最佳实践,例如妥善保管机密信息/凭证、避免使用 root 用户作为容器的默认用户等等。应对容器领域安全漏洞的更好方法是引入工具/技术驱动,对环境中的容器执行可靠的安全检查。目前有几种工具可以添加到你的安全库中。
避免复制所有内容
使用 COPY 总是好的,但选择性复制更明智。对于 Docker,建议尽量避免使用COPY . .命令,因为它会将当前工作目录中的所有内容复制到 Docker 镜像的目录中。建议只选择需要复制的文件,并在.dockerignore中指定文件,以免意外复制(不需要的文件或包含敏感数据的文件) 。
FROM node:10
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 8888
CMD [ “node”, “index.js” ]
例如,下面我们避免复制所有内容,而是仅指定需要的文件/目录,这降低了意外复制不需要的数据的风险,这最终可能导致 Docker 镜像臃肿以及整体构建时间增加。
FROM node:10
WORKDIR /app
COPY package.json ./
RUN npm install
COPY index.js src ./
EXPOSE 8888
CMD [ “node”, “index.js” ]
需要时使用 WORKDIR
WORKDIR是 Dockerfile 中的另一个重要关键字,它有助于完成大部分繁重的工作,并避免在需要时额外创建和导航到特定的工作目录。
WORKDIR可以广泛应用于某些特定用例,例如需要在 Dockerfile 中编写一个额外的步骤来导航到特定目录。在这种情况下,我们只需使用WORKDIR就可以安全地删除这些导航引用,例如 cd 命令。
FROM node:10
RUN mkdir -p /app/mynodejsapp
COPY package.json /app/mynodejsapp
RUN cd /app/mynodejsapp && npm install
COPY . ./app/mynodejsapp
EXPOSE 8888
CMD [ “node”, “index.js” ]
在这种情况下,我们必须使用 **mkdir** 创建一个文件夹,然后对于每个附加引用,我们必须提及该特定文件夹的完整路径,并且还涉及使用 cd 导航到该文件夹,所有存在的附加引用都可以用简单的 WORKDIR 替换
FROM node:10
WORKDIR /app/mynodejsapp
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 8888
CMD [ “node”, “index.js” ]
在这种情况下,如果文件夹不存在,WORKDIR 也会自动创建一个文件夹,无需额外导航到当前工作目录,因为 WORKDIR 已经完成了它应该做的事情。
多阶段构建
多阶段构建技术最适合于 Docker 镜像包含 Dockerfile 中应用程序构建过程的场景。虽然构建包含所有依赖项的应用程序是可以的,但为了进一步优化,我们可以进一步将构建过程和最终部署过程划分为两个阶段。将整个镜像划分为两个阶段有助于确保我们避免在构建应用程序时早期需要的、构建完成后不再需要的不必要的依赖项。
使用多阶段构建是一种很好的做法,因为它鼓励只保留最终 Docker 镜像中需要的东西,而留下所有不需要的构建依赖项和其他文件。
# STAGE ONE: INVOLVES BUILDING THE APPLICATION
FROM node:10 AS build
WORKDIR /myapp
COPY package.json index.js ./
RUN npm install ./
# STAGE TWO: COPYING THE ONLY CONTENTS
# NEEDED TO RUN THE APPLICATION WHILE
# LEAVING BEHIND THE REST
FROM node:10-prod
WORKDIR /myapp
COPY –from=build /app/package.json /app/index.js ./
COPY –from=build /app/node_modules /app/node_modules ./
EXPOSE 8080
CMD [“node”, “/app/index.js”]
在上面的例子中,我们将整个 Dockerfile 分为两个阶段。第一阶段涉及安装所需的依赖项,然后仅将实际需要的文件复制到第二阶段,也就是最终使用的构建版本。这种方法不仅提供了关注点分离,还确保我们可以挑选最终构建版本中真正需要的内容。
最后缓存
现在让我们来讨论一下真正重要的东西,那就是缓存。
打包和构建通常需要花费大量时间,构建 Dockerfile 也是如此,它包含一系列最终构建成 Docker 镜像的步骤。在构建 Docker 镜像时,Docker 会从上到下逐步构建,并检查每个步骤中是否已存在缓存中的层。如果该层已存在,则不会构建新层,而是使用现有层,从而节省大量时间。
事实证明,缓存在更新 Dockerfile 的更改时非常有效,尤其是在 Dockerfile 包含一系列涉及下载特定软件包的指令时。通过网络下载通常会耗费更多时间并消耗额外的网络带宽,而缓存可以显著减少这种情况。尽管 Docker 默认提供缓存,但缓存可能会因检测到的更改而中断,这是预料之中的行为。因此,最终用户有责任确保 Dockerfile 中的指令按特定顺序执行,以避免缓存中断,因为顺序对于缓存至关重要。
Docker 中的缓存遵循连锁反应,因此在开始时如果在 Dockerfile 中检测到更改,则之后提到的指令将不被视为缓存,并且这基本上会破坏缓存。因此,通常建议在 Dockerfile 的开头包含预计不会频繁更改的步骤,这将确保缓存不会中断。Docker 将缓存 Dockerfile 第一次构建的结果,从而使后续构建非常快。只有存储了缓存时,缓存才会起作用,如果我们在下次尝试构建映像时删除该缓存,它将从头开始重建并耗时。Docker 工作非常智能,无需任何额外配置即可提供即时缓存。
Docker 是一个非常灵活的工具,它允许我们在构建镜像时完全忽略缓存,这可以通过使用 –no-cache 来实现。这确保了构建镜像时缓存机制不起作用,但这会增加整体构建时间。
这不是结束
正如我之前承诺的那样,这是一份涵盖 2020 年 Dockerfile 最佳实践的 20 多项清单,随着我不断探索构建优秀 Docker 容器的黑暗未知领域,我将不断添加更多内容。如果你也这样做了
文章来源:https://dev.to/chrisedrego/21-best-practise-in-2021-for-dockerfile-1dji