Før en feature går i produktion, skal den i dag gennem en lang vej fra commit til tests og deployment i en verden med komplekse orkestratorer og CI/CD. Tidligere kunne man blot uploade nye filer via FTP (det gør ingen længere, vel?), og deployeringsprocessen tog sekunder. Nu skal man oprette en merge request og vente en del tid, før funktionen når brugerne.
En del af denne rejse er opbygningen af Docker-billedet. Nogle gange tager opbygningen minutter, nogle gange - titusinder af minutter, hvilket er svært at kalde normalt. I denne artikel vil vi tage en simpel applikation, pakke den ind i et billede, anvende forskellige metoder til at fremskynde opbygningen og se på nuancerne i disse metoder.

Vi har en god erfaring med at oprette og supportere nyhedswebsites: , , , … For nylig udvidede vi vores portefølje ved at lancere et website i produktion. . Og mens vi hurtigt færdiggjorde nye features og rettede gamle fejl, blev den langsomme deployment et stort problem.
Vi foretager deployment på GitLab. Vi bygger billeder, pusher dem til GitLab Registry og udruller dem i produktion. Det, der tager længst tid i denne proces, er opbygningen af billederne. Som eksempel: uden optimering tog hver backend-opbygning 14 minutter.
![]()
Til sidst blev det klart, at vi ikke kunne fortsætte sådan, så vi satte os ned for at finde ud af, hvorfor billederne tog så lang tid at opbygge. I sidste ende lykkedes det os at reducere opbygningstiden til 30 sekunder!
![]()
Til denne artikel, for ikke at knytte os til Reminder-miljøet, ser vi på et eksempel på opbygning af en tom applikation på Angular. Så lad os oprette vores applikation:
ng n appVi tilføjer PWA (vi er jo progressive):
ng add @angular/pwa --project appMens millioner af npm-pakker downloades, lad os se på, hvordan docker-billedet er opbygget. Docker giver mulighed for at pakke applikationer og køre dem i et isoleret miljø kaldet en container. Takket være isolation kan man køre mange containere samtidigt på en server. Containere er betydeligt lettere end virtuelle maskiner, da de kører direkte på systemets kerne. For at køre en container med vores applikation skal vi først oprette et billede, hvor vi pakker alt, hvad der er nødvendigt for at køre vores applikation. I bund og grund er et billede et aftryk af filsystemet. For eksempel, lad os tage Dockerfile:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prodDockerfile er et sæt af instruktioner; ved at udføre hver enkelt vil Docker gemme ændringer i filsystemet og anvende dem på de tidligere. Hver kommando skaber sit lag. Det færdige billede er en samling af lagene.
Det er vigtigt at vide: hvert lag kan Docker cache. Hvis intet har ændret sig siden den sidste bygning, vil Docker tage det allerede færdige lag i stedet for at udføre kommandoen. Da den vigtigste hastighedsforøgelse i bygningen vil være gennem brug af cache, vil vi i målinger af byggehastighed særligt fokusere på bygningen af billedet med den eksisterende cache. Så lad os gå i gang:
- Vi fjerner billeder lokalt, så tidligere kørsel ikke påvirker testen.
docker rmi $(docker images -q) - Vi kører bygningen første gang.
time docker build -t app . - Vi ændrer filen src/index.html — simulerer programmørens arbejde.
- Vi kører bygningen anden gang.
time docker build -t app .
Hvis miljøet for at bygge billeder er korrekt konfigureret (som beskrevet lidt senere), vil Docker allerede have en masse caches tilgængelige, når bygningen startes. Vores opgave er at lære at bruge cachen, så bygningen forløber så hurtigt som muligt. Da vi antager, at bygningen uden cache kun sker én gang — den allerførste — kan vi ignorere, hvor langsom denne første gang var. I vores tests er den anden bygning vigtig, når cachen allerede er klar, og vi er klar til at bage vores kage. Ikke desto mindre vil nogle råd også påvirke den første bygning.
Lad os placere Dockerfile, som beskrevet ovenfor, i projektmappen og starte bygningen. Alle nævnte lister er forkortet for at lette læsningen.
$ 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.000sVi ændrer indholdet i src/index.html og kører den anden gang.
$ 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.000sFor at se, om vi har fået billedet, kører vi kommandoen docker images:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 For omkring et minut siden 1.74GBFør Docker bygger, tager den alle filer i den nuværende kontekst og sender dem til sin demon Sender byggekontext til Docker-demon 409MB. Bygge konteksten angives som det sidste argument i build-kommandoen. I vores tilfælde er det den nuværende mappe — «.», — og Docker trækker alt, hvad vi har i denne mappe. 409 MB er meget: lad os overveje, hvordan vi kan løse dette.
Reducerer konteksten
For at reducere konteksten er der to muligheder. Enten læg alle filer, der er nødvendige for bygningen, i en separat mappe og angiv konteksten til Docker specifikt til den mappe. Dette kan ikke altid være bekvemt, så der er mulighed for at angive undtagelser: hvad der ikke skal medtages i konteksten. For at gøre dette lægger vi en .dockerignore-fil i projektet og angiver, hvad der ikke er nødvendigt til bygningen:
.git
/node_modulesog vi kører bygningen igen:
$ time docker build -t app .
Sender byggekontext til Docker-demon 607.2kB
Trin 1/5: FROM node:12.16.2
Trin 2/5: WORKDIR /app
---> Bruger cache
Trin 3/5: COPY . .
Trin 4/5: RUN npm ci
tilføjet 1357 pakker på 22.47s
Trin 5/5: RUN npm run build --prod
Dato: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Tid: 37313ms
Succesfuldt bygget 4942f010792a
Succesfuldt mærket app:latest
real 1m47.763s
user 0m0.000s
sys 0m0.000s607.2 Kbyte — meget bedre end 409 MB. Og vi har også reduceret størrelsen af billedet fra 1.74 til 1.38GB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 4942f010792a 3 minutter siden 1.38GBLad os prøve at reducere billedstørrelsen yderligere.
Bruger Alpine
En anden måde at spare på billedstørrelsen — er at bruge et lille forældrebillede. Forældrebilledet er det billede, vores billede er baseret på. Den nederste lag angives med kommandoen FROM i Dockerfile. I vores tilfælde er vi ved at bruge et billede baseret på Ubuntu, som allerede har nodejs installeret. Og det vejer …
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutter siden 916MB… næsten en gigabyte. Man kan betydeligt reducere størrelsen ved at bruge et billede baseret på Alpine Linux. Alpine er en meget lille Linux. Docker-billedet for nodejs baseret på Alpine vejer kun 88.5 MB. Så lad os erstatte vores fede forældrebillede med:
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 --prodVi måtte installere nogle ting, der er nødvendige for at bygge applikationen. Ja, Angular bygger ikke uden Python ¯(°_o)¯
Men billedstørrelsen faldt med 150 MB:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest aa031edc315a 22 minutter siden 761MBVi går videre.
Multi-stage bygning
Ikke alt, hvad der er i billedet, er nødvendigt for os i produktionen.
$ 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.jsonVed hjælp af docker run app ls -lah Vi har startet en container baseret på vores billede app og udført kommandoen i den ls -lah, hvorefter containeren blev stoppet.
I produktionen har vi kun brug for mappen dist. Files skal deles på en eller anden måde. Vi kan starte en HTTP-server med nodejs. Men vi gør det lettere. Gæt det russiske ord, der har fire bogstaver 'ы'. Rigtigt! Ынжыныксы. Vi tager et billede med nginx, lægger mappen dist og en lille konfiguration:
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}Multi-stage build vil hjælpe os med dette. Lad os ændre vores 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 .Nu har vi to instruktioner FROM i Dockerfile, hver af dem starter sin egen byggefase. Den første kaldte vi builder, men fra den sidste FROM begynder vi at forberede vores endelige billede. Som det sidste skridt kopierer vi artefakten fra vores tidligere byggefase til det endelige billede med nginx. Størrelsen på billedet er væsentligt reduceret:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 29 minutes ago 36MBLad os starte containeren med vores billede og sikre os, at alt fungerer:
docker run -p8080:80 appMed -p8080:80 har vi forwarded port 8080 på vores værtsmaskine til port 80 inde i containeren, hvor nginx kører. Åbn i browseren og se vores application. Alt fungerer!

