更快构建Docker映像的六种方法(甚至几秒钟)

更快构建Docker映像的六种方法(甚至几秒钟)

不知道大家对现在软件交付内心平衡度感受到差异化了吗,这句话说的不以为然,如今,复杂的编排器和CI CD对于软件开发至关重要,但是结果从提交到测试和交付,再到量产,还有一段很长的路要走。结合你的经验不知道你对此有什么看法,欢迎结尾讨论留言~

以前,做过开发的知道,开发人员过去常常通过FTP将新文件上传到服务器,因此部署花费了几秒钟。但是,现在我们必须创建一个合并请求,并等待很长时间才能使功能进入用户体系当中。而构建Docker镜像是此过程的一部分,可能需要花费数十分钟的时间。这几乎是大家不能接受的。不知道你们环境当中是不是也遇到了此问题,在本文中,我们将对一个简单的应用程序进行docker化,然后使用多种方法来加快构建时间并考虑其细微差别。

最近,我们正在将网站部署到生产环境中,并且在添加新功能和修复旧错误的同时,缓慢部署成为一个大问题,这令我很担忧。

加速构建🐳Docker镜像的六种方法这就回到我们的项目当中了,回顾一下我们使用的GitLab进行部署:构建的Docker镜像会将它们推送到我们的Container Registry并将我们的容器镜像部署到生产环境。构建Docker镜像是此列表上最长的过程。例如,构建每个未优化的后端镜像花了14分钟。

我感觉必须为此做些事情。我们决定弄清楚为什么构建Docker镜像需要这么长时间以及如何解决这种情况。结果,我们能够将构建时间减少到30秒构建完成!是吧,看来事情做对了,让我们继续研究一下如何优化的吧,这里告诉大家,希望可以帮助你脱离缓慢的苦海当中,😄

1 Docker镜像是什么?刚开始聊聊Docker镜像是什么,说白了Docker使打包应用程序并在称为容器的隔离环境中运行它们成为可能性,这个说的有点官方。其实就是由于这种隔离,你可以在单个服务器上同时运行多个容器。但是与虚拟机不同,Docker容器直接在内核上运行,因此它们更轻巧。在运行dockerized这个应用程序之前,我们会构建一个Docker镜像,将应用程序正常运行所需的一切打包到其中。Docker镜像就像文件系统的转换一样。例如,让我们看一下这个Dockerfile:

FROM node:12.16.2WORKDIR /appCOPY . .RUN npm ciRUN npm run build --prodDockerfile是一组指令。Docker并逐步执行这些指令,并将更改保存到文件系统中,并将其添加到以前的文件系统中。每个命令创建自己的层。完成的Docker镜像将所有这些层组合在一起。

重要的是要知道Docker缓存了每一层。如果自上次构建以来没有任何变化,则Docker将使用已完成的层而不是执行命令。构建速度的主要提高是由于有效的缓存使用,因此在测量构建速度时,我们将重点关注使用就绪缓存构建Docker镜像的速度。让我们一步一步走,先理解一下Dockerfile的图层结构

2 理解Dockerfile图层1.首先,我们需要在本地删除镜像,以便先前的运行不会影响测试。docker rmi $(docker images -q)2.接下来,让我们第一次运行我们的构建。time docker build -t app .3.现在,我们更改src index.html,以模拟程序的工作。

4.然后我们第二次运行构建。

time docker build -t app .如果我们正确设置了构建环境,那么在开始构建镜像时,Docker将已经拥有大量缓存。我们的目标是学习如何利用缓存,以便尽快执行构建过程。由于我们是在第一次构建镜像时不使用缓存,因此我们可以忽略它的速度。在测试中,我们对构建的第二次运行很感兴趣,就是当我们的缓存已经预热并且准备好就绪时才构建镜像。但是,应用一些技巧也会影响第一个构建。

让我们将上述Dockerfile放在项目文件夹中,然后运行查看一下构建的过程。但是为了便于阅读,这里缩短了所有清单。

