当前位置:首页 > 编程语言 > 正文内容

如何更高效更快地构建Docker镜像

a811625533年前 (2023-04-15)编程语言22

在构建服务期间,我们经常需要构建docker镜像。我们每天都要做很多次。这可能是一个耗时的任务。在本地,我们只注意到一点,但在CI/CD管道中,这可能是一个问题。

在这篇文章中,我将告诉你如何加快构建Docker镜像这一过程。我将向你展示如何使用缓存,将你的Docker文件分层,并使用多阶段构建,以使你的构建更快。

为此,我将使用一个简单的go应用程序。你可以使用你的任何其他应用程序。你使用哪个堆、语言或框架并不重要。原则都是一样的。

我所做的一切都在我的本地机器上执行。我不使用任何CI/CD工具。我使用Docker Desktop for Mac。

清理工作

为了确保我们从一个干净的状态开始,我们可以删除所有未使用的镜像、容器、卷和 *** :

$ docker system prune -a
WARNING! This will remove:
- all stopped contAIners
- all networks not used by at least one container
- all images without at least one container associated to them
- all build cache
Are you sure you want to continue? [y/N] y
...gone with the wind...

起始点

我从一个简单的Dockerfile(Dockerfile_1)开始:

FROM golang:buster
WORKDIR /app
COPY app /app/
ENTRYPOINT [ "/app/app" ]

为了能够使用这个Docker文件,我必须先建立一个应用程序:

$ go build -o app

然后再建立镜像:

$ docker build  . -f Dockerfile_1
Sending build context to Docker daemon  22.84MB
Step 1/4 : FROM golang:buster
---> f8c6c6bf3e26
Step 2/4 : WORKDIR /app
---> Running in 62eb8791ace1
Removing intermediate container 62eb8791ace1
---> d586151d2813
Step 3/4 : COPY app /app/
---> 25b4f091cba7
Step 4/4 : ENTRYPOINT [ "/app/app" ]
---> Running in 7853090f8c3b
Removing intermediate container 7853090f8c3b
---> 0e3D3835a61b
Successfully built 0e3d3835a61b

我想启动它,但我需要知道镜像的名称。我可以用 docker images 来找到它:

$ docker images
REPOSITORY              TAG               IMAGE ID       CREATED          SIZE
<none>                  <none>            0e3d3835a61b   48 seconds ago   739MB
excalidraw/excalidraw   latest            d6392f9c5191   2 days ago       34.8MB
golang                  buster            f8c6c6bf3e26   4 days ago       720MB
moby/buildkit           buildx-stable-1   4dc9f4d5bf89   2 weeks ago      168MB
slimdotai/dd-ext        0.8.2             56f11b815b6c   7 months ago     153MB

我可以看到镜像的名称是 <none> 。我可以用它来启动容器:

$ docker run 0e3d3835a61b
exec /app/app: exec format error

会发生什么?回到Dockerfile_1,看一下它。这里面有几个问题:

  • 我正在为OSX构建应用程序,但我想在Linux中运行它。
  • 我没有指定我使用的是哪个Go版本。在本地,我可以使用Go 1.16,但镜像上有最新的Go版本(目前是1.20)。
  • 我的应用程序使用9999端口,但我没有公开它。
  • 我的镜像没有名称和版本。

多阶段构建

为了解决之一个问题,我可以使用多阶段构建。我将创建一个新的Dockerfile(Dockerfile_2):

ARG GO_VERSION=1.20.3
FROM golang:${GO_VERSION}-buster as builder
WORKDIR /app
COPY . /app/
RUN go mod tidy
RUN go build -o app
FROM debian:buster as final
WORKDIR /app
COPY --from=builder /app/app /app/
EXPOSE ${PORT:-9999}
ENTRYPOINT [ "/app/app" ]

在新的Docker文件中,我用 ARG 指令处理Go版本。你不一定要这样做。你也可以对版本进行硬编码。但有了 ARG ,你可以在构建镜像时覆盖它。

