Avant qu'une fonctionnalité n'atteigne la production, dans notre époque d'orchestrateurs complexes et de CI/CD, elle doit passer par un long chemin depuis le commit jusqu'aux tests et à la livraison. Auparavant, il était possible de transférer de nouveaux fichiers par FTP (ce qui n'est plus la norme, n'est-ce pas ?), et le processus de "déploiement" ne prenait que quelques secondes. Maintenant, il faut créer une demande de fusion et attendre un certain temps avant que la fonctionnalité ne parvienne aux utilisateurs.
Une partie de ce parcours consiste à construire une image Docker. Parfois, la construction dure des minutes, parfois des dizaines de minutes, ce qui est difficilement acceptable. Dans cet article, nous prendrons une application simple que nous emballerons dans une image, appliquerons plusieurs méthodes pour accélérer la construction et examinerons les nuances de ces méthodes.

Nous avons une bonne expérience de création et de maintenance de sites d'information : , , , ⊠Récemment, nous avons complété notre portefeuille en lançant en production le site . Et pendant que nous peaufinions rapidement de nouvelles fonctionnalités et corrigions d'anciens bugs, le déploiement lent est devenu un gros problÚme.
Nous effectuons le déploiement sur GitLab. Nous construisons des images, les poussons dans le GitLab Registry et les déployons en production. Dans cette liste, le plus long est la construction des images. à titre d'exemple : sans optimisation, chaque construction du backend prenait 14 minutes.
![]()
Finalement, il est devenu clair qu'il était impossible de continuer ainsi, nous avons donc décidé d'explorer pourquoi les images prenaient autant de temps à se construire. Au final, nous avons réussi à réduire le temps de construction à 30 secondes !
![]()
Pour cet article, afin de ne pas ĂȘtre liĂ© Ă l'environnement de Reminder, nous examinerons un exemple de construction d'une application vide sur Angular. Donc, crĂ©ons notre application :
ng n appAjoutons-lui PWA (nous sommes progressistes) :
ng add @angular/pwa --project appPendant que des millions de paquets npm sont tĂ©lĂ©chargĂ©s, examinons comment fonctionne une image docker. Docker permet d'emballer des applications et de les exĂ©cuter dans un environnement isolĂ©, appelĂ© conteneur. GrĂące Ă l'isolation, il est possible d'exĂ©cuter plusieurs conteneurs en mĂȘme temps sur un mĂȘme serveur. Les conteneurs sont beaucoup plus lĂ©gers que les machines virtuelles, car ils s'exĂ©cutent directement sur le noyau du systĂšme. Pour exĂ©cuter un conteneur avec notre application, nous devons d'abord crĂ©er une image dans laquelle nous emballons tout ce qui est nĂ©cessaire au fonctionnement de notre application. En gros, une image est une copie de la structure du systĂšme de fichiers. Par exemple, prenons un Dockerfile :
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prodUn Dockerfile est un ensemble d'instructions ; en exécutant chacune d'elles, Docker sauvegardera les modifications dans le systÚme de fichiers et les appliquera aux précédentes. Chaque commande crée sa propre couche. L'image finale est la combinaison de toutes ces couches.
Ce qu'il est important de savoir : chaque couche peut ĂȘtre mise en cache par Docker. Si rien n'a changĂ© depuis la derniĂšre construction, Docker utilisera la couche dĂ©jĂ prĂ©parĂ©e au lieu d'exĂ©cuter la commande. Comme le principal gain de vitesse de construction proviendra de l'utilisation du cache, nous nous concentrerons sur la construction de l'image avec le cache dĂ©jĂ prĂȘt. Ainsi, de maniĂšre chronologique :
- Nous supprimons les images localement pour que les exécutions précédentes n'affectent pas le test.
docker rmi $(docker images -q) - Nous lançons la construction pour la premiÚre fois.
time docker build -t app . - Nous modifions le fichier src/index.html â nous simulons le travail d'un programmeur.
- Nous lançons la construction une deuxiÚme fois.
time docker build -t app .
Si l'environnement de construction des images est correctement configurĂ© (nous en parlerons un peu plus bas), Docker aura dĂ©jĂ un tas de caches disponibles lors du lancement de la construction. Notre objectif est d'apprendre Ă utiliser le cache afin que la construction se fasse le plus rapidement possible. Comme nous supposons que le lancement de la construction sans cache n'a lieu qu'une seule fois â la toute premiĂšre â nous pouvons ignorer la lenteur de ce premier lancement. Dans nos tests, ce qui compte, c'est la seconde exĂ©cution de la construction, lorsque les caches sont dĂ©jĂ radiĂ©s et que nous sommes prĂȘts Ă cuire notre gĂąteau. Cependant, quelques conseils auront Ă©galement un impact sur la premiĂšre construction.
Mettons le Dockerfile décrit ci-dessus dans le dossier du projet et lançons la construction. Tous les listings fournis ont été abrégés pour faciliter la lecture.
$ time docker build -t app .
Envoi du contexte de construction au démon Docker 409 Mo
Ătape 1/5 : FROM node:12.16.2
Statut : Image plus récente téléchargée pour node:12.16.2
Ătape 2/5 : WORKDIR /app
Ătape 3/5 : COPY . .
Ătape 4/5 : RUN npm ci
1357 paquets ajoutés en 22,47 s
Ătape 5/5 : RUN npm run build --prod
Date : 2020-04-16T19:20:09.664Z - Hash : fffa0fddaa3425c55dd3 - Temps : 37581ms
Construit avec succĂšs c8c279335f46
ĂtiquetĂ© avec succĂšs app:latest
réel 5m4.541s
utilisateur 0m0.000s
systÚme 0m0.000sNous modifions le contenu de src/index.html et lançons une deuxiÚme fois.
$ time docker build -t app .
Envoi du contexte de construction au démon Docker 409 Mo
Ătape 1/5 : FROM node:12.16.2
Ătape 2/5 : WORKDIR /app
---> Utilisation du cache
Ătape 3/5 : COPY . .
Ătape 4/5 : RUN npm ci
1357 paquets ajoutés en 22,47 s
Ătape 5/5 : RUN npm run build --prod
Date : 2020-04-16T19:26:26.587Z - Hash : fffa0fddaa3425c55dd3 - Temps : 37902ms
Construit avec succĂšs 79f335df92d3
ĂtiquetĂ© avec succĂšs app:latest
réel 3m33.262s
utilisateur 0m0.000s
systÚme 0m0.000sPour vérifier si notre image a été créée avec succÚs, exécutons la commande docker images:
RĂPERTOIRE TAG ID DE L'IMAGE CRĂĂ TAILLE
app latest 79f335df92d3 Il y a environ une minute 1.74 GoAvant de construire, Docker prend tous les fichiers dans le contexte actuel et les envoie Ă son dĂ©mon. Envoi du contexte de construction au dĂ©mon Docker 409 Mo. Le contexte de construction est spĂ©cifiĂ© comme le dernier argument de la commande build. Dans notre cas, il s'agit du rĂ©pertoire courant â «. » â et Docker tire tout ce que nous avons dans ce dossier. 409 Mo, c'est beaucoup : rĂ©flĂ©chissons Ă comment optimiser cela.
Réduisons le contexte
Pour rĂ©duire le contexte, il y a deux options. Soit nous plaçons tous les fichiers nĂ©cessaires Ă la construction dans un dossier sĂ©parĂ© et nous indiquons le contexte de Docker sur ce dossier. Cela peut ne pas toujours ĂȘtre pratique, donc il est possible de spĂ©cifier des exceptions : ce qui ne doit pas ĂȘtre inclus dans le contexte. Pour cela, nous allons crĂ©er un fichier .dockerignore et indiquer ce qui n'est pas nĂ©cessaire pour la construction :
.git
/node_moduleset nous allons relancer la construction :
$ time docker build -t app .
Envoi du contexte de construction au démon Docker 607.2 Ko
Ătape 1/5 : FROM node:12.16.2
Ătape 2/5 : WORKDIR /app
---> Utilisation du cache
Ătape 3/5 : COPY . .
Ătape 4/5 : RUN npm ci
Ajout de 1357 paquets en 22.47s
Ătape 5/5 : RUN npm run build --prod
Date : 2020-04-16T19:33:54.338Z - Hash : fffa0fddaa3425c55dd3 - Temps : 37313ms
Construit avec succĂšs 4942f010792a
Tagué avec succÚs app:latest
réel 1m47.763s
utilisateur 0m0.000s
systĂšme 0m0.000s607.2 Ko â bien mieux que 409 Mo. De plus, nous avons rĂ©duit la taille de l'image de 1.74 Ă 1.38 Go :
DĂPĂT TAG ID D'IMAGE CRĂĂ TAILLE
app latest 4942f010792a il y a 3 minutes 1.38GBEssayons encore de réduire la taille de l'image.
Utilisons Alpine
Une autre façon d'économiser sur la taille de l'image est d'utiliser une petite image parent. L'image parent est l'image sur laquelle notre image est construite. Le niveau inférieur est spécifié par la commande DE dans le Dockerfile. Dans notre cas, nous utilisons une image basée sur Ubuntu, qui a déjà Node.js installé. Et elle pÚse ...
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB... presque un gigaoctet. On peut réduire considérablement l'espace en utilisant une image basée sur Alpine Linux. Alpine est un Linux trÚs léger. L'image Docker pour Node.js basée sur Alpine ne pÚse que 88.5 Mo. Alors, remplaçons notre lourde image par :
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 --prodNous avons dû installer certaines choses nécessaires à la construction de l'application. Oui, Angular ne se construit pas sans Python ¯(°_o)\/¯
Mais au moins, la taille de l'image a diminué de 150 Mo :
DĂPĂT TAG ID D'IMAGE CRĂĂ TAILLE
app latest aa031edc315a il y a 22 minutes 761MBAllons plus loin.
Construction multi-étapes
Tout ce qui se trouve dans l'image n'est pas nécessaire en production.
$ 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.jsonAvec docker run app ls -lah Nous avons lancé un conteneur basé sur notre image app et avons exécuté la commande ls -lah, aprÚs quoi le conteneur a terminé son travail.
En production, nous avons seulement besoin du dossier dist. Cependant, les fichiers doivent ĂȘtre accessibles Ă l'extĂ©rieur. Nous pouvons dĂ©marrer un serveur HTTP sur Node.js. Mais nous allons simplifier les choses. Devinez le mot russe qui contient quatre lettres «Ń». Correct ! Đ«ĐœĐ¶ŃĐœŃĐșŃŃ. Prenons une image avec Nginx, mettons-y le dossier dist et une petite configuration :
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}Tout cela sera facilité par un multi-stage build. Modifions notre 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 .Maintenant, nous avons deux instructions DE dans le Dockerfile, chacune d'elles lance sa propre étape de construction. La premiÚre s'appelle builder, et à partir du dernier FROM, notre image finale sera préparée. En derniÚre étape, nous copions l'artéfact de notre construction de l'étape précédente dans l'image finale avec Nginx. La taille de l'image a été considérablement réduite :
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MBLançons un conteneur avec notre image et vérifions que tout fonctionne :
docker run -p8080:80 appAvec l'option -p8080:80, nous avons redirigĂ© le port 8080 de notre machine hĂŽte vers le port 80 Ă l'intĂ©rieur du conteneur, oĂč Nginx fonctionne. Ouvrons dans le navigateur et voyons notre application. Tout fonctionne !