$ time docker build -t app .Sending build context to Docker daemon 409MBStep 1/5 : FROM node:12.16.2Status: Downloaded newer image for node:12.16.2Step 2/5 : WORKDIR /appStep 3/5 : COPY . .Step 4/5 : RUN npm ciadded 1357 packages in 22.47sStep 5/5 : RUN npm run build --prodDate: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Time: 37581msSuccessfully built c8c279335f46Successfully tagged app:latestreal 5m4.541suser 0m0.000ssys 0m0.000s然后,我们必须更改src index.html的内容并第二次运行它。

$ time docker build -t app .Sending build context to Docker daemon 409MBStep 1/5 : FROM node:12.16.2Step 2/5 : WORKDIR /app---> Using cacheStep 3/5 : COPY . .Step 4/5 : RUN npm ciadded 1357 packages in 22.47sStep 5/5 : RUN npm run build --prodDate: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Time: 37902msSuccessfully built 79f335df92d3Successfully tagged app:latestreal 3m33.262suser 0m0.000ssys 0m0.000s现在,我们执行docker images命令来查看我们的镜像是否已成功创建:

REPOSITORY TAG IMAGE ID CREATED SIZEapp latest 79f335df92d3 About a minute ago 1.74GB在构建过程开始之前,Docker会获取当前构建上下文中的所有文件,并将它们发送到Docker守护程序:将构建上下文发送到Docker守护程序409MB。构建上下文由最后一个构建命令参数指示-在我们的示例中,它是一个点(.),这意味着Docker将从当前文件夹中获取所有文件。但是409MB很大,所以我们应该考虑如何解决这种情况。

所以这里给大家分享一下如何在这种情况下的解决方法,这里分为6种。

一、减少镜像上下文大小1、我们可以将构建所需的所有文件放在单独的文件夹中,并将Docker指向该文件夹,但这并不总是很方便。

2、通过将.dockerignore文件添加到上下文目录中,我们可以排除构建不需要的文件,这样大大减少了镜像的占用大小

让我们再次建立我们的镜像,如果你看到的话,其中镜像的体积已经减少了:

$ time docker build -t app .Sending build context to Docker daemon 607.2kBStep 1/5 : FROM node:12.16.2Step 2/5 : WORKDIR /app---> Using cacheStep 3/5 : COPY . .Step 4/5 : RUN npm ciadded 1357 packages in 22.47sStep 5/5 : RUN npm run build --prodDate: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Time: 37313msSuccessfully built 4942f010792aSuccessfully tagged app:latestreal 1m47.763suser 0m0.000ssys 0m0.000s是的,607.2KB比409MB好得多。我们还将镜像大小从1.74GB减小到1.38GB:

REPOSITORY TAG IMAGE ID CREATED SIZEapp latest 4942f010792a 3 minutes ago 1.38GB现在,我们将尝试进一步减小图像尺寸。

二、使用Alpine Linux减小Docker镜像大小的另一种方法是使用小的父镜像。父镜像是我们镜像所基于的镜像。最低层由Dockerfile中的FROM命令指示。在本例中,我们将使用基于Ubuntu的镜像,该镜像已安装Node.js。但这几乎是1GB(挺大的容量的!)。

$ docker images -a | grep nodenode 12.16.2 406aa3abbc6c 17 minutes ago 916MB如果使用基于Alpine Linux的镜像,则可以大大减小镜像大小。Alpine Linux它是一种极其轻量级的Linux发行版。Node.js的 Alpine镜像只有88.5MB!因此,让我们用较小的镜像代替较大的镜像,这样似乎体积由大大的减少了不少

FROM node:12.16.2-alpine3.11RUN apk --no-cache --update --virtual build-dependencies addpythonmakeg++WORKDIR /appCOPY . .RUN npm ciRUN npm run build --prod另外我们还必须安装构建应用程序所需的一些东西。是的,没有Python就不会构建Angular这个应用程序。

