Voordat een functie in productie komt, moet het in deze tijden van complexe orkestrators en CI/CD een lange weg afleggen van commit tot tests en levering. Vroeger kon je nieuwe bestanden via FTP uploaden (dat doet tegenwoordig echt niemand meer, toch?) en het 'deployen' duurde seconden. Nu moet je een merge-request aanmaken en een aanzienlijke tijd wachten voordat de functie bij de gebruikers aankomt.
Een deel van deze weg is de bouw van de Docker-afbeelding. Soms duurt de bouw minuten, soms zelfs tientallen minuten, wat moeilijk als normaal te beschouwen is. In dit artikel nemen we een eenvoudige applicatie die we in een afbeelding verpakt, passen we een paar methoden toe om de bouw te versnellen en bespreken we de nuances van deze methoden.

We hebben een goede ervaring met het maken en onderhouden van nieuwswebsites: , , , … Niet zo lang geleden hebben we ons portfolio uitgebreid door de livegang van de site . En terwijl we snel nieuwe functies implementeerden en oude bugs oplosten, werd de trage deploy een groot probleem.
We doen de deploy op GitLab. We bouwen de afbeeldingen, pushen ze naar de GitLab Registry en rollen ze uit in productie. Het langste gedeelte in deze lijst is de bouw van de afbeeldingen. Ter illustratie: zonder optimalisatie duurde elke backend-bouw 14 minuten.
![]()
Uiteindelijk werd duidelijk dat we zo niet verder konden, en we gingen ons verdiepen in waarom de afbeeldingen zoveel tijd nodig hadden voor de bouw. Uiteindelijk is het gelukt om de bouwtijd tot 30 seconden te verkorten!
![]()
Voor dit artikel, om ons niet aan de omgeving van Reminder te binden, bekijken we een voorbeeld van het bouwen van een lege applicatie op Angular. Dus, laten we onze applicatie maken:
ng n appWe voegen PWA toe (we zijn tenslotte progressief):
ng add @angular/pwa --project appTerwijl miljoenen npm-pakketten worden gedownload, laten we eens kijken hoe een docker-afbeelding is opgebouwd. Docker biedt de mogelijkheid om applicaties te verpakken en in een geïsoleerde omgeving, die container wordt genoemd, uit te voeren. Dankzij deze isolatie kunnen veel containers tegelijkertijd op één server draaien. Containers zijn aanzienlijk lichter dan virtuele machines, omdat ze rechtstreeks op de systeemkern draaien. Om een container met onze applicatie te starten, moeten we eerst een afbeelding maken waarin we alles verpakken wat nodig is voor onze applicatie om te functioneren. In wezen is een afbeelding een afdruk van het bestandssysteem. Laten we bijvoorbeeld een Dockerfile nemen:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prodDockerfile is een set instructies; wanneer elk ervan wordt uitgevoerd, zal Docker de wijzigingen in het bestandssysteem opslaan en deze toepassen op de vorige. Elke opdracht creëert zijn eigen laag. Een kant-en-klaar image is een combinatie van deze lagen.
Belangrijk om te weten: elke laag kan gecacheerd worden door Docker. Als er niets is veranderd sinds de vorige build, dan zal Docker in plaats van de opdracht uit te voeren de reeds bestaande laag gebruiken. Aangezien de grootste winst in bouwsnelheid zal komen door het gebruik van de cache, zullen we bij het meten van de bouwsnelheid specifiek letten op de build met de opgebouwde cache. Laten we de stappen doornemen:
- We verwijderen lokaal de images, zodat eerdere runs geen invloed hebben op de test.
docker rmi $(docker images -q) - We starten de build voor de eerste keer.
time docker build -t app . - We wijzigen het bestand src/index.html - we simuleren het werk van een programmeur.
- We starten de build voor de tweede keer.
time docker build -t app .
Als de omgeving voor het bouwen van images goed is ingesteld (waarover iets verderop meer), dan zal Docker bij het starten van de build al een aantal caches hebben. Onze taak is om te leren de cache zo te gebruiken dat de build zo snel mogelijk gaat. Aangezien we aannemen dat de build zonder cache maar één keer plaatsvindt — de eerste keer — kunnen we negeren hoe langzaam deze eerste keer was. In onze tests is de tweede build belangrijk, wanneer de caches al warm zijn en we klaar zijn om onze taart te bakken. Desondanks zullen sommige tips ook invloed hebben op de eerste build.
We plaatsen de hierboven beschreven Dockerfile in de projectmap en starten de build. Alle gegeven listings zijn ingekort voor een betere leesbaarheid.
$ time docker build -t app .
Sending build context to Docker daemon 409MB
Step 1/5 : FROM node:12.16.2
Status: Nieuwere image gedownload voor node:12.16.2
Step 2/5 : WORKDIR /app
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
1357 packages toegevoegd in 22.47s
Step 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Tijd: 37581ms
Succesvol gebouwd c8c279335f46
Succesvol getagd app:latest
real 5m4.541s
user 0m0.000s
sys 0m0.000sWe wijzigen de inhoud van src/index.html en voeren de build voor de tweede keer uit.
$ 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
---> Gebruik makend van cache
Step 3/5 : COPY . .
Step 4/5 : RUN npm ci
1357 packages toegevoegd in 22.47s
Step 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Tijd: 37902ms
Succesvol gebouwd 79f335df92d3
Succesvol getagd app:latest
real 3m33.262s
user 0m0.000s
sys 0m0.000sOm te zien of we onze image hebben gekregen, voeren we de opdracht uit docker images:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 Ongeveer een minuut geleden 1.74GBVoordat Docker bouwt, neemt het alle bestanden in de huidige context en stuurt deze naar zijn daemon. Bouwcontext verzenden naar Docker daemon 409MB. De context voor de bouw wordt opgegeven als het laatste argument van het commando build. In ons geval is dit de huidige directory — «.», — en Docker sleurt alles mee wat in deze map staat. 409 MB is veel: laten we nadenken over hoe we dit kunnen verhelpen.
We verkleinen de context.
Om de context te verkleinen, zijn er twee opties. Ofwel plaatsen we alle bestanden die nodig zijn voor de bouw in een aparte map en geven we deze map aan Docker als context. Dit kan soms onhandig zijn, daarom is er de mogelijkheid om uitzonderingen op te geven: wat niet mee moet worden genomen in de context. Hiervoor plaatsen we een bestand .dockerignore in het project en geven we op wat niet nodig is voor de bouw:
.git
/node_modulesen starten we de bouw opnieuw:
$ time docker build -t app .
Bouwcontext verzenden naar Docker daemon 607.2kB
Stap 1/5 : VAN node:12.16.2
Stap 2/5 : WORKDIR /app
---> Cache gebruiken
Stap 3/5 : KOPIEER . .
Stap 4/5 : RUN npm ci
1357 pakketten toegevoegd in 22.47s
Stap 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Tijd: 37313ms
Met succes gebouwd 4942f010792a
Met succes getagd app:latest
real 1m47.763s
user 0m0.000s
sys 0m0.000s607.2 Kbyte — veel beter dan 409 MB. En bovendien hebben we de grootte van het image verminderd van 1.74 naar 1.38GB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 4942f010792a 3 minuten geleden 1.38GBLaten we proberen om de grootte van het image verder te verminderen.
We gebruiken Alpine.
Een andere manier om te besparen op de grootte van het image is het gebruik van een klein ouder image. Het ouder image is de basis waarop ons image is gebouwd. De onderste laag wordt opgegeven met het commando FROM in de Dockerfile. In ons geval gebruiken we een image gebaseerd op Ubuntu, dat al nodejs bevat. En het weegt ...
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minuten geleden 916MB… bijna een gigabyte. We kunnen de grootte aanzienlijk verminderen door een image op basis van Alpine Linux te gebruiken. Alpine is een zeer kleine Linux-distributie. De Docker-image voor nodejs op basis van Alpine weegt slechts 88.5 MB. Laten we daarom ons zware image vervangen:
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 --prodWe moesten een paar dingen installeren die nodig zijn voor het bouwen van de applicatie. Ja, Angular bouwt niet zonder Python ¯(°_o)\/¯
Maar de grootte van het image is met 150 MB verminderd:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest aa031edc315a 22 minuten geleden 761MBLaten we nog een stap verder gaan.
Multi-stage build.
Niet alles wat in het image zit, is nodig voor onze productie.
$ 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.jsonMet docker run app ls -lah we hebben een container gestart op basis van ons image app en hebben daar een commando in uitgevoerd ls -lah, waarna de container zijn werk beëindigde.
Op productie hebben we alleen de map dist. De bestanden moeten op de een of andere manier extern worden aangeboden. We kunnen een HTTP-server op nodejs starten. Maar we doen het eenvoudiger. Raad het Russische woord dat vier letters 'ы' heeft. Juist! Ынжыныксы. Laten we een image pakken met nginx en deze map toevoegen dist en een kleine configuratie:
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}Dit alles helpt ons met multi-stage build. Laten we ons Dockerfile aanpassen:
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 .Nu hebben we twee instructies FROM in het Dockerfile, elk voert zijn eigen bouwfase uit. De eerste hebben we genoemd , bedoeld voor het samenstellen van pakketten en distributies. Voor aarch64 is een live-variant samengesteld, gelijk aan x86_64; voor beide architecturen zijn rootfs-afbeeldingen voor RPi4 en qemu-afbeeldingen samengesteld., maar vanaf de laatste FROM zal ons eindimage worden voorbereid. In de laatste stap kopiëren we het artifact van onze build in de vorige fase naar het eindimage met nginx. De grootte van het image is aanzienlijk verminderd:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MBLaten we de container met ons image starten en bevestigen dat alles werkt:
docker run -p8080:80 appMet de optie -p8080:80 hebben we poort 8080 op onze hostmachine doorgeschakeld naar poort 80 binnen de container, waar nginx draait. We openen in de browser en zien onze applicatie. Alles werkt!

