如果要减小 docker 镜像大小,则需要使用标准最佳实践来构建 Docker 镜像。
本文讨论了不同的优化技术,您可以快速实现这些技术来制作最小和最小的 docker 镜像。我们还将介绍一些用于 Docker 镜像优化的最佳工具。
Docker 作为容器引擎,可以很容易地获取一段代码并在容器中运行它。它使工程师能够将所有代码依赖项和文件收集到一个位置,该位置可以在任何地方快速轻松地运行。
“随处运行”镜像的整个概念始于一个名为 Dockerfile 的简单配置文件。首先,我们在 Dockerfile 中添加所有生成说明,例如代码依赖项、命令和基础镜像详细信息。
必须进行 Docker 镜像优化
尽管 Docker 构建过程很简单,但许多公司或个人犯了一个错误,即在不优化容器镜像的情况下构建臃肿的 Docker 镜像。
在典型的软件开发中,每个服务都会有多个版本/发行版,每个版本需要更多的依赖项、命令和配置。这给 Docker 镜像构建带来了一个挑战,就像现在一样——同样的代码需要更多的时间和资源来构建,然后才能作为容器发布。
我见过这样的情况:初始应用程序镜像从 350MB 开始,随着时间的推移,它增长到 1.5 GB 以上。
此外,通过安装不需要的库,我们通过增加攻击面来增加潜在安全风险的可能性。
因此,DevOps 工程师必须优化 docker 镜像,以确保 docker 镜像在应用程序构建或未来版本后不会变得臃肿。不仅对于生产环境,在 CI/CD 过程的每个阶段,都应该优化 docker 镜像。
此外,使用 Kubernetes 等容器编排工具,最好使用小型镜像,以减少镜像传输和部署时间。
如何减小 Docker 镜像大小?
如果我们采用典型应用程序的容器镜像,它包含基本镜像、Dependencies/Files/Configs 和 cruft(不需要的软件)。
因此,这一切都归结为我们如何有效地管理容器镜像中的这些资源。
让我们看一下优化 Docker 镜像的不同既定方法。此外,我们还提供了实际示例来实时了解 docker 镜像优化。
您可以使用本文中给出的示例,也可以在现有 Dockerfile 上尝试优化技术。
以下是我们可以实现 docker 镜像优化的方法。
- 使用
Distroless
/最小基础镜像 - 多阶段构建
- 最小化层数
- 利用缓存
- 使用 Dockerignore
- 将应用程序数据保留在其他地方
方法一:使用最小的基础镜像
首先要注意的是选择正确的基础镜像,并将操作系统占用空间最小。
一个这样的例子是高山基础镜像。Alpine 镜像可以小至 5.59MB。它不仅很小;它也非常安全。
alpine latest c059bfaa849c 5.59MB
Nginx alpine 基础镜像只有 22MB。
默认情况下,它带有 sh shell,通过附加容器来帮助调试容器。
您可以使用 distroless 镜像进一步减小基础镜像大小。它是操作系统的精简版本。Distroless 基础映像可用于 java、nodejs、python、Rust 等。
无发行版镜像非常小,以至于它们甚至没有外壳。那么,你可能会问,那么我们如何调试应用程序呢?它们具有用于调试的 busybox 附带的相同镜像的调试版本。
此外,大多数发行版现在都具有最少的基础镜像。
注意:您不能在项目环境中直接使用公开可用的基础镜像。需要获得企业安全团队的批准才能使用基础镜像。在一些组织中,安全团队本身在测试和安全扫描后每个月都会发布基础镜像。这些镜像将在公共组织 docker 私有存储库中提供。
方法二:使用Docker多阶段构建
多阶段构建模式是从构建器模式的概念演变而来的,在构建器模式中,我们使用不同的 Dockerfile 来构建和打包应用程序代码。尽管此模式有助于减小镜像大小,在构建管道时,它几乎没有开销。
在多阶段构建中,我们获得了与构建器模式类似的优势。在此方法中,我们使用中间镜像(构建阶段)来编译代码、安装依赖项和打包文件。这样操作是消除镜像中不需要的图层。
之后,仅将运行应用程序所需的必要应用程序文件复制到另一个仅包含所需库的镜像中,即运行应用程序时更轻。
让我们通过一个实际示例来了解一下,在该示例中,我们创建了一个简单的 Nodejs 应用程序并优化了其 Dockerfile。
首先,创建代码文件,代码结构如下。
├── Dockerfile1 ├── Dockerfile2 ├── env ├── index.js └── package.json
将以下内容另存为 .index.js
const dotenv=require('dotenv'); dotenv.config({ path: './env' }); dotenv.config(); const express=require("express"); const app=express(); app.get('/',(req,res)=>{ res.send(`Learning to Optimize Docker Images with DevOpsCube!`); }); app.listen(process.env.PORT,(err)=>{ if(err){ console.log(`Error: ${err.message}`); }else{ console.log(`Listening on port ${process.env.PORT}`); } } )
将以下内容另存为 .package.json
{ "name": "nodejs", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "dotenv": "^10.0.0", "express": "^4.17.2" } }
将以下端口变量保存在名为 .env
的文件中
PORT=8080
对于此应用程序来说,示例如下 - 将其另存为 .Dockerfile
Dockerfile1
FROM node:16 COPY . . RUN npm installEXPOSE 3000 CMD [ "node", "index.js" ]
让我们通过构建它来查看它所需的存储空间:
docker build -t devopscube/node-app:1.0 --no-cache -f Dockerfile1 .
生成完成后,使用 ls 命令查看:
docker image ls
结果如下:
devopscube/node-app 1.0 b15397d01cca 22 seconds ago 910MB
大小是 910M
现在,让我们使用此方法创建一个多阶段构建。
我们所有依赖项和模块安装的镜像作为基础镜像,之后,我们将内容移动到一个最小且更轻的基础镜像中。该镜像具有更少的实用程序,因此非常轻。node:16
alpine
alpine
下面是 Docker 多阶段构建的图形表示。
此外,一个 Dockerfile
拥有具有不同基础镜像的多个阶段。例如,您可以设置不同的阶段,用于生成、测试、静态分析和使用不同的基础镜像进行打包。Dockerfile
让我们看看新的 Dockerfile 可能是什么样子。我们只是将必要的文件从基础镜像复制到主镜像。
将以下内容另存为 .Dockerfile2
FROM node:16 as build WORKDIR /app COPY package.json index.js env ./ RUN npm install FROM node:alpine as main COPY --from=build /app / EXPOSE 8080 CMD ["index.js"]
让我们通过构建它来查看它所需的存储空间。
docker build -t devopscube/node-app:2.0 --no-cache -f Dockerfile2 .
生成完成后,使用 ls 命令查看:
docker image ls
结果如下:
devopscube/node-app 2.0 fa6ae75da252 32 seconds ago 171MB
因此,与具有所有依赖项的镜像相比,新减小的镜像大小为 171MB。
这是超过 80% 的优化!
但是,如果我们使用在构建阶段使用的相同基础镜像,则不会看到太大差异。
您可以使用 distroless 镜像进一步减小镜像大小。这里是相同的多阶段构建步骤,使用谷歌nodeJS 的分布式镜像,而不是 alpine。Dockerfile
FROM node:16 as build WORKDIR /app COPY package.json index.js env ./ RUN npm install FROM gcr.io/distroless/nodejs COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
如果构建上述 Dockerfile,则镜像将为 118MB,
devopscube/distroless-node 1.0 302990bc5e76 118MB
方法三:最小化层数
Docker 镜像的工作方式如下 – 每个 Dockerfile 指令都会添加一个新层,每一层都会增加构建执行时间,并增加镜像的存储要求。RUN, COPY, FROM
让我们这个实际的例子:创建一个带有更新和升级库的 ubuntu 镜像,以及一些必要的软件包,如 vim、net-tools、dnsutils。
要实现此目的,如下所示 – 将其另存为 .Dockerfile
Dockerfile3
FROM ubuntu:latest ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -y RUN apt-get upgrade -y RUN apt-get install vim -y RUN apt-get install net-tools -y RUN apt-get install dnsutils -y
我们还希望了解此镜像的构建时间。
Docker 守护程序具有内置功能,可以显示 Dockerfile 所花费的总执行时间。
要启用此功能,请执行以下步骤 -
- 使用以下内容创建文件,
/etc/docker/daemon.json
{ "experimental": true }
2. 执行以下命令以启用该功能。
export DOCKER_BUILDKIT=1
让我们构建它,看看存储和构建时间。
time docker build -t devopscube/optimize:3.0 --no-cache -f Dockerfile3 .
它将在终端中显示执行时间。
time docker build -t devopscube/optimize:3.0 --no-cache -f Dockerfile3 . [+] Building 117.1s (10/10) FINISHED => [internal] load build definition from Dockerfile . . . . => => writing image sha256:9601bcac010062c656dacacbc7c554b8ba552c7174f32fdcbd24ff9c7482a805 0.0s => => naming to docker.io/devopscube/optimize:3.0 0.0s real 1m57.219s user 0m1.062s sys 0m0.911s
构建完成后,执行时间为 117.1 秒。
执行
docker image ls
结果如下:
devopscube/optimize 3.0 9601bcac0100 About a minute ago 227MB
所以大小是 227MB。
让我们将 RUN 命令合并到一个层中,并将其另存为 Dockerfile4。
FROM ubuntu:latest ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -y && apt-get upgrade -y && apt-get install --no-install-recommends vim net-tools dnsutils -y
在上面的 RUN 命令中,我们使用了 flag 来禁用推荐的软件包。 --no-install-recommends
install
Dockerfiles
让我们看看构建它所需的存储和构建时间。
time docker build -t devopscube/optimize:4.0 --no-cache -f Dockerfile4 .
它将在终端中显示执行时间。
time docker build -t devopscube/optimize:0.4 --no-cache -f Dockerfile4 . [+] Building 91.7s (6/6) FINISHED => [internal] load build definition from Dockerfile2 0.4s . . . => => naming to docker.io/devopscube/optimize:4.0 0.0s real 1m31.874s user 0m0.884s sys 0m0.679s
构建完成后,执行时间为 91.7 秒。
执行
docker image ls
结果如下。
devopscube/optimize 4.0 37d746b976e3 42 seconds ago 216MB
所以大小是 216MB。
使用这种优化技术,执行时间从 117.1 秒减少到 91.7 秒,存储大小从 227MB 减少到 216MB。
方法四:利用缓存
通常,必须一次又一次地重建相同的镜像,并对代码进行轻微的修改。
在这种情况下,Docker 通过存储构建的每一层的缓存来提供帮助,希望它在未来可能有用。
由于这个概念,建议在 COPY 命令之前添加,用于在 Dockerfile 中安装依赖项和包。
原因是 docker 将能够缓存具有所需依赖项的镜像,然后当代码被修改时,可以在以下构建中使用此缓存。
例如,让我们看一下以下两个 Dockerfile。
Dockerfile5
FROM ubuntu:latest ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -y && \ apt-get upgrade -y && \ apt-get install -y vim net-tools dnsutils COPY . .
Dockerfile6
FROM ubuntu:latest COPY . . ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -y && \ apt-get upgrade -y && \ apt-get install -y vim net-tools dnsutils
由于 COPY 命令的位置靠后,Docker 将能够更好地使用缓存功能。
方法五:使用Dockerignore
通常,只需将必要的文件复制到 docker 镜像上。
Docker 可以忽略配置在 .dockerignore
文件中的文件。
在优化 docker 镜像时应牢记此功能。
方法六:将应用程序数据保留在其他地方
在镜像中存储应用程序数据将不必要地增加镜像的大小。
强烈建议使用容器运行时的卷功能,将镜像与数据分开。
Docker 镜像优化工具
以下是一些可帮助您优化 Docker 镜像的开源工具。您可以选择一个工具,并将其作为 Docker 镜像管道的一部分,以确保仅为应用程序部署创建优化的镜像。
- Dive:它是一个镜像资源管理器工具,可帮助您发现 Docker 和 OCI 容器镜像中的层。使用 Dive,您可以找到优化 Docker 镜像的方法。查看 Dive Github 存储库了解更多详细信息。
- Docker Slim: 它可以帮助您优化 Docker 镜像的安全性和大小。有关更多详细信息,请查看 Docker Slim Github 存储库。您可以使用 Slim 将 docker 镜像大小减小到 30 倍。
- Docker Squash此实用程序可帮助您通过使用 squash 压缩镜像图层来减小镜像大小。在Docker CLI中使用squash标志也可以使用 squash 功能。
总结
上述方法应该可以帮助您构建优化的 Docker 镜像并编写更好的 Dockerfile。
此外,如果遵循所有标准容器最佳实践,则可以减小 docker 镜像大小以实现轻量级镜像部署。
此外,从 DevSecOps 的角度来看,您可以使用开源漏洞扫描工具(如 Trivy)来扫描 Docker 镜像