但这是值得的因为我们已经将镜像大小减少了619MB:

REPOSITORY TAG IMAGE ID CREATED SIZEapp latest aa031edc315a 22 minutes ago 761MB三、使用多阶段构建我们只会从镜像中获取生产中实际需要的东西。这就是我们现在所拥有的:

$ docker run app ls -lahtotal 576Kdrwxr-xr-x 1 root root 4.0K Apr 16 19:54 .drwxr-xr-x 1 root root 4.0K Apr 16 20:00 ..-rwxr-xr-x 1 root root 19 Apr 17 2020 .dockerignore-rwxr-xr-x 1 root root 246 Apr 17 2020 .editorconfig-rwxr-xr-x 1 root root 631 Apr 17 2020 .gitignore-rwxr-xr-x 1 root root 181 Apr 17 2020 Dockerfile-rwxr-xr-x 1 root root 1020 Apr 17 2020 README.md-rwxr-xr-x 1 root root 3.6K Apr 17 2020 angular.json-rwxr-xr-x 1 root root 429 Apr 17 2020 browserslistdrwxr-xr-x 3 root root 4.0K Apr 16 19:54 distdrwxr-xr-x 3 root root 4.0K Apr 17 2020 e2e-rwxr-xr-x 1 root root 1015 Apr 17 2020 karma.conf.js-rwxr-xr-x 1 root root 620 Apr 17 2020 ngsw-config.jsondrwxr-xr-x 1 root root 4.0K Apr 16 19:54 node_modules-rwxr-xr-x 1 root root 494.9K Apr 17 2020 package-lock.json-rwxr-xr-x 1 root root 1.3K Apr 17 2020 package.jsondrwxr-xr-x 5 root root 4.0K Apr 17 2020 src-rwxr-xr-x 1 root root 210 Apr 17 2020 tsconfig.app.json-rwxr-xr-x 1 root root 489 Apr 17 2020 tsconfig.json-rwxr-xr-x 1 root root 270 Apr 17 2020 tsconfig.spec.json-rwxr-xr-x 1 root root 1.9K Apr 17 2020 tslint.json使用docker run app ls -lah,我们根据应用程序镜像运行一个容器并执行ls -lah命令,然后退出容器。

对于生产,我们只需要dist文件夹。另外,我们需要以某种方式发送文件。我们可以运行某种Node.js HTTP服务器,但是有一种更简单的方法。我们使用Nginx镜像,并将dist文件夹和下面的小配置文件放入其中:

server {listen 80 default_server;server_name localhost;charset utf-8;root /app/dist;location / { try_files $uri $uri/ /index.html;}}我们将通过使用多阶段构建来实现。让我们更改我们的

Dockerfile:FROM node:12.16.2-alpine3.11 as builderRUN apk --no-cache --update --virtual build-dependencies addpythonmakeg++WORKDIR /appCOPY . .RUN npm ciRUN npm run build --prodFROM nginx:1.17.10-alpineRUN rm /etc/nginx/conf.d/default.confCOPY nginx/static.conf /etc/nginx/conf.dCOPY --from=builder /app/dist/app .现在,我们有两个FROM命令,每个命令都开始其自己的构建过程阶段。我们命名了第一阶段的构建器,第二个阶段的构建器开始了创建最终镜像的过程。在最后一步中,我们将构建从构建器阶段复制到最终的Nginx镜像。镜像尺寸已大大减小:

REPOSITORY TAG IMAGE ID CREATED SIZEapp latest 2c6c5da07802 29 minutes ago 36MB让我们将镜像作为容器运行,并确保一切正常:

docker run -p8080:80 app使用-p8080:80选项,我们将主机的端口8080转发到运行Nginx的容器的端口80。现在,我们在浏览器中打开http:// localhost:8080 并查看我们的应用程序。可以访问!

将镜像大小从1.74GB减少到36MB可以大大缩短应用程序投产的时间。再次让我们回到构建时间看一下

