如果您想减少docker镜像的大小,您需要使用构建docker镜像的标准最佳实践。
本博客讨论了您可以快速实施的各种优化技术,以制作最小、最精简的 docker 镜像。我们还将介绍一些用于 Docker 镜像优化的最佳工具。
Docker 作为一种容器引擎,可以轻松地获取一段代码并在容器内运行。它使工程师能够将所有代码依赖项和文件收集到一个位置,然后可以在任何地方快速轻松地运行。
“随处运行”镜像的整个概念始于一个名为 Dockerfile 的简单配置文件。首先,我们在 Dockerfile 中添加所有构建指令,例如代码依赖项、命令和基础镜像详细信息。
Docker 镜像优化需求
尽管Docker构建过程很简单,但许多组织都犯了一个错误,即构建 臃肿的 Docker 镜像 而没有优化容器镜像。
在典型的软件开发中,每个服务都会有多个版本/发布,每个版本都需要更多的依赖项、命令和配置。这给 Docker 镜像构建带来了挑战,因为现在——相同的代码需要更多的时间和资源来构建,然后才能作为容器运送。
我曾经见过这样的情况:初始应用程序映像大小为 350MB,随着时间的推移,其大小增长到 1.5 GB 以上。
此外,通过安装不需要的库,我们增加了攻击面,从而增加了潜在安全风险的可能性。
因此,DevOps 工程师必须优化 docker 镜像,以确保 docker 镜像在应用程序构建或未来发布后不会变得臃肿。不仅限于生产环境,在 CI/CD 流程的每个阶段,您都应该优化 docker 镜像。
此外,使用Kubernetes 等容器编排工具时,最好使用小尺寸的镜像,以减少镜像传输和部署时间。
如何减少 Docker 镜像大小?
如果我们采用典型应用程序的容器镜像,它包含基本镜像、依赖项/文件/配置和垃圾(不需要的软件)。
所以一切都归结为我们如何有效地管理容器镜像中的这些资源。
让我们看看优化 Docker 镜像的不同既定方法。此外,我们还提供了实际示例,以便实时了解 Docker 镜像优化。
您可以使用文章中给出的示例,也可以在现有的 Dockerfile 上尝试优化技术。
下面我们可以通过以下几种方法来实现docker镜像的优化。
- 使用 distroless/minimal 基础镜像
- 多阶段构建
- 最小化层数
- 了解缓存
- 使用 Dockerignore
- 将应用程序数据保存在其他地方
Docker 练习文件:本文中使用的所有应用程序代码、Dockerfile 和配置都托管在这个 Github 存储库中。您可以克隆它并按照本教程进行操作。
方法 1:使用最小基础镜像
您首先应该关注的是选择具有最小操作系统占用空间的正确基础映像。
一个这样的例子是 alpine 基础镜像。Alpine 镜像可以小到 5.59MB。它不仅小,而且非常安全。
alpine latest c059bfaa849c 5.59MB
Nginx alpine基础镜像仅有 22MB。
默认情况下,它带有 sh shell,可通过附加它来帮助调试容器。
您可以使用distroless images进一步减小基础镜像大小。它是操作系统的精简版。Distroless 基础镜像适用于 java、nodejs、python、Rust 等。
Distroless 镜像非常小,甚至没有 shell。那么,你可能会问,那么我们如何调试应用程序呢?他们有与 busybox 一起提供的相同镜像的调试版本,用于调试。
此外,现在大多数发行版都具有最少的基础图像。
注意:您不能直接在项目环境中使用公开可用的基础镜像。您需要获得企业安全团队的批准才能使用基础镜像。在某些组织中,安全团队本身会在测试和安全扫描后每月发布基础镜像。这些镜像将在通用组织 docker 私有存储库中提供。
方法 2:使用 Docker 多阶段构建
多阶段构建模式是从构建器模式的概念发展而来的,我们使用不同的 Dockerfile 来构建和打包应用程序代码。尽管这种模式有助于减小镜像大小,但在构建管道时,它几乎没有开销。
在多阶段构建中,我们获得了与构建器模式类似的优势。我们使用中间映像(构建阶段)来编译代码、安装依赖项并打包文件。其背后的想法是消除映像中不需要的层。
之后,仅将运行应用程序所需的必要应用文件复制到仅具有所需库的另一个映像中,即可更轻地运行应用程序。
让我们借助一个实际示例来看一下它的实际效果,其中我们创建了一个简单的 Nodejs 应用程序并优化了它的 Dockerfile。
首先,让我们创建代码。我们将拥有以下文件夹结构。
├── Dockerfile1
├── Dockerfile2
├── env
├── index.js
└── package.json
将以下内容保存为index.js
。
将以下内容保存为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:16COPY . .RUN npm installEXPOSE 3000CMD [ "node", "index.js" ]
让我们看看构建它所需的存储空间。
docker build -t devopscube/node-app:1.0 --no-cache -f Dockerfile1 .
构建完成后。让我们使用以下方法检查其大小:
docker image ls
这就是我们所得到的。
devopscube/node-app 1.0 b15397d01cca 22 seconds ago 910MB
所以尺寸是910MBs
。
现在,让我们使用此方法来创建多阶段构建。
我们将使用基础镜像,即所有依赖项和模块安装的node:16
镜像,之后,我们将内容移至最精简、更轻量的基于 ' ' 的镜像中。' ' 镜像具有最低限度的实用程序,因此非常轻量。alpine
alpine
以下是 Docker 多阶段构建的图示。
此外,在单个 中Dockerfile
,您可以拥有多个具有不同基础映像的阶段。例如,您可以拥有具有不同基础映像的构建、测试、静态分析和打包的不同阶段。
让我们看看新的 Dockerfile 是什么样子。我们只是将必要的文件从基础镜像复制到主镜像。
将以下内容保存为Dockerfile2
。
FROM node:16 as buildWORKDIR /app
COPY package.json index.js env ./
RUN npm installFROM node:alpine as mainCOPY --from=build /app /
EXPOSE 8080
CMD ["index.js"]
让我们看看构建它所需的存储空间。
docker build -t devopscube/node-app:2.0 --no-cache -f Dockerfile2 .
构建完成后。让我们使用以下代码检查其大小
docker image ls
这就是我们所得到的。
devopscube/node-app 2.0 fa6ae75da252 32 seconds ago 171MB
因此,与具有所有依赖项的图像相比,新的缩小图像大小为 171MB 。
这优化了超过 80%!
但是,如果我们使用与构建阶段相同的基础图像,就不会看到太大的差异。
您可以使用 distroless images 进一步减小图像大小。以下是Dockerfile
使用 Google nodeJS distroless 图像(而不是 alpine)的多阶段构建步骤。
FROM node:16 as buildWORKDIR /appCOPY package.json index.js env ./RUN npm installFROM gcr.io/distroless/nodejsCOPY --from=build /app /EXPOSE 3000CMD ["index.js"]
如果你构建上述 Dockerfile,你的镜像将有118MB,
devopscube/distroless-node 1.0 302990bc5e76 118MB
方法 3:最小化层数
Docker 镜像的工作方式如下 - 每个RUN, COPY, FROM
Dockerfile 指令添加一个新层,每个层都会增加构建执行时间并增加镜像的存储要求。
让我们借助一个实际示例来看一下它的实际效果:让我们创建一个包含更新和升级的库的 ubuntu 映像,并安装一些必要的包,例如 vim、net-tools、dnsutils。
实现此目的的方法Dockerfile
如下 - 将其另存为Dockerfile3
。
FROM ubuntu:latestENV DEBIAN_FRONTEND=noninteractiveRUN apt-get update -yRUN apt-get upgrade -yRUN apt-get install vim -yRUN apt-get install net-tools -yRUN apt-get install dnsutils -y
我们还希望了解该图像的构建时间。
Docker 守护进程具有内置功能,可以显示 Dockerfile 所需的总执行时间。
要启用此功能,请执行以下步骤 -
- 在以下位置创建
daemon.json
包含以下内容的文件/etc/docker/
{"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:latestENV DEBIAN_FRONTEND=noninteractiveRUN apt-get update -y && \apt-get upgrade -y && \apt-get install --no-install-recommends vim net-tools dnsutils -y
在上面的 RUN 命令中,我们使用了--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。
方法 4:了解缓存
通常,只需对代码进行少许修改即可一次又一次地重建相同的图像。
由于 Docker 使用分层文件系统,因此每条指令都会创建一个层。因此,Docker 会缓存该层,如果该层未发生更改,则可以重复使用它。
由于这个概念,建议在COPY 命令之前在–内添加用于安装依赖项和包的行。Dockerfile
这样做的原因是,docker 能够缓存具有所需依赖项的图像,然后在代码修改时可以在后续构建中使用此缓存。
此外, a 中的COPY
和ADD
指令Dockerfile
会使后续层的缓存无效。这意味着,Docker 将在 COPY 和 ADD 之后重建所有层。
这意味着,建议在 Dockerfile 中尽早添加不太可能改变的指令。
例如,我们来看看下面两个Dockerfile。
Dockerfile 5(良好示例)
FROM ubuntu:latestENV DEBIAN_FRONTEND=noninteractiveRUN apt-get update -y && \apt-get upgrade -y && \apt-get install -y vim net-tools dnsutilsCOPY . .
Dockerfile 6(次优示例)
FROM ubuntu:latestENV DEBIAN_FRONTEND=noninteractiveCOPY . .RUN apt-get update -y && \apt-get upgrade -y && \apt-get install -y vim net-tools dnsutils
由于COPY 命令的位置更佳,Docker 能够更好地使用缓存功能。Dockerfile5
Dockerfile6
方法 5:使用 Dockerignore
通常来说,只需要将必要的文件复制到 docker 镜像上。
如果在文件中配置了,Docker 可以忽略工作目录中存在的文件.dockerignore
。
它还通过忽略不必要的文件来改进缓存,并防止不必要的缓存无效。
在优化docker镜像时应该牢记这个特性。
方法 6:将应用程序数据保存在其他地方
将应用程序数据存储在图像中会不必要地增加图像的大小。
强烈建议使用容器运行时的卷功能将图像与数据分开。
如果你正在使用 Kubernetes,请确保
Docker 镜像优化工具
以下是一些可帮助您优化 Docker 镜像的开源工具。您可以选择一个工具并将其作为 Docker 镜像管道的一部分,以确保仅为应用程序部署创建优化的镜像。
1. Dive
这是一个图像浏览器工具,可帮助您发现 Docker 和 OCI 容器图像中的层。使用 Dive,您可以找到优化 Docker 图像的方法。查看Dive Github repo了解更多详细信息。
2. SlimtoolKit
它可以帮助您优化 Docker 镜像的安全性和大小。查看Docker Slim Github repo了解更多详情。您可以使用 Slim 将 docker 镜像大小缩小至原来的 1/30。
3. Docker Squash
此实用程序可帮助您通过压缩图像层来减小图像大小。使用squash 标志,Docker CLI 中也提供压缩功能。
我会继续将工具添加到此列表中。
总结
上述方法应该可以帮助您构建优化的 Docker 镜像并编写更好的 Dockerfile。
此外,如果您遵循所有标准容器最佳实践,则可以减小docker 镜像大小以实现轻量级镜像部署。