在功能投入生產之前,在複雜的協調器和 CI/CD 的時代,從提交到測試和交付還有很長的路要走。以前,您可以透過 FTP 上傳新檔案(現在已經沒有人這樣做了,對吧?),「部署」過程只需幾秒鐘。現在您需要建立合併請求並等待很長時間才能讓該功能到達使用者手中。
此路徑的一部分是建立 Docker 映像。有時集會持續幾分鐘,有時長達數十分鐘,這很難說是正常的。在本文中,我們將採用一個簡單的應用程序,將其打包到圖像中,應用多種方法來加速構建,並了解這些方法如何工作的細微差別。
我們在創建和支援媒體網站方面擁有豐富的經驗:
我們部署到 GitLab。我們收集圖像,將它們推送到 GitLab 註冊表並將其投入生產。這個清單中最長的事情是組裝圖像。例如:在沒有最佳化的情況下,每個後端建置需要 14 分鐘。
最後,我們清楚地意識到我們不能再這樣生活了,我們坐下來思考為什麼要花這麼長時間來收集圖像。結果,我們成功地將組裝時間縮短至 30 秒!
在本文中,為了不被 Reminder 的環境所束縛,我們來看看一個組裝空 Angular 應用程式的範例。那麼,讓我們創建我們的應用程式:
ng n app
新增 PWA(我們是漸進式的):
ng add @angular/pwa --project app
當一百萬個 npm 套件被下載時,讓我們弄清楚 docker 映像像是如何運作的。 Docker 提供了打包應用程式並在稱為容器的隔離環境中運行它們的能力。由於隔離,您可以在一台伺服器上同時執行多個容器。容器比虛擬機器輕得多,因為它們直接運行在系統核心上。要使用我們的應用程式運行容器,我們首先需要建立一個映像,在其中打包應用程式運行所需的所有內容。本質上,映像是檔案系統的副本。以 Dockerfile 為例:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
Dockerfile 是一組指令;透過執行每一個操作,Docker 都會將變更儲存到檔案系統中,並將它們覆蓋在先前的變更上。每個團隊創建自己的層。最終的圖像是層層組合在一起的。
需要了解的重要一點是:每個 Docker 層都可以快取。如果自上次建置以來沒有任何變化,那麼 docker 將採用現成的層,而不是執行命令。由於建置速度的主要成長將歸因於快取的使用,因此在測量建置速度時,我們將特別注意使用現成的快取建置影像。所以,一步一步:
- 我們在本地刪除圖像,以便先前的運行不會影響測試。
docker rmi $(docker images -q)
- 我們第一次啟動建置。
time docker build -t app .
- 我們更改 src/index.html 檔案 - 我們模仿程式設計師的工作。
- 我們第二次運行建置。
time docker build -t app .
如果建置映像的環境配置正確(更多內容請見下文),那麼當建置開始時,Docker 已經擁有了一堆快取。我們的任務是學習如何使用緩存,以便構建盡快進行。由於我們假設在沒有快取的情況下運行建置僅發生一次(第一次),因此我們可以忽略第一次的速度有多慢。在測試中,當快取已經預熱並且我們準備好烘烤蛋糕時,建造的第二次運行對我們來說很重要。然而,一些技巧也會影響第一次建造。
讓我們將上面描述的 Dockerfile 放入專案資料夾中並開始建置。為了方便閱讀,所有清單都經過壓縮。
$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Status: Downloaded newer image for node:12.16.2
Step 2/5 : WORKDIR /app
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Time: 37581ms
Successfully built c8c279335f46
Successfully tagged app:latest
real 5m4.541s
user 0m0.000s
sys 0m0.000s
我們更改 src/index.html 的內容並再次運行它。
$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Time: 37902ms
Successfully built 79f335df92d3
Successfully tagged app:latest
real 3m33.262s
user 0m0.000s
sys 0m0.000s
要查看我們是否有圖像,請運行命令 docker images
:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 About a minute ago 1.74GB
在建置之前,docker 會取得當前上下文中的所有檔案並將它們傳送到其守護進程 Sending build context to Docker daemon 409MB
。建構上下文被指定為建構指令的最後一個參數。在我們的範例中,這是目前目錄 - “.”, - Docker 會將我們擁有的所有內容拖曳到此資料夾中。 409 MB 太大了:讓我們考慮一下如何解決它。
減少情境
為了減少上下文,有兩種選擇。或將組裝所需的所有檔案放在一個單獨的資料夾中,並將 docker 上下文指向該資料夾。這可能並不總是很方便,因此可以指定例外:不應將哪些內容拖曳到上下文中。為此,請將 .dockerignore 檔案放入專案中並指出建置不需要的內容:
.git
/node_modules
並再次運行建置:
$ time docker build -t app .
Sending build context to Docker daemon 607.2kB
Step 1/5 : FROM node:12.16.2
Step 2/5 : WORKDIR /app
---> Using cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
added 1357 packages in 22.47s
Step 5/5 : RUN npm run build --prod
Date: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Time: 37313ms
Successfully built 4942f010792a
Successfully tagged app:latest
real 1m47.763s
user 0m0.000s
sys 0m0.000s
607.2 KB 比 409 MB 好得多。我們也將影像大小從 1.74 GB 減少到 1.38 GB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 4942f010792a 3 minutes ago 1.38GB
讓我們嘗試進一步減小圖像的大小。
我們用阿爾派
節省圖像大小的另一種方法是使用較小的父圖像。父母的形像是我們的形象準備的基礎上的形象。底層由指令指定 FROM
在 Dockerfile 中。在我們的例子中,我們使用的是已經安裝了nodejs的基於Ubuntu的映像。而且它的重量...
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB
……幾乎是千兆位元組。使用基於Alpine Linux的鏡像可以顯著減少體積。 Alpine 是一個非常小的Linux。基於 alpine 的 Nodejs 的 docker 映像僅重 88.5 MB。因此,讓我們更換房屋中的活潑形象:
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
我們必須安裝一些建立應用程式所需的東西。是的,沒有 Python 就無法建構 Angular ¯(°_o)/¯
但影像大小下降到 150 MB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest aa031edc315a 22 minutes ago 761MB
讓我們更進一步。
多級組裝
並非圖像中的所有內容都是我們在生產中需要的。
$ docker run app ls -lah
total 576K
drwxr-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 browserslist
drwxr-xr-x 3 root root 4.0K Apr 16 19:54 dist
drwxr-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.json
drwxr-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.json
drwxr-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
我們根據我們的鏡像啟動了一個容器 app
並執行其中的命令 ls -lah
,之後容器完成其工作。
在生產中我們只需要一個資料夾 dist
。在這種情況下,文件需要以某種方式提供給外部。你可以在nodejs上運行一些HTTP伺服器。但我們會讓事情變得更容易。猜猜一個有四個字母“y”的俄語單字。正確的! Ynzhynyksy。讓我們用 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 builder
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod
FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .
現在我們有兩個指令 FROM
在 Dockerfile 中,它們每個都執行不同的建置步驟。我們叫第一個 builder
,但從最後一個 FROM 開始,我們的最終影像將會準備好。最後一步是使用 nginx 將上一個步驟中的組件複製到最終映像中。影像的大小顯著減小:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MB
讓我們用我們的鏡像運行容器並確保一切正常:
docker run -p8080:80 app
使用 -p8080:80 選項,我們將主機上的連接埠 8080 轉送到執行 nginx 的容器內的連接埠 80。在瀏覽器中開啟
將映像大小從 1.74 GB 減小到 36 MB 可以大幅縮短將應用程式交付到生產環境所需的時間。但讓我們回到集合時間。
$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/11 : FROM node:12.16.2-alpine3.11 as builder
Step 2/11 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Using cache
Step 3/11 : WORKDIR /app
---> Using cache
Step 4/11 : COPY . .
Step 5/11 : RUN npm ci
added 1357 packages in 47.338s
Step 6/11 : RUN npm run build --prod
Date: 2020-04-16T21:16:03.899Z - Hash: fffa0fddaa3425c55dd3 - Time: 39948ms
---> 27f1479221e4
Step 7/11 : FROM nginx:stable-alpine
Step 8/11 : WORKDIR /app
---> Using cache
Step 9/11 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
Step 10/11 : COPY nginx/static.conf /etc/nginx/conf.d
---> Using cache
Step 11/11 : COPY --from=builder /app/dist/app .
Successfully built d201471c91ad
Successfully tagged app:latest
real 2m17.700s
user 0m0.000s
sys 0m0.000s
更改圖層順序
我們的前三個步驟已被快取(提示 Using cache
)。第四步,複製所有專案文件,第五步安裝依賴項 RUN npm ci
- 長達 47.338 秒。如果依賴項很少發生變化,為什麼每次都要重新安裝相依性呢?讓我們找出為什麼它們沒有被快取。重點是,Docker 會逐層檢查命令以及與其關聯的檔案是否發生了變化。第四步,我們複製了我們專案的所有文件,其中當然也有變化,所以Docker不僅不會從快取中取出這一層,還包括所有後續的!讓我們對 Dockerfile 進行一些小更改。
FROM node:12.16.2-alpine3.11 as builder
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build --prod
FROM nginx:1.17.10-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/static.conf /etc/nginx/conf.d
COPY --from=builder /app/dist/app .
首先,複製package.json和package-lock.json,然後安裝依賴項,然後複製整個專案。因此:
$ time docker build -t app .
Sending build context to Docker daemon 608.8kB
Step 1/12 : FROM node:12.16.2-alpine3.11 as builder
Step 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Using cache
Step 3/12 : WORKDIR /app
---> Using cache
Step 4/12 : COPY package*.json ./
---> Using cache
Step 5/12 : RUN npm ci
---> Using cache
Step 6/12 : COPY . .
Step 7/12 : RUN npm run build --prod
Date: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Time: 38287ms
---> 1b9448c73558
Step 8/12 : FROM nginx:stable-alpine
Step 9/12 : WORKDIR /app
---> Using cache
Step 10/12 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
Step 11/12 : COPY nginx/static.conf /etc/nginx/conf.d
---> Using cache
Step 12/12 : COPY --from=builder /app/dist/app .
Successfully built a44dd7c217c3
Successfully tagged app:latest
real 0m46.497s
user 0m0.000s
sys 0m0.000s
46 秒而不是 3 分鐘 - 好多了!正確的層順序很重要:先複製不變的內容,然後複製很少變化的內容,最後複製經常變化的內容。
接下來,簡單介紹一下在 CI/CD 系統中組裝鏡像的情況。
使用以前的圖像進行快取
如果我們使用某種 SaaS 解決方案進行構建,那麼本地 Docker 快取可能會是乾淨且新鮮的。為了給docker一個地方來獲取烘焙層,給他之前建造的映像。
讓我們以在 GitHub Actions 中建立應用程式為例。我們使用這個配置
on:
push:
branches:
- master
name: Test docker build
jobs:
deploy:
name: Build
runs-on: ubuntu-latest
env:
IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
IMAGE_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
該鏡像在 20 分 XNUMX 秒內完成組裝並推送到 GitHub Packages:
現在讓我們更改構建,以便基於先前構建的圖像使用快取:
on:
push:
branches:
- master
name: Test docker build
jobs:
deploy:
name: Build
runs-on: ubuntu-latest
env:
IMAGE_NAME: docker.pkg.github.com/${{ github.repository }}/app
IMAGE_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
首先我們要告訴你為什麼要啟動兩個指令 build
。事實上,在多階段組裝中,產生的影像將是來自最後階段的一組圖層。在這種情況下,先前圖層中的圖層將不會包含在影像中。因此,當使用先前建置的最終映像時,Docker 將無法找到準備好的層來使用 nodejs(建置器階段)建置映像。為了解決這個問題,創建了中間圖像 $IMAGE_NAME-builder-stage
並被推送到 GitHub Packages,以便可以在後續建置中用作快取來源。
總組裝時間減少至一分半鐘。花了半分鐘來調出以前的影像。
預成像
解決乾淨 Docker 快取問題的另一種方法是將某些層移至另一個 Dockerfile 中,單獨建置它,將其推送到容器註冊表中並將其用作父級。
我們創建自己的 Nodejs 映像來建立 Angular 應用程式。在專案中建立Dockerfile.node
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++
我們在 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 的工作原理,並且您將能夠以最佳方式配置您的部署。為了使用本文中的範例,創建了一個儲存庫
來源: www.habr.com