$ time docker build -t app .Sending build context to Docker daemon 608.8kBStep 1/11 : FROM node:12.16.2-alpine3.11 as builderStep 2/11 : RUN apk --no-cache --update --virtual build-dependencies add python make g++---> Using cacheStep 3/11 : WORKDIR /app---> Using cacheStep 4/11 : COPY . .Step 5/11 : RUN npm ciadded 1357 packages in 47.338sStep 6/11 : RUN npm run build --prodDate: 2020-04-16T21:16:03.899Z - Hash: fffa0fddaa3425c55dd3 - Time: 39948ms---> 27f1479221e4Step 7/11 : FROM nginx:stable-alpineStep 8/11 : WORKDIR /app---> Using cacheStep 9/11 : RUN rm /etc/nginx/conf.d/default.conf---> Using cacheStep 10/11 : COPY nginx/static.conf /etc/nginx/conf.d---> Using cacheStep 11/11 : COPY --from=builder /app/dist/app .Successfully built d201471c91adSuccessfully tagged app:latestreal 2m17.700suser 0m0.000ssys 0m0.000s四、更改图层顺序Docker缓存了前三个步骤(使用缓存)。在第四步中,将复制所有项目文件,在第五步中,npm ci将安装依赖项,整个过程耗时47.338秒。如果依赖关系很少更改,为什么每次都需要重新安装依赖关系?让我们看看为什么它们没有被缓存。事实是,Docker会逐层检查以查看命令和与其关联的文件是否已更改。第四步,我们复制项目的所有文件,其中一些已更改。这就是为什么Docker不仅不使用该层的缓存版本,而且不使用以下缓存版本的原因!让我们对Dockerfile进行一些更改。

FROM node:12.16.2-alpine3.11 as builderRUN apk --no-cache --update --virtual build-dependencies addpythonmakeg++WORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build --prodFROM nginx:1.17.10-alpineRUN rm /etc/nginx/conf.d/default.confCOPY nginx/static.conf /etc/nginx/conf.dCOPY --from=builder /app/dist/app .首先,复制package.json和package-lock.json,然后安装依赖项,然后复制整个项目。结果是:

$ time docker build -t app .Sending build context to Docker daemon 608.8kBStep 1/12 : FROM node:12.16.2-alpine3.11 as builderStep 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++---> Using cacheStep 3/12 : WORKDIR /app---> Using cacheStep 4/12 : COPY package*.json ./---> Using cacheStep 5/12 : RUN npm ci---> Using cacheStep 6/12 : COPY . .Step 7/12 : RUN npm run build --prodDate: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Time: 38287ms---> 1b9448c73558Step 8/12 : FROM nginx:stable-alpineStep 9/12 : WORKDIR /app---> Using cacheStep 10/12 : RUN rm /etc/nginx/conf.d/default.conf---> Using cacheStep 11/12 : COPY nginx/static.conf /etc/nginx/conf.d---> Using cacheStep 12/12 : COPY --from=builder /app/dist/app .Successfully built a44dd7c217c3Successfully tagged app:latestreal 0m46.497suser 0m0.000ssys 0m0.000s该过程花费了46秒而不是3分钟,这要好得多!按正确的顺序排列图层很重要:首先,我们复制不变的图层,然后复制很少更改的图层,最后复制经常更改的图层。

接下来,让我们谈谈在CI CD系统中构建Docker镜像的方法。

五、使用以前的镜像作为缓存源如果我们使用某种SaaS解决方案来构建Docker镜像,则本地docker缓存可能绝对为空。我们需要给Docker一个先前构建的镜像,以便它准备就绪。

例如,让我们考虑在GitHub Actions中构建应用程序。我们将使用以下配置文件:

