от Джошуа Хармс

Уеб разработчиците не са непознати за раздутите приложения или гигантските контейнери на Docker. Въпреки че много приложения могат да постигнат впечатляващи печалби само като произнасят вълшебните думи "node_modules", дори относително простото приложение за точки може да стане обемисто, след като бъдат инсталирани dotnet SDK и зависимостите от пакета.

docker

Такъв беше случаят с контейнера, изпълняващ точно този уебсайт, който е персонализирана програма F # (друг .NET език, родствен до C #) с нулеви зависимости на възлите, с изключение на компилаторите на Stylus и TypeScript. Този сайт дори няма файл package.json и никакви зависимости не се инсталират в папка node_modules. Това е много близо до чистия .NET и контейнерът на Docker, който е хоствал сайта, е почти 8gb веднъж построен.

Удивително, дори не осъзнавах, че контейнерът на този сайт е толкова голям, докато не се опитах да преместя сайта от персонализирана виртуална машина в уеб приложение на Azure Container. Размерът беше толкова голям, че контейнерът буквално нямаше да работи по базовия план на B1 на Azure - най-евтиният наличен план, една стъпка над безплатната. Моят изчислителен екземпляр на Azure изобщо не може да издърпа/извлече/стартира контейнера и ще върне само 502 грешки, недостъпни за услуга.

Оказва се, че съм допуснал няколко грешки "новобранец", които причиниха размера на контейнера ми да се подува толкова зле. Поставих "новобранец" в кавички, защото повечето от тях са неща, които знаех, че трябва да правя, но вместо това поех по мързеливия път. Не мислех, че ще има значение за малък сайт като този, тъй като в края на краищата няма начин почти статичен сайт да бъде повече от един или два гигабайта веднъж поставен в контейнер, нали?

Но отзад нататък тези грешки на новобранеца имаха значение и трябваше да положа малко повече усилия в моя Dockerfile при първото преминаване. Направих три лесни промени масово намалете размера на контейнера на сайта от почти осем гигабайта на по-малко от двеста мегабайта:

  1. Използвайте многоетапни компилации. Моят контейнер на Docker използваше изображението fsharp: netcore, което само по себе си е доста голямо и след това инсталирах изпълнението на NodeJS (и всички базови пакети, на които разчита), плюс моите компилатори Stylus и TypeScript. Използвайки многоетапни компилации, трябва само да въведете битове като компилатора F # и Node за стъпките, от които се нуждаете, а след това можете да ги пуснете и да копирате нещата в по-тънък образ.
  2. Премахнете излишните файлове и пакети, след като изходният код е компилиран. Въпреки че самите файлове с изходния код не са толкова големи, самата папка на .NET пакетите за този уебсайт беше голяма над два гигабайта - и това е за уебсайт само с четири .NET зависимости.
  3. Използвайте Alpine като окончателно изображение по време на изпълнение. Това е частично свързано с # 1, но по-специално използването на Alpine е огромна печалба за всяко контейнерно приложение, тъй като тежи общо под шест мегабайта! Това е огромно количество пространство, спестено само от използването на това конкретно изображение като окончателно изображение на контейнера. Въпреки това, тази мазнина беше подрязана отнякъде, което означава, че Alpine има само необходимите нужди, необходими за самото стартиране, и не идва с общи двоични файлове, които можете да намерите в редовно изображение на Ubuntu.

Пример

Нека да разгледаме един бърз пример, където можем да преминем през всяка от стъпките, описани по-горе. По-долу е точно същият Dockerfile, който използвах за този уебсайт, преди да го отслабна:

И този Dockerfile доведе до контейнер, който беше 7.61gb:

Както можете да видите в Dockerfile, няма пакет.json и единствената причина Node и Yarn да бъдат инсталирани е да инсталирате компилаторите TypeScript и Stylus. Сайтът използва (и е) използващ .NET мениджър на пакети, наречен Paket, за да възстанови пакетите за самия уебсайт. Paket е много подобен на Nuget, с изключение на това, че поддържа заключващи файлове в момент, когато Nuget CLI не (въпреки че заключващите файлове най-накрая идват в Nuget скоро).

