Před tím, než se funkce dostane na produkci, je třeba projít dlouhou cestou od commitu přes testy až k nasazení v dnešní době složitých orchestrátorů a CI/CD. Dříve stačilo nahrát nové soubory pomocí FTP (to už dnes nikdo nedělá, že?), a proces „deploy“ trval sekundy. Dnes je nutné vytvořit merge request a čekat, než se funkce dostane k uživatelům.
Část z této cesty je build Docker obrazu. Někdy build trvá minuty, jindy desítky minut, což se těžko dá považovat za normální. V tomto článku vezmeme jednoduchou aplikaci, zabalíme ji do obrazu, použijeme několik metod pro zrychlení buildu a prozkoumáme nuance těchto metod.

Máme slušné zkušenosti s vytvářením a údržbou webů pro média: , , , … Nedávno jsme rozšířili naše portfolio tím, že jsme spustili web . A zatímco jsme rychle dodělávali nové funkce a opravovali staré chyby, pomalý deploy se stal velkým problémem.
Deploy provádíme na GitLab. Vytváříme obrazy, pushujeme do GitLab Registry a nasazujeme na produkci. Nejdelší částí tohoto procesu je právě build obrazů. Například: bez optimalizace každý build backendu trval 14 minut.
![]()
Nakonec se ukázalo, že takto žít nelze, a usadili jsme se, abychom zjistili, proč trvá build obrazů tak dlouho. Nakonec se nám podařilo zkrátit dobu buildu na 30 sekund!
![]()
Pro tento článek, abychom se nevázali k prostředí Reminderu, prozkoumáme příklad buildu prázdné aplikace na Angular. Takže, vytvořme naši aplikaci:
ng n appPřidáme do ní PWA (jsme přece progresivní):
ng add @angular/pwa --project appZatímco se stahuje milion npm balíčků, podívejme se, jak funguje docker obraz. Docker umožňuje balit aplikace a spouštět je v izolovaném prostředí, které se nazývá kontejner. Díky izolaci můžeme spouštět více kontejnerů na jednom serveru. Kontajnery jsou mnohem lehčí než virtuální stroje, protože běží přímo na jádru systému. Abychom spustili kontejner s naší aplikací, musíme nejdříve vytvořit obraz, do kterého zabalíme vše, co je potřeba pro běh naší aplikace. V podstatě je obraz otiskem souborového systému. Například, vezmeme Dockerfile:
FROM node:12.16.2
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build --prodDockerfile je sada pokynů; při vykonávání každého z nich Docker uloží změny do souborového systému a aplikuje je na předchozí. Každý příkaz vytváří svou vlastní vrstvu. Hotový obraz je pak spojením těchto vrstev.
Co je důležité vědět: každá vrstva může být cachována. Pokud se nic nezměnilo od poslední kompilace, Docker místo vykonávání příkazu vezme již hotovou vrstvu. Hlavní přínos v rychlosti kompilace bude díky využívání cachí, a proto se při měření rychlosti kompilace budeme soustředit právě na kompilaci obrazu s již připravenou cache. Takže postupně:
- Odstraňujeme obrazy lokálně, aby předchozí spuštění neovlivnila test.
docker rmi $(docker images -q) - Spouštíme build poprvé.
time docker build -t app . - Měníme soubor src/index.html – imitujeme práci programátora.
- Spouštíme build podruhé.
time docker build -t app .
Pokud je prostředí pro budování obrazů správně nakonfigurováno (o čemž bude řeč níže), Docker při spuštění build bude mít k dispozici spoustu cachí. Naším úkolem je naučit se využívat cache tak, aby kompilace probíhala co nejrychleji. Předpokládáme, že spuštění build bez cache probíhá jen jednou – poprvé – tedy můžeme ignorovat, jak pomalé bylo toto první spuštění. V testech je pro nás důležitý druhý běh kompilace, kdy jsou cache již aktivovány a jsme připraveni péct náš koláč. Nicméně některé rady se projeví už při prvním buildování.
Uložíme výše popsaný Dockerfile do složky s projektem a spustíme build. Všechny uvedené úryvky byly zkráceny pro snazší čtení.
$ time docker build -t app .
Odesílání kontextu build do Docker démona 409MB
Krok 1/5 : FROM node:12.16.2
Stav: Stáhnuta novější verze obrazu pro node:12.16.2
Krok 2/5 : WORKDIR /app
Krok 3/5 : COPY . .
Krok 4/5 : RUN npm ci
přidáno 1357 balíčků za 22.47s
Krok 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:20:09.664Z - Hash: fffa0fddaa3425c55dd3 - Čas: 37581ms
Úspěšně vytvořeno c8c279335f46
Úspěšně označeno app:latest
skutečný 5m4.541s
uživatelský 0m0.000s
systémový 0m0.000sMěníme obsah src/index.html a spouštíme podruhé.
$ time docker build -t app .
Odesílání kontextu build do Docker démona 409MB
Krok 1/5 : FROM node:12.16.2
Krok 2/5 : WORKDIR /app
---> Používá cache
Krok 3/5 : COPY . .
Krok 4/5 : RUN npm ci
přidáno 1357 balíčků za 22.47s
Krok 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:26:26.587Z - Hash: fffa0fddaa3425c55dd3 - Čas: 37902ms
Úspěšně vytvořeno 79f335df92d3
Úspěšně označeno app:latest
skutečný 3m33.262s
uživatelský 0m0.000s
systémový 0m0.000sAbychom zjistili, jestli jsme úspěšně vytvořili obraz, provedeme příkaz docker images:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 79f335df92d3 Před asi minutou 1.74GBPřed sestavením Docker shromáždí všechny soubory v aktuálním kontextu a odešle je svému démonu. Odesílání kontextu sestavení do démonu Docker 409MB. Kontext pro sestavení se uvádí jako poslední argument příkazu build. V našem případě je to aktuální adresář – „.“, – a Docker přenáší všechno, co máme v této složce. 409 MB je hodně: přemýšlejme, jak to napravit.
Snižujeme kontext
Abychom snížili kontext, existují dvě možnosti. Buď přesunout všechny soubory potřebné pro sestavení do samostatné složky a poskytnout kontext Dockeru právě na tuto složku. To nemusí být vždy pohodlné, a proto máme možnost uvést výjimky: co nemusíme přenášet do kontextu. K tomu přidáme do projektu soubor .dockerignore a uvedeme, co není potřeba pro sestavení:
.git
/node_modulesa spustíme sestavení znovu:
$ time docker build -t app .
Odesílání kontextu sestavení do démonu Docker 607.2kB
Krok 1/5 : Z FROM node:12.16.2
Krok 2/5 : WORKDIR /app
---> Použití vyrovnávací paměti
Krok 3/5 : COPY . .
Krok 4/5 : RUN npm ci
přidáno 1357 balíčků za 22.47s
Krok 5/5 : RUN npm run build --prod
Datum: 2020-04-16T19:33:54.338Z - Hash: fffa0fddaa3425c55dd3 - Čas: 37313ms
Úspěšně sestaveno 4942f010792a
Úspěšně označeno app:latest
reálné 1m47.763s
uživatel 0m0.000s
systém 0m0.000s607.2 Kb — mnohem lepší než 409 MB. A ještě jsme zmenšili velikost obrazu z 1.74 na 1.38GB:
REPOSITÁŘ TAG ID OBRAZU VYTVOŘENO VELIKOST
app latest 4942f010792a před 3 minutami 1.38GBZkusme obraz zmenšit ještě více.
Používáme Alpine
Dalším způsobem, jak ušetřit na velikosti obrazu, je použít malý základní obraz. Základní obraz je obraz, na jehož základě je náš obraz připravován. Spodní vrstvu uvádíme příkazem FROM v Dockerfile. V našem případě používáme obraz založený na Ubuntu, který již má nainstalovaný nodejs. A jeho váha je …
$ docker images -a | grep node
node 12.16.2 406aa3abbc6c před 17 minutami 916MB… téměř gigabyte. Výrazně snížit objem lze použitím obrazu založeného na Alpine Linux. Alpine je velmi malý linux. Docker obraz pro nodejs na základě Alpine váží pouze 88.5 MB. Takže pojďme nahradit náš tlustý základní obraz:
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 --prodMuseli jsme nainstalovat některé věci, které jsou potřebné pro sestavení aplikace. Ano, Angular se nesestaví bez Pythona ¯(°_o)¯
Ale velikost obrazu klesla o 150 MB:
REPOSITÁŘ TAG ID OBRAZU VYTVOŘENO VELIKOST
app latest aa031edc315a před 22 minutami 761MBJdeme dál.
Více fází sestavení
Ne všechno, co je v obraze, potřebujeme v produkci.
$ 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.jsonPomocí docker run app ls -lah Spustili jsme kontejner na základě našeho obrazu app a vykonali v něm příkaz ls -lah, po kterém kontejner ukončil svou činnost.
Na produkci potřebujeme pouze složku dist. Přitom je potřeba nějakým způsobem sdílet soubory ven. Můžeme spustit nějaký HTTP server na nodejs. Ale uděláme to jednoduše. Uhodněte ruské slovo, které má čtyři písmena „ы“. Správně! Ынжыныксы. Vezmeme obraz s nginx, vložíme do něj složku dist a malý konfigurák:
server {
listen 80 default_server;
server_name localhost;
charset utf-8;
root /app/dist;
location / {
try_files $uri $uri/ /index.html;
}
}To vše nám pomůže provést multi-stage build. Změníme náš 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 .Teď máme dvě instrukce FROM v Dockerfile, každá z nich spouští svůj fázi sestavení. První jsme nazvali builder, ale od posledního FROM se bude připravovat náš finální obraz. Posledním krokem je zkopírovat artefakt našeho sestavení z předchozí fáze do finálního obrazu s nginx. Velikost obrazu se významně zmenšila:
REPOSITORY TAG IMAGE ID CREATED SIZE
app latest 2c6c5da07802 před 29 minutami 36MBPojďme spustit kontejner s naším obrazem a ujistěme se, že vše funguje:
docker run -p8080:80 appS parametrem -p8080:80 jsme propojili port 8080 na našem hostitelském stroji s portem 80 uvnitř kontejneru, kde běží nginx. Otevřeme v prohlížeči a vidíme naši aplikaci. Vše funguje!