on:push:branches:- mastername: Test docker buildjobs:deploy:name: Buildruns-on: ubuntu-latestenv:IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/appIMAGE_TAG: ${{ github.sha }}steps:- name: Checkout uses: actions/checkout@v2- name: Login to GitHub Packages env: TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN- name: Build run: | docker build \ -t $IMAGE_NAME:$IMAGE_TAG \ -t $IMAGE_NAME:latest \ .- name: Push image to GitHub Packages run: | docker push $IMAGE_NAME:latest docker push $IMAGE_NAME:$IMAGE_TAG- name: Logout run: | docker logout docker.pkg.github.com构建镜像并将其推送到GitHub Packages花费了2分20秒:

现在,我们将更改构建配置,以便Docker将使用之前步骤中的缓存层:

on:push:branches:- mastername: Test docker buildjobs:deploy:name: Buildruns-on: ubuntu-latestenv:IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/appIMAGE_TAG: ${{ github.sha }}steps:- name: Checkout uses: actions/checkout@v2- name: Login to GitHub Packages env: TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | docker login docker.pkg.github.com -u $GITHUB_ACTOR -p $TOKEN- name: Pull latest images run: | docker pull $IMAGE_NAME:latest || true docker pull $IMAGE_NAME-builder-stage:latest || true- name: Images list run: | docker images- name: Build run: | docker build \ --target builder \ --cache-from $IMAGE_NAME-builder-stage:latest \ -t $IMAGE_NAME-builder-stage \ . docker build \ --cache-from $IMAGE_NAME-builder-stage:latest \ --cache-from $IMAGE_NAME:latest \ -t $IMAGE_NAME:$IMAGE_TAG \ -t $IMAGE_NAME:latest \ .- name: Push image to GitHub Packages run: | docker push $IMAGE_NAME-builder-stage:latest docker push $IMAGE_NAME:latest docker push $IMAGE_NAME:$IMAGE_TAG- name: Logout run: | docker logout docker.pkg.github.com在这里我们必须解释为什么我们需要两个构建命令。问题是在多阶段构建中,生成的镜像是最后一个阶段的一组图层。上一阶段的图层未包含在镜像中。因此,当使用来自先前构建的最终镜像时,Docker将无法找到准备就绪的层以使用Node.js构建镜像(构建器阶段)。为了解决此问题,我们创建了一个中间镜像 $ IMAGE_NAME-builder-stage,并将其发送到GitHub Packages,以便它可以用作后续构建的缓存源。

总构建时间减少到1.5分钟。花了半分钟来拉之前的镜像。

六、使用预构建的Docker镜像解决清晰的Docker缓存问题的另一种方法是将某些层移动到另一个Dockerfile,分别构建该镜像,将其推送到Container Registry,并将其用作父镜像。

让我们创建用于构建Angular应用程序的Node.js镜像。首先,我们在项目中创建一个Dockerfile.node:

FROM node:12.16.2-alpine3.11RUN apk --no-cache --update --virtual build-dependencies addpythonmakeg++然后,我们构建公共镜像并将其推送到Docker Hub:

docker build -t exsmund/node-for-angular -f Dockerfile.node .docker push exsmund/node-for-angular:latest现在,我们在主Dockerfile中使用完成的镜像:

FROM exsmund/node-for-angular:latest as builder...在此示例中,构建时间没有减少,但是如果有许多项目并且必须在所有项目中放置相同的依赖项,则预构建的镜像可能会很有用。

在本文中,我们研究了几种加快构建Docker镜像的方法。如果你想更快地部署,可以尝试下:

减少构建上下文的大小;

使用小的父镜像;

使用多阶段构建;

在Dockerfile中重新排序命令以有效利用缓存;

在CI / CD系统中配置缓存源;

使用预先构建的Docker镜像

我希望这些示例阐明Docker的工作方式,并且将能够使用我的技巧来优化你的部署。如果你想尝试本文中的示例,可参见以下存储库链接:

https : //github.com/devopsprodigy/test-docker-build。

书籍把我们引入最美好的社会,使我们认识各个时代的伟大智者。——史美尔斯

如果喜欢😍文章的话,点点关注,就差你的关注了,更多好玩有趣的云原生前沿技术尽在云原生CTO

相关探索