Några tips om hur man snabbar på att bygga Docker-bilder. Till exempel upp till 30 sekunder

Innan en funktion sätts i produktion, i dessa dagar av komplexa orkestratorer och CI/CD, är det en lång väg att gå från engagemang till tester och leverans. Tidigare kunde du ladda upp nya filer via FTP (ingen gör det längre, eller hur?), och "distributionsprocessen" tog några sekunder. Nu måste du skapa en sammanslagningsförfrågan och vänta länge på att funktionen ska nå användarna.

En del av denna väg är att bygga en Docker-bild. Ibland tar monteringen minuter, ibland tiotals minuter, vilket knappast kan kallas normalt. I den här artikeln tar vi en enkel applikation som vi paketerar till en bild, tillämpar flera metoder för att påskynda bygget och tittar på nyanserna i hur dessa metoder fungerar.

Några tips om hur man snabbar på att bygga Docker-bilder. Till exempel upp till 30 sekunder

Vi har god erfarenhet av att skapa och stödja mediawebbplatser: TASS, Bell, "Ny tidning", Republiken… För inte så länge sedan utökade vi vår portfölj genom att släppa en produktwebbplats påminnelse. Och medan nya funktioner snabbt lades till och gamla buggar fixades, blev långsam implementering ett stort problem.

Vi distribuerar till GitLab. Vi samlar in bilder, skickar dem till GitLab Registry och rullar ut dem till produktion. Det längsta på den här listan är att sätta ihop bilder. Till exempel: utan optimering tog varje backend-bygge 14 minuter.

Några tips om hur man snabbar på att bygga Docker-bilder. Till exempel upp till 30 sekunder

Till slut stod det klart att vi inte längre kunde leva så här, och vi satte oss ner för att ta reda på varför bilderna tog så lång tid att samla in. Som ett resultat lyckades vi minska monteringstiden till 30 sekunder!

Några tips om hur man snabbar på att bygga Docker-bilder. Till exempel upp till 30 sekunder

För den här artikeln, för att inte vara bundna till Reminders miljö, låt oss titta på ett exempel på att montera en tom Angular-applikation. Så låt oss skapa vår applikation:

ng n app

Lägg till PWA till det (vi är progressiva):

ng add @angular/pwa --project app

Medan en miljon npm-paket laddas ner, låt oss ta reda på hur docker-bilden fungerar. Docker ger möjligheten att paketera applikationer och köra dem i en isolerad miljö som kallas en container. Tack vare isolering kan du köra många containrar samtidigt på en server. Behållare är mycket lättare än virtuella maskiner eftersom de körs direkt på systemkärnan. För att köra en container med vår applikation måste vi först skapa en bild där vi paketerar allt som behövs för att vår applikation ska köras. I huvudsak är en bild en kopia av filsystemet. Ta till exempel Dockerfilen:

FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prod

En Dockerfil är en uppsättning instruktioner; Genom att göra var och en av dem kommer Docker att spara ändringarna i filsystemet och lägga dem över de tidigare. Varje lag skapar sitt eget lager. Och den färdiga bilden är lager kombinerade.

Vad som är viktigt att veta: varje Docker-lager kan cache. Om inget har förändrats sedan den senaste byggnaden, kommer dockern istället för att utföra kommandot att ta ett färdigt lager. Eftersom den huvudsakliga ökningen i bygghastighet kommer att bero på användningen av cachen, när vi mäter bygghastigheten kommer vi att vara särskilt uppmärksamma på att bygga en bild med en färdig cache. Så, steg för steg:

  1. Vi tar bort bilderna lokalt så att tidigare körningar inte påverkar testet.
    docker rmi $(docker images -q)
  2. Vi lanserar bygget för första gången.
    time docker build -t app .
  3. Vi ändrar filen src/index.html - vi imiterar en programmerares arbete.
  4. Vi kör bygget en andra gång.
    time docker build -t app .

Om miljön för att bygga bilder är korrekt konfigurerad (mer om det nedan), kommer Docker redan att ha ett gäng cacher ombord när bygget startar. Vår uppgift är att lära sig hur man använder cachen så att bygget går så snabbt som möjligt. Eftersom vi antar att köra en build utan cache bara händer en gång – allra första gången – kan vi därför ignorera hur långsam den första gången var. I tester är den andra omgången av bygget viktig för oss, när cacherna redan är uppvärmda och vi är redo att baka vår tårta. Vissa tips kommer dock också att påverka den första konstruktionen.