构建一个应用程序被移到之一个或 builder 阶段。当应用程序构建完成后,它被复制到第二阶段或 final 阶段。在这两个阶段,我都使用Debian Buster。它是一个小的映像,对我的应用程序来说已经足够了。我还暴露了一个端口,设置默认值为9999。

现在我可以建立镜像了:

$ docker build . -t rnemet/echo:0.0.1 -f Dockerfile_2
Sending build context to Docker daemon  22.84MB
Step 1/11 : ARG GO_VERSION=1.20.3
Step 2/11 : FROM golang:${GO_VERSION}-buster as builder
1.20.3-buster: Pulling from library/golang
Digest: sha256:413cd9e04db86fee3f5c667de293f37d9199b74880771c37dcfeb165cefaf424
Status: Downloaded newer image for golang:1.20.3-buster
---> f8c6c6bf3e26
Step 3/11 : WORKDIR /app
---> Using cache
---> d586151d2813
Step 4/11 : COPY . /app/
---> 331d288c0f19
Step 5/11 : RUN go mod tidy
---> Running in 2657122aa7fe
go: downloading github.com/prometheus/client_golang v1.14.0
...snip...
go: downloading github.com/rogpeppe/go-internal v1.8.0
Removing intermediate container 2657122aa7fe
---> 48197d27f8ab
Step 6/11 : RUN go build -o app
---> Running in 7e593ea7ffb4
Removing intermediate container 7e593ea7ffb4
---> d086687f4f17
Step 7/11 : FROM debian:buster
buster: Pulling from library/debian
4e2befb7f5d1: Already exists
Digest: sha256:235f2a778fbc0d668c66afa9fd5f1efabab94c1d6588779ea4e221e1496f89da
Status: Downloaded newer image for debian:buster
---> 4591634d6289
Step 8/11 : WORKDIR /app
---> Running in a79e19ed4815
Removing intermediate container a79e19ed4815
---> b316081e2c13
Step 9/11 : COPY --from=builder /app/app /app/
---> 6fdc4f84223f
Step 10/11 : EXPOSE ${PORT:-9999}
---> Running in e5bf1bc188b9
Removing intermediate container e5bf1bc188b9
---> 8da39c1270c4
Step 11/11 : ENTRYPOINT [ "/app/app" ]
---> Running in 421008b145ee
Removing intermediate container 421008b145ee
---> 159ca8b29354
Successfully built 159ca8b29354
Successfully tagged rnemet/echo:0.0.1

现在我可以看到镜像有名称和版本:

docker images
REPOSITORY              TAG               IMAGE ID       CREATED          SIZE
rnemet/echo             0.0.1             159ca8b29354   4 minutes ago    133MB
<none>                  <none>            d086687f4f17   4 minutes ago    1.17GB
<none>                  <none>            0e3d3835a61b   40 minutes ago   739MB
excalidraw/excalidraw   latest            d6392f9c5191   2 days ago       34.8MB
golang                  1.20.3-buster     f8c6c6bf3e26   5 days ago       720MB
golang                  buster            f8c6c6bf3e26   5 days ago       720MB
moby/buildkit           buildx-stable-1   4dc9f4d5bf89   2 weeks ago      168MB
debian                  buster            4591634d6289   2 weeks ago      114MB
slimdotai/dd-ext        0.8.2             56f11b815b6c   7 months ago     153MB

而且我可以运行这个容器:

$ docker run rnemet/echo:0.0.1
2021/12/05 20:56:05 Starting server on port 9999

如果你想覆盖Go版本,你可以这样做:

$ docker build . -t rnemet/echo:0.0.1 -f Dockerfile_2 --build-arg GO_VERSION=1.16.10

分层和缓存

再看一下Dockerfile_2。Dockerfile中的每个条目都创建了一个新的层,每个层都被缓存了。如果你改变了Dockerfile中的内容,Docker将重建被改变的层和所有后续层。