Snížení velikosti obrazu z 1,74 GB na 36 MB výrazně zkracuje dobu dodání vaší aplikace do produkce. Ale vraťme se k času sestavení.
$ 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.000sMěníme pořadí vrstev
První tři kroky byly vyrovnávány (nápověda Using cache). V čtvrtém kroku se kopírují všechny soubory projektu a v pátém kroku se instalují závislosti RUN npm ci — celých 47,338 s. Proč pokaždé znovu instalovat závislosti, když se mění jen velmi zřídka? Pojďme si objasnit, proč se neuložily do cache. Je to tím, že Docker kontroluje vrstvu po vrstvě, zda se nezměnily příkazy a související soubory. Ve čtvrtém kroku kopírujeme všechny soubory našeho projektu a samozřejmě mezi nimi jsou změny, takže Docker nejenže nebere tuto vrstvu z cache, ale ani všechny následující! Pojďme provést malé úpravy v 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 .Nejprve se zkopírují package.json a package-lock.json, poté se nainstalují závislosti a až nakonec se zkopíruje celý projekt. Výsledek:
$ 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 sekund místo 3 minut — výrazně lepší! Důležitý je správný pořádek vrstev: nejprve kopírujeme to, co se nemění, poté to, co se mění zřídka, a nakonec to, co se mění často.
Dále několik slov o sestavení obrazů v CI/CD systémech.
Použití předchozích obrazů pro cache
Pokud používáme k sestavení nějaké SaaS řešení, může být lokální cache Dockeru čistá a čerstvá. Aby měl Docker odkud vzít upečené vrstvy, dejte mu předchozí sestavený obraz.
Podívejme se na sestavení naší aplikace v GitHub Actions. Použijeme takovou konfiguraci.
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.comObraz je shromážděn a nahrán do GitHub Packages za dvě minuty a 20 sekund:

Nyní změníme sestavení tak, aby používalo mezipaměť založenou na předchozích shromážděných obrazech:
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.comNejprve je třeba vysvětlit, proč se spouští dvě příkazy build. To je proto, že v multi-stage sestavení bude výsledným obrazem sada vrstev z poslední fáze. Přitom vrstvy z předchozích vrstev nebyly zahrnuty do obrazu. Proto, když se používá konečný obraz z předchozího sestavení, Docker nebude moci najít připravené vrstvy pro sestavení obrazu s nodejs (fáze builder). Abychom tento problém vyřešili, vytvoříme meziprochodný obraz $IMAGE_NAME-builder-stage a odešleme ho do GitHub Packages, aby ho bylo možné použít v následném sestavení jako zdroj mezipaměti.

Celkový čas sestavení se zkrátil na jednu a půl minuty. Půl minuty se ztrácí při stahování předchozích obrazů.
Předběžné vytvoření obrazů
Další způsob, jak vyřešit problém s čistou mezipamětí Dockeru, je část vrstev vyčlenit do jiného Dockerfile, sestavit ho samostatně, nahrát do Container Registry a použít jako rodičovský.
Vytváříme svůj obraz nodejs pro sestavení Angular aplikace. V projektu vytvořte Dockerfile.node
FROM node:12.16.2-alpine3.11
RUN apk --no-cache --update --virtual build-dependencies add
python
make
g++Sestavujeme a nahráváme veřejný obraz do Docker Hub:
docker build -t exsmund/node-for-angular -f Dockerfile.node .
docker push exsmund/node-for-angular:latestNyní v našem hlavním Dockerfile použijeme hotový obraz:
FROM exsmund/node-for-angular:latest as builder
...V našem příkladu se doba sestavení nezkrátila, ale předem vytvořené obrazy mohou být užitečné, pokud máte mnoho projektů a v každém z nich musíte instalovat stejné závislosti.

Prozkoumali jsme několik metod zrychlení sestavení docker obrazů. Pokud chcete, aby nasazení probíhalo rychle, zkuste ve svém projektu použít:
- snížení kontextu;
- používání malých rodičovských obrazů;
- multi-stage build;
- změnu pořadí instrukcí v Dockerfile, aby bylo možné efektivně využít mezipaměť;
- nastavení mezipaměti v CI/CD systémech;
- předběžné vytváření obrazů.
Doufám, že na příkladu bude jasnější, jak Docker funguje, a budete moci optimálně nastavit své nasazení. Abychom si mohli vyzkoušet příklady z článku, byl vytvořen repozitář .
Zdroj: habr.com