Възстановените от Paket .NET пакети бяха лесно най-големите свине за съхранение освен самото изображение на контейнера. Веднъж възстановена, папката с пакети тежи тежка 2gb. Още по-изненадващото е, че тези два гигабайта пакета на стойност бяха инсталирани за уебсайт, който има само четири зависимости: FSharp.Core, Microsoft.Fsharplu.Json (лек JSON парсер за F #), Suave (лека уеб рамка като ASP. NET или Nancy) и Markdig (пакет за конвертиране на Markdown в HTML).

Как тези четири зависимости се превеждат на два гигабайта, никога няма да разбера, тъй като дори имах Paket специално да инсталирам пакети само за рамката netstandard1.0 (където обикновено той инсталира пакети за всички версии на рамката по подразбиране, за да направи превключването на рамката по-бързо). Знам, че Node има големи проблеми с подуването на пакетите с папката node_modules, но тази папка с два гигабайта пакета лесно отвежда дори най-големите проекти на Node, които съм изградил.

Независимо от това, процесът на изграждане на този контейнер на Docker е по следния начин:

  1. Изтеглете вече голямо fsharp: netcore основно изображение и добавете Node/Yarn към него.
  2. Инсталирайте компилаторите TypeScript и Stylus.
  3. Копирайте всички файлове и папки от изходната директория и възстановете .NET пакетите с помощта на Paket.
  4. Компилирайте файловете Stylus и TypeScript.
  5. Call dotnet публикува в проекта на уебсайта, който компилира програмата и я обединява с всичките й зависимости, като ги пуска в една папка.

Публикуването на .NET проекта означава, че единствените неща, които уебсайтът трябва да изпълни, се съдържат в изходната папка. Пакетите, които се възстановяват от Paket (или дори Nuget), вече не са необходими след този момент и служат само като мъртво тегло. Освен това всички изходни файлове на C #, TypeScript и Stylus също бяха мъртви. Те вече бяха компилирани съответно в програмата, JS и CSS файлове. Въпреки че тези файлове не са близо до размера на папката .NET пакети, те все още не са необходими за стартиране на самия уебсайт и не служат за по-нататъшна цел.

Добавяне на многоетапни компилации

Най-голямото подобрение, което може да се направи на този Dockerfile (и повечето други Dockerfiles), е използването на многоетапни компилации и смяна на тънко алпийско изображение в края. Многоетапните компилации също се предлагат със страничната полза, че не е необходимо да инсталираме Node и Yarn - можем просто да превключим към официалния Node образ, когато е необходимо.

Обичам да организирам скриптовете си за изграждане на Docker от най-бавни задачи до най-бързите, което се възползва от системата за кеширане на Docker за повторно използване на стъпките за изграждане, ако никой от файловете не се е променил. В този случай процесът на възстановяване и публикуване на .NET/Paket е най-бавната част от процеса на изграждане, така че това ще отиде първо. Ако се правят промени във файловете TypeScript/Stylus, но не се правят промени във F # файловете, тогава Dockerfile ще използва повторно кешираните .NET стъпки за възстановяване/изграждане/публикуване и ще рекомпилира само фронтенд файловете, спестявайки прилично парче време.

В по-големите уеб приложения е възможно процесът на компилиране на Webpack да е по-бавен и може да искате тази част да отиде първа.

Използването на многоетапни компилации всъщност е супер просто. Всеки Dockerfile трябва да започне с от IMAGENAME, за да изберете началното изображение и за да използвате многоетапни компилации, всичко, което трябва да направите, е да добавите повече от тези, където имате нужда от тях. Изображенията, които ще използвам, са fsharp: netcore image за изграждане на приложението F #, след това ще премина към nodejs: 10 image, за да инсталирам/стартирам TypeScript и Stylus компилаторите и накрая ще премина към Microsoft /dotnet:2.2-runtime-alpine за стартиране на самия уеб сървър.

Тъй като Docker всъщност превключва на нов контейнер всеки път, когато превключвате на различно изображение, файловете ще трябва да бъдат копирани от различните етапи, а WORKDIR винаги ще трябва да бъде зададен и след размяна.

С направените промени ето как трябва да изглежда Dockerfile:

Едно нещо, което може да забележите в този Dockerfile е, че всъщност променя целевото време на изпълнение на командата за публикуване на dotnet от linux-x64 на linux-musl-x64. Това ми отне няколко минути, за да озадача, но се оказва, че не можете да публикувате вашия .NET проект за Linux x64 и да очаквате да работи в контейнер на Alpine. Това са две различни изпълнения, така че, за да накарате вашия .NET проект да работи в контейнер на Alpine Docker, трябва да се насочите към linux-musl-x64 .

След изграждането на този контейнер с docker build -t myapp. получаваме 63% намаление на общия размер от 7.61gb на 2.75gb!

Все още трябва да се направи още едно подобрение и то да се копират само файловете, които са абсолютно необходими за изпълнението на приложението - това означава премахване на пакети, файлове с изходен код и малки допълнителни неща като заключени файлове и README. Това няма да е толкова драстично, колкото преминаването към Alpine за крайното изображение, но (в моя случай) премахването само на папката пакети освобождава допълнителни два гигабайта.

(Ще видите, че копирам и върху папка „публикации“, която е просто папка, която съдържа всички публикации на този уебсайт във формат Markdown.)

И накрая, след още един docker build -t myapp. завършваме с контейнер, който тежи по-малко от 150MB:

Някои прости подобрения в Dockerfile драстично са намалили размера на контейнера с 98. Това е огромна печалба за нещо, което дори не променя процеса на компилация на самия уебсайт.

Научете как да създавате солидни приложения Shopify с C # и ASP.NET!

Хареса ли ви тази статия? Написах първокласен курс за разработчици на C # и ASP.NET и всичко е свързано с изграждането на стабилни приложения Shopify от първия ден.

Въведете вашия имейл тук и ще ви изпратя безплатна проба от Наръчник за развитие на Shopify. Това ще ви помогне да започнете да интегрирате магазините на Shopify на потребителите си и да ги таксувате с API за таксуване на Shopify.