Reduktionen af billedstørrelsen fra 1,74 GB til 36 MB reducerer betydeligt tiden til at levere din applikation til produktion. Men lad os vende tilbage til byggetiden.
$ 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.000sLad os ændre rækkefølgen af lagene
De første tre trin har været cached (hint Using cache). I det fjerde trin kopieres alle projekfiler og i det femte trin installeres afhængighederne RUN npm ci — hele 47,338 sek. Hvorfor skal man hver gang installere afhængighederne igen, når de sjældent ændrer sig? Lad os finde ud af, hvorfor de ikke er blevet cachet. Sagen er, at Docker tjekker lag for lag, om kommandoen og de relaterede filer er ændret. På det fjerde trin kopierer vi alle filerne fra vores projekt, og blandt dem er der selvfølgelig ændringer, hvilket betyder, at Docker ikke kun undlader at tage dette lag fra cachen, men også alle efterfølgende! Lad os foretage en lille ændring i 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 .Først kopieres package.json og package-lock.json, derefter installeres afhængighederne, og først derefter kopieres hele projektet. Resultatet:
$ time docker build -t app .
Sender byggekontext til Docker dæmon 608.8kB
Trin 1/12 : FROM node:12.16.2-alpine3.11 as builder
Trin 2/12 : RUN apk --no-cache --update --virtual build-dependencies add python make g++
---> Bruger cache
Trin 3/12 : WORKDIR /app
---> Bruger cache
Trin 4/12 : COPY package*.json ./
---> Bruger cache
Trin 5/12 : RUN npm ci
---> Bruger cache
Trin 6/12 : COPY . .
Trin 7/12 : RUN npm run build --prod
Dato: 2020-04-16T21:29:44.770Z - Hash: fffa0fddaa3425c55dd3 - Tid: 38287ms
---> 1b9448c73558
Trin 8/12 : FROM nginx:stable-alpine
Trin 9/12 : WORKDIR /app
---> Bruger cache
Trin 10/12 : RUN rm /etc/nginx/conf.d/default.conf
---> Bruger cache
Trin 11/12 : COPY nginx/static.conf /etc/nginx/conf.d
---> Bruger cache
Trin 12/12 : COPY --from=builder /app/dist/app .
Bygget succesfuldt a44dd7c217c3
Succesen tagget app:latest
real 0m46.497s
user 0m0.000s
sys 0m0.000s46 sekunder i stedet for 3 minutter — betydeligt bedre! Den rette rækkefølge af lag er vigtig: Først kopierer vi det, der ikke ændrer sig, derefter det, der sjældent ændrer sig, og til sidst det, der ofte ændrer sig.
Dernæst nogle ord om opbygning af billeder i CI/CD-systemer.
Brug af tidligere billeder til caching
Hvis vi bruger en SaaS-løsning til opbygningen, kan den lokale Docker-cache vise sig at være ren og frisk. For at Docker har noget at hente de bagte lag fra, skal du give det det tidligere opbyggede billede.
Lad os tage et eksempel på, hvordan vores applikation bygges i GitHub Actions. Vi bruger denne konfiguration
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.comBilled image is built and pushed to GitHub Packages in two minutes and 20 seconds:

Now let's modify the build to use cache based on previously built images:
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.comFirst, we need to explain why two commands are executed build. The matter is that in a multi-stage build, the resulting image will consist of layers from the last stage. The layers from the previous stages will not be included in the image. So when using the final image from the previous build, Docker won't be able to find the ready layers to build the image with nodejs (builder stage). To solve this problem, an intermediate image is created $IMAGE_NAME-builder-stage and sent to GitHub Packages, so it can be used in the next build as a cache source.

The total build time has been reduced to one and a half minutes. Half a minute is spent pulling previous images.
Pre-building images
En anden måde at løse problemet med ren Docker-cache på er at flytte nogle lag til en anden Dockerfile, bygge den separat, skubbe den til Container Registry og bruge den som forælder.
Vi opretter vores eget Node.js-billede til at bygge et Angular-program. Opret Dockerfile.node i projektet.
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++Vi bygger og skubber et offentligt billede til Docker Hub:
docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latestNu bruger vi det færdige billede i vores hoved-Dockerfile:
FROM exsmund/node-for-angular:latest as builder
...I vores eksempel er byggetiden ikke reduceret, men forudbyggede billeder kan være nyttige, hvis du har mange projekter, og hver af dem kræver de samme afhængigheder.

Vi har gennemgået flere metoder til at fremskynde bygningen af Docker-billeder. Hvis du ønsker, at deployment skal forløbe hurtigt, så prøv at anvende følgende i dit projekt:
- reduktion af konteksten;
- brug af små forældre-billeder;
- multi-stage bygning;
- ændring af rækkefølgen af instruktioner i Dockerfile for effektivt at bruge cachen;
- konfiguration af cachen i CI/CD-systemer;
- forudgående oprettelse af billeder.
Jeg håber, eksemplet vil gøre det lettere at forstå, hvordan Docker fungerer, og at du kan optimalt indstille din deployment. For at lege med eksemplerne fra artiklen er der oprettet et repository. .
Kilde: habr.com