Het verminderen van de grootte van het image van 1,74 GB naar 36 MB verkort aanzienlijk de tijd die nodig is om uw applicatie in productie te brengen. Maar laten we teruggaan naar de bouwtijd.
$ 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.000sWe veranderen de volgorde van de lagen
De eerste drie stappen waren gecached (hint Using cache). In de vierde stap worden alle projectbestanden gekopieerd en in de vijfde stap worden de afhankelijkheden geïnstalleerd RUN npm ci — in totaal 47,338 seconden. Waarom elke keer opnieuw afhankelijkheden installeren, als ze zelden veranderen? Laten we eens kijken waarom ze niet gecached zijn. Het komt doordat Docker laag voor laag controleert of de commando's en de bijbehorende bestanden zijn gewijzigd. In de vierde stap kopiëren we alle bestanden van ons project, en daar zijn natuurlijk wijzigingen bij, dus Docker haalt niet alleen deze laag niet uit de cache, maar ook alle daaropvolgende! Laten we een paar kleine wijzigingen aanbrengen in het 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 .Eerst worden package.json en package-lock.json gekopieerd, daarna worden de afhankelijkheden geïnstalleerd, en pas daarna wordt het volledige project gekopieerd. Als resultaat:
$ time docker build -t app .
Verzendt build context naar Docker daemon 608.8kB
Stap 1/12 : FROM node:12.16.2-alpine3.11 as builder
Stap 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Using cache
Stap 3/12 : WORKDIR /app
---> Using cache
Stap 4/12 : COPY package*.json ./
---> Using cache
Stap 5/12 : RUN npm ci
---> Using cache
Stap 6/12 : COPY . .
Stap 7/12 : RUN npm run build --prod
Datum: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Tijd: 38287ms
---> 1b9448c73558
Stap 8/12 : FROM nginx:stable-alpine
Stap 9/12 : WORKDIR /app
---> Using cache
Stap 10/12 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
Stap 11/12 : COPY nginx/static.conf /etc/nginx/conf.d
---> Using cache
Stap 12/12 : COPY --from=builder /app/dist/app .
Met succes gebouwd a44dd7c217c3
Met succes gelabeld app:latest
reëel 0m46.497s
gebruiker 0m0.000s
sys 0m0.000s46 seconden in plaats van 3 minuten — aanzienlijk beter! De juiste volgorde van lagen is belangrijk: eerst kopiëren we wat niet verandert, dan wat zelden verandert, en tenslotte wat vaak verandert.
Vervolgens een paar woorden over het bouwen van images in CI/CD-systemen.
Het gebruik van eerdere images voor caching
Als we een SaaS-oplossing gebruiken voor de bouw, kan de lokale Docker-cache schoon en fris zijn. Om Docker een plek te geven om gebakken lagen van te nemen, geef hem de vorige gebouwde afbeelding.
Laten we als voorbeeld de bouw van onze applicatie in GitHub Actions bekijken. We gebruiken zo'n configuratie.
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.comHet beeld wordt binnen twee minuten en 20 seconden opgebouwd en gepusht naar GitHub Packages:

