Wprowadzenie

Poprzedni artykuł zakończyliśmy utworzeniem obrazu z naszą aplikacją. Była to aplikacja konsolowa przygotowana w oparciu o technologię .NET Core. Zauważyliśmy jednak pewien problem, wielkość obrazu na poziomie 1.79GB może zastanawiać. Wywołajmy jeszcze raz polecenie:
docker image ls Docker: lista obrazów

W poprzednim artykule wspominałem, że naszym obrazem bazowym jest microsoft/dotnet:2.0-sdk. Obraz ten zawiera wszystkie odpowiednie zależności do przygotowania build’a i uruchomienia aplikacji. Niestety, zależności niezbędne do przygotowania kompilacji wprowadzają bardzo duży narzut – dlatego możecie zobaczyć tak duży rozmiar obrazu bazowego.

Zastanówmy się czy te zależności są nam potrzebne, kiedy uruchamiamy nasz obraz za pomocą polecenia:
docker run sample-app Docker: uruchomienie aplikacji

Obraz został już przygotowany, skompilowany – jedyne czego potrzebujemy to środowiska wykonawczego .NET Core. Wykorzystamy w tym wypadku funkcjonalność budowania wielopoziomowego.

Budowanie wielopoziomowe

Funkcjonalność ta pozwala na wielokrotne używanie instrukcji FROM w pliku Dockerfile. Każde kolejne użycie definiuje nowy „etap”. Instrukcje wykonywane są sekwencyjne a Docker używa ostatniego obrazu do przygotowania finalnej kompilacji.

Dzięki temu obraz microsoft/dotnet:2.0-sdk możemy wykorzystać do zbudowania aplikacji a następnie wykorzystać inny obraz, microsoft/dotnet:2.0-runtime, do przygotowania ostatecznej wersji naszego obrazu. Wspomniana powyżej wersja zawiera tylko zależności potrzebne do uruchomienia aplikacji. Wynikiem użycia tej wersji obrazu powinna być zauważalna zmiana rozmiaru.

Więcej możecie dowiedzieć się z dokumentu: Use multi-stage builds

Optymalizacja obrazu

Nasz dotychczasowy plik Dockerfile prezentuje się w następujący sposób:

FROM microsoft/dotnet:2.0-sdk
COPY . ./sample-app
WORKDIR /sample-app/

RUN dotnet build -c Release
ENTRYPOINT ["dotnet", "run", "-c", "Release", "--no-build"]
Używamy tylko jednej instrukcji FROM - wskazana wersja obrazu używana jest do przygotowania finalnej kompilacji.

Spróbujmy wprowadzić do naszego pliku następującej zmiany:

FROM microsoft/dotnet:2.0-sdk AS build
COPY . ./sample-app
WORKDIR /sample-app/
RUN dotnet build -c Release –o output

FROM microsoft/dotnet:2.0-runtime AS runtime
COPY --from=build /sample-app/output .
ENTRYPOINT ["dotnet", "sample-app.dll"]
Jakich zmian dokonaliśmy:
  • w pierwszym wierszu możemy zobaczyć opcję kompilacji AS, która nadaje nazwę własną temu etapowi przygotowywania obrazu. Takie podejście pozwala nam na odniesie się do tego etapu w kolejnych fazach kompilacji;
  • do polecenia dotnet build dodałem dodatkowy parametr -o output, który umieszcza wszystkie dane budowania w katalogu wyjściowym;
  • w kolejnym kroku definiujemy nowy etap kompilacji za pomocą obrazu środowiska wykonawczego microsoft/dotnet:2.0-runtime. Nadawanie nazwy za pomocą opcji kompilacji AS jest w tym miejscu niepotrzebne (do tego etapu nie będziemy się już odnosić) ale napewno wpływa na poprawe przejrzystości przygotowanego dokumentu;
  • instrukcja COPY --from=build /sample-app/output . kopiuje zawartość naszego katalogu wyjściowego z etapu budowy do katalogu głównego naszego środowiska uruchomieniowego. Jest to niezwykle ważna linia, łacząca oba etapy kompilacji. Pozwala na użycie wersji zbudowanej w pierwszym etapie kompilacji w środowisku uruchomieniowym bez niepotrzebnych zależności budowania;
  • finalna instrukcja: ENTRYPOINT ["dotnet", "sample-app.dll"] – jest nieznaczną odmianą tej zastosowanej w pierwszej części kompilacji. Obraz dotnet:2.0-runtime nie obsługuje takich poleceń jak dotnet run - podajemy zatem nazwę biblioteki, która należy uruchomić. Bibliotekę tę otrzymaliśmy z poprzedniego użycia instrukcji dotnet build, która ma taką samą nazwę jak nazwa przygotowanej przez nas aplikacji.
Możemy teraz przystąpić do przygotowania nowej wersji naszego obrazu korzystając z polecenia:
dotnet build –t sample-app .

Podsumowanie

Przygotowaliśmy nasz nowy obraz za pomocą budowania wielopoziomowego. Przejdźmy zatem do najbardziej interesującej części – porównania rozmiarów. Skorzystajmy z polecenia:
docker image ls Docker: sprawdzenie listy obrazów

Wynik: 219MB! To w przybliżeniu 88% mniej niż wcześniej. Obraz wciąż wydaje się trochę za duży dla Witaj Świecie - tak jednak wygląda świat kontenerów.

Zdaje sobię sprawę, że przykład jest bardzo ogólny i uproszczony. Celem było jednak pokazanie jak wiele miejsca można oszczędzić wybierając odpowiednie środowisko wykonawcze i stosując odpowiednie funkcje kompilacji dostępne na platformie Docker.