Réduire la taille de l'image de 1,74 Go à 36 Mo diminue considérablement le temps de livraison de votre application en production. Mais revenons au temps de construction.
$ 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.000sChangeons l'ordre des couches
Les trois premiĂšres Ă©tapes ont Ă©tĂ© mises en cache (indice Using cache). Ă la quatriĂšme Ă©tape, tous les fichiers du projet sont copiĂ©s et Ă la cinquiĂšme, les dĂ©pendances sont installĂ©es RUN npm ci â un total de 47,338 s. Pourquoi installer les dĂ©pendances Ă chaque fois alors qu'elles changent trĂšs rarement ? Voyons pourquoi elles n'ont pas Ă©tĂ© mises en cache. En fait, Docker vĂ©rifie couche par couche si la commande et les fichiers associĂ©s ont changĂ©. Ă la quatriĂšme Ă©tape, nous copions tous les fichiers de notre projet et, bien sĂ»r, il y a des modifications, donc Docker ne prend pas seulement pas cette couche dans le cache, mais toutes les suivantes ! Faisons quelques petites modifications dans le 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 .D'abord, package.json et package-lock.json sont copiés, puis les dépendances sont installées, et seulement aprÚs cela, l'ensemble du projet est copié. En conséquence :
$ 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.000s46 secondes au lieu de 3 minutes â c'est bien mieux ! L'ordre des couches est crucial : nous copions d'abord ce qui ne change pas, puis ce qui change rarement, et enfin ce qui change souvent.
Ensuite, quelques mots sur la construction des images dans les systĂšmes CI/CD.
Utilisation des images précédentes pour le cache
Si nous utilisons une solution SaaS pour la construction, le cache Docker local peut ĂȘtre propre et frais. Pour que Docker puisse rĂ©cupĂ©rer des couches dĂ©jĂ cuites, donnez-lui l'image prĂ©cĂ©dente construite.
Prenons l'exemple de la construction de notre application dans GitHub Actions. Utilisons cette configuration.
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.comL'image se construit et se pousse dans GitHub Packages en deux minutes et 20 secondes :