Låt oss lägga Dockerfilen som beskrivs ovan i projektmappen och starta bygget. Alla listor har komprimerats för att underlätta 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.000s

Vi ändrar innehållet i src/index.html och kör det en andra gång.

$ 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

För att se om vi har bilden, kör kommandot docker images:

REPOSITORY   TAG      IMAGE ID       CREATED              SIZE
app          latest   79f335df92d3   About a minute ago   1.74GB

Innan du bygger tar docker alla filer i det aktuella sammanhanget och skickar dem till dess demon Sending build context to Docker daemon 409MB. Byggkontexten anges som det sista argumentet till byggkommandot. I vårt fall är detta den aktuella katalogen - ".", - och Docker drar allt vi har i den här mappen. 409 MB är mycket: låt oss fundera på hur vi fixar det.

Förminska sammanhanget

För att minska sammanhanget finns det två alternativ. Eller lägg alla filer som behövs för montering i en separat mapp och peka docker-kontexten till den här mappen. Detta kanske inte alltid är bekvämt, så det är möjligt att ange undantag: vad som inte ska dras in i sammanhanget. För att göra detta, lägg in .dockerignore-filen i projektet och ange vad som inte behövs för bygget:

.git
/node_modules

och kör bygget igen:

$ 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 är mycket bättre än 409 MB. Vi minskade också bildstorleken från 1.74 till 1.38 GB:

REPOSITORY   TAG      IMAGE ID       CREATED         SIZE
app          latest   4942f010792a   3 minutes ago   1.38GB

Låt oss försöka minska storleken på bilden ytterligare.

Vi använder Alpine

Ett annat sätt att spara på bildstorleken är att använda en liten överordnad bild. Föräldrabilden är bilden på grundval av vilken vår bild är beredd. Det undre lagret anges av kommandot FROM i Dockerfile. I vårt fall använder vi en Ubuntu-baserad avbildning som redan har nodejs installerade. Och den väger...

$ docker images -a | grep node
node 12.16.2 406aa3abbc6c 17 minutes ago 916MB

... nästan en gigabyte. Du kan minska volymen avsevärt genom att använda en bild baserad på Alpine Linux. Alpine är en mycket liten Linux. Docker-bilden för nodejs baserad på alpin väger endast 88.5 MB. Så låt oss byta ut vår livliga image i husen:

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

Vi var tvungna att installera några saker som är nödvändiga för att bygga applikationen. Ja, Angular bygger inte utan Python ¯(°_o)/¯

Men bildstorleken sjönk till 150 MB:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   aa031edc315a   22 minutes ago   761MB

Låt oss gå ännu längre.

Flerstegsmontering

Allt som finns i bilden är inte det vi behöver 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.json

Med docker run app ls -lah vi lanserade en container baserad på vår bild app och utförde kommandot i den ls -lah, varefter containern avslutade sitt arbete.

I produktionen behöver vi bara en mapp dist. I det här fallet måste filerna på något sätt ges utanför. Du kan köra en viss HTTP-server på nodejs. Men vi ska göra det lättare. Gissa ett ryskt ord som har fyra bokstäver "y". Höger! Ynzhynyksy. Låt oss ta en bild med nginx, lägga en mapp i den dist och en liten konfiguration:

server {
    listen 80 default_server;
    server_name localhost;
    charset utf-8;
    root /app/dist;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Bygg i flera steg kommer att hjälpa oss att göra allt detta. Låt oss ändra vår 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 två instruktioner FROM i Dockerfilen kör var och en av dem olika byggsteg. Vi ringde den första builder, men från och med den sista FROM kommer vår slutliga bild att förberedas. Det sista steget är att kopiera artefakten av vår montering i föregående steg till den slutliga bilden med nginx. Storleken på bilden har minskat avsevärt:

REPOSITORY   TAG      IMAGE ID       CREATED          SIZE
app          latest   2c6c5da07802   29 minutes ago   36MB

Låt oss köra behållaren med vår bild och se till att allt fungerar:

docker run -p8080:80 app

Med alternativet -p8080:80 vidarebefordrade vi port 8080 på vår värddator till port 80 inuti behållaren där nginx körs. Öppna i webbläsaren http://localhost:8080/ och vi ser vår ansökan. Allt fungerar!

Några tips om hur man snabbar på att bygga Docker-bilder. Till exempel upp till 30 sekunder

Att minska bildstorleken från 1.74 GB till 36 MB minskar avsevärt tiden det tar att leverera din applikation till produktion. Men låt oss gå tillbaka till monteringstiden.

$ 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

Ändra ordningen på lagren

Våra första tre steg cacheades (tips Using cache). I det fjärde steget kopieras alla projektfiler och i det femte steget installeras beroenden RUN npm ci - så mycket som 47.338s. Varför installera om beroenden varje gång om de ändras mycket sällan? Låt oss ta reda på varför de inte cachades. Poängen är att Docker kommer att kontrollera lager för lager för att se om kommandot och filerna som är associerade med det har ändrats. I det fjärde steget kopierar vi alla filer i vårt projekt, och bland dem finns det naturligtvis förändringar, så Docker tar inte bara det här lagret från cachen, utan även alla efterföljande! Låt oss göra några små ändringar i Dockerfilen.

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 kopieras package.json och package-lock.json, sedan installeras beroenden och först efter det kopieras hela projektet. Som ett resultat:

$ 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 sekunder istället för 3 minuter - mycket bättre! Rätt ordning på lagren är viktig: först kopierar vi det som inte ändras, sedan det som ändras sällan och slutligen det som ändras ofta.

Därefter några ord om montering av bilder i CI/CD-system.

Använder tidigare bilder för cache

Om vi ​​använder någon form av SaaS-lösning för bygget, kan den lokala Docker-cachen vara ren och fräsch. För att ge hamnarbetaren en plats att få de bakade lagren, ge honom den tidigare byggda bilden.

Låt oss ta ett exempel på att bygga vår applikation i GitHub Actions. Vi använder den här konfigurationen

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

Bilden sätts ihop och skickas till GitHub-paket på två minuter och 20 sekunder:

Några tips om hur man snabbar på att bygga Docker-bilder. Till exempel upp till 30 sekunder

Låt oss nu ändra konstruktionen så att en cache används baserat på tidigare byggda bilder:

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

Först måste vi berätta varför två kommandon startas build. Faktum är att i en flerstegsmontering kommer den resulterande bilden att vara en uppsättning lager från det sista steget. I det här fallet kommer inte lager från tidigare lager att inkluderas i bilden. Därför, när du använder den slutliga bilden från ett tidigare bygge, kommer Docker inte att kunna hitta färdiga lager för att bygga bilden med nodejs (byggarstadiet). För att lösa detta problem skapas en mellanbild $IMAGE_NAME-builder-stage och skjuts till GitHub-paket så att den kan användas i en efterföljande build som en cachekälla.

Några tips om hur man snabbar på att bygga Docker-bilder. Till exempel upp till 30 sekunder

Den totala monteringstiden reducerades till en och en halv minut. En halv minut går åt till att dra upp tidigare bilder.

Föravbildning

Ett annat sätt att lösa problemet med en ren Docker-cache är att flytta några av lagren till en annan Dockerfil, bygga den separat, skjuta in den i Container Registry och använda den som förälder.

Vi skapar vår egen nodejs-bild för att bygga en Angular-applikation. Skapa Dockerfile.node i projektet

FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add 
    python 
    make 
    g++

Vi samlar in och publicerar en offentlig bild i Docker Hub:

docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latest

Nu i vår huvudsakliga Dockerfile använder vi den färdiga bilden:

FROM exsmund/node-for-angular:latest as builder
...

I vårt exempel minskade inte byggtiden, men förbyggda bilder kan vara användbara om du har många projekt och måste installera samma beroenden i vart och ett av dem.

Några tips om hur man snabbar på att bygga Docker-bilder. Till exempel upp till 30 sekunder

Vi tittade på flera metoder för att påskynda byggandet av docker-bilder. Om du vill att implementeringen ska gå snabbt, prova att använda detta i ditt projekt:

  • reducerande sammanhang;
  • användning av små föräldrabilder;
  • flerstegsmontering;
  • ändra ordningen på instruktionerna i Dockerfilen för att effektivt använda cachen;
  • ställa in en cache i CI/CD-system;
  • preliminärt skapande av bilder.

Jag hoppas att exemplet kommer att göra det tydligare hur Docker fungerar, och att du kommer att kunna konfigurera din distribution optimalt. För att leka med exemplen från artikeln har ett arkiv skapats https://github.com/devopsprodigy/test-docker-build.

Källa: will.com

Lägg en kommentar