看一下 docker build 命令的输出

Sending build context to Docker daemon  22.84MB
Step 1/11 : ARG GO_VERSION=1.20.3
Step 2/11 : FROM golang:${GO_VERSION}-buster as builder
1.20.3-buster: Pulling from library/golang
Digest: sha256:413cd9e04db86fee3f5c667de293f37d9199b74880771c37dcfeb165cefaf424
Status: Downloaded newer image for golang:1.20.3-buster
---> f8c6c6bf3e26
Step 3/11 : WORKDIR /app
---> Using cache                            <=== here cache is used
---> d586151d2813
Step 4/11 : COPY . /app/
---> 331d288c0f19
Step 5/11 : RUN go mod tidy
---> Running in 2657122aa7fe
go: downloading github.com/prometheus/client_golang v1.14.0
...snip...
go: downloading github.com/rogpeppe/go-internal v1.8.0
Removing intermediate container 2657122aa7fe
---> 48197d27f8ab

我的目标是编写基本相同的图层。这样一来,我就可以使用缓存,更快地建立镜像。在第4步,我把所有文件从我的本地目录复制到镜像上。乍一看,这的确有道理。但是,如果我改变了一个README文件,或者任何其他与应用程序无关的文件,我将重建整个镜像。这就不妙了。所以,我要么指定复制什么,要么不复制什么。

对于第二个选择,我可以使用 .dockerignore 文件。它类似于 .gitignore 文件。它包含一个不应该被复制到镜像中的文件列表:

.gitignore
.dockerignore
**/compose*
Dockerfile
License
Makefile
Readme.md

那么 COPY . /app/ 将只复制文件,不在 .dockerignore 文件中。

让我们再考虑一件事。在第5步,我正在运行 go mod tidy 。它下载了所有的依赖项。这些依赖项并不经常改变。当它们被改变时,我应该重建这个应用程序。对于Go应用程序来说,下载依赖项并不是一个大问题,但对于其他语言来说,这可能是一个问题(想想Nodejs)。所以,让我们先处理依赖关系,然后再复制源代码。这样一来,我就用一个缓存来处理依赖关系,而不是在每次改变源代码时都重建它们。

ARG GO_VERSION=1.20.3
FROM golang:${GO_VERSION}-buster as builder
WORKDIR /app
COPY go.mod go.sum /app/
RUN go mod download -x
COPY . /app/
RUN go build -o app
FROM debian:buster
WORKDIR /app
COPY --from=builder /app/app /app/
EXPOSE ${PORT:-9999}
ENTRYPOINT [ "/app/app" ]

当最初运行 docker build . -t rnemet/echo:0.0.1 -f Dockerfile_3 会花一些时间来下载依赖项。因为我使用了选项 -x,我可以看到所有下载的依赖项。如果你觉得麻烦,你可以删除 -x 选项。如果你重新运行它,它将会快得多。而且,你会注意到,依赖关系是被缓存的。

如果你改变了源代码,依赖项就不会被再次下载。所以构建镜像的速度会快很多。

自己试试吧。比较Dockerfile_2和Dockerfile_3的构建时间。

远程缓存

在使用CI/CD时,你要么依靠CI/CD缓存的实现,要么依靠远程缓存。远程缓存是一个存储在远程位置的缓存,因此,你可以用它来加快构建速度,在不同的机器和不同的用户之间共享。

为此,我不得不使用。它是Docker的一个新的构建工具箱。你可以像这样使用它:

docker buildx build -t rnemet/echo:0.0.1 . -f Dockerfile_3 --cache-to type=registry,ref=rnemet/echo:test --cache-from type=registry,ref=rnemet/echo:test --cache-from type=registry,ref=rnemet/echo:main [--push|--load]