Laten we nu de build wijzigen, zodat de cache op basis van eerder gebouwde images wordt gebruikt:
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.comEerst moeten we uitleggen waarom er twee opdrachten worden uitgevoerd. buildDit komt omdat in een multi-stage build de resulterende image een set lagen van de laatste stage zal zijn. De lagen van de vorige stages worden echter niet in de image opgenomen. Daarom kan Docker bij gebruik van de finale image uit de vorige build geen gereed lagen vinden voor het bouwen van de image met nodejs (builder stage). Om dit probleem op te lossen, wordt er een tussen-image gemaakt. $IMAGE_NAME-builder-stage en deze wordt naar GitHub Packages gepusht, zodat deze in de volgende build als cachebron kan worden gebruikt.

De totale bouwtijd is verminderd tot anderhalve minuut. Dertig seconden worden besteed aan het ophalen van eerdere images.
Voorlopig bouwen van images
Een andere manier om het probleem van een schone Docker-cache op te lossen, is door delen van de lagen naar een andere Dockerfile te verplaatsen, deze apart te bouwen, naar de Container Registry te pushen en als basis te gebruiken.
We maken onze eigen Node.js-image voor het bouwen van een Angular-applicatie. Maak een Dockerfile.node in het project.
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++Bouw en push een openbaar image naar Docker Hub:
docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latestNu gebruiken we het kant-en-klare image in onze hoofd Dockerfile:
FROM exsmund/node-for-angular:latest as builder
...In ons voorbeeld is de bouwtijd niet verkort, maar vooraf gebouwde images kunnen nuttig zijn als je veel projecten hebt en in elk dezelfde afhankelijkheden moet installeren.

We hebben verschillende methoden bekeken om het bouwen van Docker-images te versnellen. Als je wilt dat de deployment snel verloopt, probeer dan in je project het volgende:
- de context te verkleinen;
- kleine basis-images te gebruiken;
- multi-stage builds;
- de volgorde van instructies in de Dockerfile te wijzigen om de cache efficiënt te gebruiken;
- de cache in CI/CD-systemen in te stellen;
- vooraf gebouwde images.
Ik hoop dat het voorbeeld duidelijk maakt hoe Docker werkt en dat je je deployment optimaal kunt instellen. Voor degenen die met de voorbeelden uit het artikel willen experimenteren, is er een repository aangemaakt. .
Bron: habr.com