Nous allons maintenant modifier la construction pour utiliser le cache basé sur les images précédemment construites :
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.comPour commencer, il faut expliquer pourquoi deux commandes sont lancĂ©es. buildEn effet, dans une construction multi-Ă©tapes, le rĂ©sultat sera un ensemble de couches du dernier stade. Les couches des stades prĂ©cĂ©dents ne seront pas incluses dans l'image. Ainsi, lorsque l'image finale de la construction prĂ©cĂ©dente est utilisĂ©e, Docker ne peut pas trouver les couches prĂȘtes pour construire l'image avec nodejs (stade builder). Pour rĂ©soudre ce problĂšme, une image intermĂ©diaire est créée. $IMAGE_NAME-builder-stage et est envoyĂ©e dans GitHub Packages afin de pouvoir ĂȘtre utilisĂ©e dans la construction suivante comme source de cache.

Le temps total de construction a été réduit à une minute et demie. Une demi-minute est consacrée à récupérer les images précédentes.
Création d'images préliminaires
Une autre façon de résoudre le problÚme de cache Docker propre est de déplacer une partie des couches dans un autre Dockerfile, de le construire séparément, de le pousser dans le Container Registry et de l'utiliser comme parent.
Créons notre propre image nodejs pour construire une application Angular. Créez un Dockerfile.node dans le projet.
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++Construisons et poussons l'image publique sur Docker Hub :
docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latestMaintenant, dans notre Dockerfile principal, utilisons l'image prĂȘte Ă l'emploi :
FROM exsmund/node-for-angular:latest as builder
...Dans notre exemple, le temps de construction n'a pas diminuĂ©, mais les images prĂ©-créées peuvent ĂȘtre utiles si vous avez de nombreux projets et que chacun d'eux nĂ©cessite les mĂȘmes dĂ©pendances.

Nous avons examiné plusieurs méthodes pour accélérer la construction des images Docker. Si vous souhaitez que le déploiement se fasse rapidement, essayez d'appliquer dans votre projet :
- la réduction du contexte ;
- l'utilisation de petites images parentales ;
- la construction multi-étapes ;
- le changement de l'ordre des instructions dans le Dockerfile pour utiliser efficacement le cache ;
- la configuration du cache dans les systĂšmes CI/CD ;
- la création préliminaire d'images.
J'espÚre que cet exemple clarifie le fonctionnement de Docker et que vous pourrez configurer votre déploiement de maniÚre optimale. Pour expérimenter avec les exemples de cet article, un dépÎt a été créé. .
Source : habr.com