如果你想使用远程缓存,请指定 --cache-to--cache-from 选项。选项 --cache-to 指定了存储缓存的位置。选项 --cache-from 指定从哪里获得缓存。你可以为这两个选项指定多个位置。如果你为 --cache-from 指定了多个位置,它将尝试从所有的位置获取缓存。如果它在其中一个地方找到了缓存,它就会使用它。

一个好的做法是为分支和主干创建一个缓存。在上面的例子中,我有 testmain 两个分支。我把 test 分支用于测试main 用于生产。所以,我为这两个分支都建立了缓存。如果我正在建立一个 test 分支,它将尝试从 test 分支获取缓存,如果失败,它将尝试从 main 分支获取缓存。

如果你想把镜像推送到注册表,使用 --push 选项。如果你要把镜像加载到你的本地机器上,你可以使用 --load 选项。

总结

在这篇文章中,我向你展示了如何构建Dockerfile以加快构建过程。我希望你觉得这篇文章对你有帮助。

参考文献

扫描二维码推送至手机访问。

版权声明:本文由2345好导航站长资讯发布,如需转载请注明出处。

本文链接:http://www.2345hao.cn/blog/index.php/post/7988.html

分享给朋友:

“如何更高效更快地构建Docker镜像” 的相关文章

宝塔面板教程之文件管理篇

宝塔面板教程之文件管理篇

宝塔面板其中一个最为便捷的功能之一,无需SFTP或者FTP即可对服务器的文件内容进行上传、下载、编辑及删除等管理操作。 文件管理,用于管理该服务器上的文件内容。 文件的基础操作 文件的基础操作有哪些了,主要有这些方面:复制、粘贴、剪切、删除、重命名、压缩、刷新、新建文件、新建目录。...

Serverless PHP简介:主要功能、用例以及如何在Lambda上开始使用Bref

Serverless PHP简介:主要功能、用例以及如何在Lambda上开始使用Bref

无服务器计算是一种基于云的执行模型,可以将应用程序作为服务托管,而无需维护服务器。 服务提供商维护服务器上的资源分配,并根据实际使用情况向用户收费。焦点转移到一个人正在创建的核心应用程序上,基础设施完全由服务提供商处理。无服务器计算也称为功能即服务 (FaaS)。 换句话说,Serverle...

什么是React以及它实际上是如何工作的?

什么是React以及它实际上是如何工作的?

是用于移动和Web应用程序开发的最流行的库之一。React由Facebook创建,包含一组可重用的JavaScript代码片段,用于构建称为组件的用户界面 (UI)。 重要的是要注意ReactJS不是JavaScript框架。那是因为它只负责渲染应用程序视图层的组件。React是和等框架的替代...

什么是查询?数据库查询解释

什么是查询?数据库查询解释

在标准英语中,查询query意味着对信息的请求。在计算机编程中,它指的是同一件事,只是信息是从数据库中检索的。 但是,编写查询需要一组预定义的代码才能使数据库理解指令。这个概念也被称为查询语言。 虽然数据库管理的标准语言是结构化查询语言 (SQL),但其他简化数据库通信的查询语言包括AQL、...

什么是Angular?一起认识这个流行JavaScript框架

什么是Angular?一起认识这个流行JavaScript框架

如果您从事软件开发,您可能听说过Angular。它是最流行的框架之一,开发人员使用它来构建动态网站。在本文中,您将了解AngularJS的概念、Angular的首次构建时间及其优势。 什么是AngularJS? 那么什么是角?它是一个开源软件工程框架,用...

10个用于WordPress插件的PHP测试工具

10个用于WordPress插件的PHP测试工具

没有软件是没有错误的。这是适用于每种编程语言和每种应用程序的公理。 当这些错误在您的网站中部署在生产环境中时,您可能会遭受不同严重程度的不利后果。这些是一些例子,从烦人到经济破坏: 轻度:用户无法点击断开的链接。 中等:联系表单的“提交”按钮不起作用,用户只有在撰写完他们的消息后才...