Czym jest Docker? I jak uruchomić model uczenia maszynowego w kontenerze?

miroslawmamczur.pl 2 miesięcy temu

Cześć moje kochane córeczki! Dzisiaj chciałbym Wam opowiedzieć o czymś super magicznym, co nazywa się Dockerem. To jak magiczna kuchnia dla komputerów!

Ooo! Magiczna kuchnia? Ale przecież komputery nie jedzą jedzenia! – powiedziała ze zdziwieniem Jagódka.

– Tak, masz rację! Ale w magicznej kuchni dockerowej gotuje się coś specjalnego, nie jedzenie, ale takie magiczne rzeczy, jak programy i modele. Wyobraźcie sobie, iż każdy przepis na czarodziejską zupę czy ciasto ma swój własny szczelny garnek.

– To jakby każda potrawa miała swoje własne miejsce do gotowania? – dodała Jagódka.

Dokładnie tak! Docker pomaga trzymać wszystkie składniki w jednym miejscu, tak jak przepis na ciasto trzymamy w jednej książce.

Tatusiu, a co jeżeli chcemy zrobić nową zupkę? – dopytała Otylka.

– To właśnie Docker jest super! jeżeli chcemy zrobić nowe danie, po prostu otwieramy nowy garnek (kontener). I co najważniejsze, jeżeli jedno danie uwalnia zapach, to nie przenosi się on do innych garnków. Każde danie jest szczelnie zamknięte!

– Ooo! To super! A skąd bierzemy odpowiednie garnki? – dociekała dalej Jagódka.

Z Docker Hub. To taka magiczna szafa, w której trzymamy garnki (kontenery) gotowe do użycia. Po ugotowaniu możemy do niej schować nasze danie, a następnie dzielić się z innymi, tak jak dzielimy się przepisami z przyjaciółmi!

– Super! To brzmi jak zabawa! Możemy też zrobić magiczne danie w Dockerze? – z euforią krzyknęła Otylka i Jagódka.

Zanim wyjaśnię Wam, czym jest Docker i jak może pomagać w projektach uczenia maszynowego, to wyjaśnijmy sobie czym jest konteneryzacja.

UWAGA1. Docker to jedno z naprawdę wielu narzędzi pomocnych w naszej pracy, które chciałem Wam przedstawić i opisać.

UWAGA2. Dla przykładu użyjemy aplikacji napisanej w FastApi w poprzednim wpisie i włożymy ją do Dockera. Zatem, jak nie wiecie, jak zamykać modele ML jako aplikacje, to zapraszam najpierw do poprzedniego wpisu [LINK].

Czym jest konteneryzacja?

Konteneryzacja to technologia, która umożliwia opakowanie aplikacji (lub modelu zamkniętego w aplikacji) i jej zależności w izolowane jednostki zwane kontenerami.

Każdy kontener zawiera wszystko, co jest potrzebne do uruchomienia aplikacji, w tym kod, środowisko wykonawcze, biblioteki i inne zależności. Kontenery zapewniają izolację aplikacji od środowiska, w którym są uruchamiane, co umożliwia jednolite i przenośne uruchamianie aplikacji na różnych systemach operacyjnych i środowiskach.

Zatem, jak już prawdopodobnie się domyślacie, konteneryzacja przede wszystkim pozwala na łatwiejsze wdrażanie aplikacji na różnych platformach i środowiskach, bez konieczności konfigurowania całej infrastruktury od zera. Dlatego konteneryzacja jest popularna wśród deweloperów i administratorów systemów, ponieważ umożliwia łatwiejsze zarządzanie aplikacjami, ich wdrożenie i skalowanie.

Kluczowe zalety konteneryzacji:

Poniżej moja subiektywna lista czterech największych zalet konteneryzacji:

  1. Izolacja: Kontenery są izolowane od siebie i od systemu operacyjnego hosta, co oznacza, iż zmiany w jednym kontenerze nie wpływają na inne kontenery ani na sam system! Dla mnie to coś pięknego, bo nikt nie zmieni nam biblioteki pakietu Python, czyli zawsze wyniki predykcji modelu będą takie same.
  2. Przenośność: Kontenery są przenośnymi jednostkami oprogramowania, co umożliwia ich uruchamianie niemal identycznie na różnych środowiskach. Dzięki czemu można uniknąć problemów związanych z różnicami w konfiguracjach systemów operacyjnych czy infrastrukturze.
  3. Skalowalność: Środowisko kontenerowe jest doskonałe do elastycznego skalowania, zarówno w górę, jak i w dół. Można błyskawicznie uruchamiać dodatkowe instancje w zależności od zapotrzebowania, a zarządzaniem nimi zajmuje się oprogramowanie do orkiestracji.
  4. Szybkość: Tworzenie, uruchamianie, replikacja i kasowanie kontenerów trwa zwykle sekundy, co znacznie przewyższa czas i zużycie zasobów związane z maszynami wirtualnymi. Dzieje się tak, ponieważ kontenery dzielą jądro systemu operacyjnego hosta.

Kluczowe wady konteneryzacji:

Poniżej subiektywnie wybrane trzy największe wady konteneryzacji:

  1. Dodatkowy poziom skomplikowania: Korzystanie z kontenerów może czasem prowadzić do nadmiernej abstrakcji, szczególnie dla deweloperów, którzy nie mają głębokiego zrozumienia infrastruktury. To może sprawić, iż debugowanie i rozwiązywanie problemów staje się trudniejsze.
  2. Złożoność konfiguracji sieciowej: Zarządzanie sieciami w kontenerach może być dość złożone, zwłaszcza w środowiskach wielokontenerowych lub w przypadku potrzeby komunikacji między kontenerami i ich skalowalnością.
  3. Narzut obciążenia sieciowego: W środowiskach mikrousługowych, gdzie wiele kontenerów komunikuje się ze sobą, może wystąpić znaczny narzut obciążenia sieciowego. To może prowadzić do opóźnień w komunikacji i potencjalnych problemów wydajnościowych. Czasami walczymy choćby o milisekundy (to tak z własnego doświadczenia)!

Konteneryzacja vs wirtualna maszyna

Spójrzmy jeszcze na różnicę pomiędzy konteneryzacją a wirtualną maszyną. Aby lepiej to zwizualizować posłużyłem się przykładem dotyczącym gotowania.

Maszyna Wirtualna (VM = Virtual Machine): Wyobraźmy sobie, iż gotujemy dwa oddzielne dania w dwóch osobnych kuchniach. Każda z nich wyposażona jest w pełen zestaw garnków, patelni, przyborów i sprzętów kuchennych. Każde danie jest gotowane w swoim własnym „kuchennym środowisku”, niezależnym od drugiego, co oznacza, iż używamy dwukrotnie więcej zasobów (garnków, gazu i energii).

Konteneryzacja: Teraz, zamiast dwóch oddzielnych kuchni, wyobramy sobie jedną dużą kochnię z podziałem na mniejsze sekcje ze wspólnymi elementami, takimi jak piekarnik czy płyta grzewcza. Każde danie (kontener) jest gotowane w swojej własnej „przestrzeni kuchennej”, ale korzysta z tych samych podstawowych zasobów, co oznacza, iż wykorzystujemy przestrzeń kuchenną bardziej efektywnie i ograniczamy zużycie energii.

Zatem w odniesieniu do systemów komputerowych maszyna wirtualna działa na pełnym systemie operacyjnym, co oznacza, iż jest bardziej niezależna, ale też bardziej zasobożerna. Kontener natomiast działa na wspólnym jądrze systemowym, co sprawia, iż jest lżejszy i bardziej efektywny, ale jednocześnie może być mniej izolowany niż maszyna wirtualna.

Ogólnie rzecz biorąc, konteneryzację często stosuje się tam, gdzie ważna jest szybkość, efektywne wykorzystanie zasobów i przenośność aplikacji, podczas gdy maszyny wirtualne są używane tam, gdzie wymagana jest pełna izolacja i niezależność od systemu operacyjnego.

Czym jest Docker?

Docker to narzędzie do konteneryzacji, które umożliwia programistom izolowanie i uruchamianie aplikacji w lekkich, przenośnych jednostkach zwanych kontenerami. Jest to projekt open source, co oznacza, iż jego kod źródłowy jest dostępny publicznie, a społeczność może przyczyniać się do jego rozwoju i udoskonalania. Tutaj LINK.

Definicja Docker’a obejmuje platformę oraz format kontenera. Platforma Docker zapewnia narzędzia do zarządzania cyklem życia kontenerów, takie jak budowanie, uruchamianie, skalowanie i zarządzanie nimi. Format kontenera Docker to standardowy sposób opakowywania aplikacji i wszystkich jej zależności w izolowaną jednostkę.

Projekt Docker jest rozwijany i utrzymywany przez firmę Docker, Inc., jednak kod źródłowy jest publiczny, a społeczność open source również wnosi wkład w rozwój narzędzia. Docker cieszy się ogromną popularnością wśród programistów i administratorów systemów, dzięki swojej prostocie, skuteczności i elastyczności w zarządzaniu kontenerami.

W czym pomaga Docker w projektach uczenia maszynowego?

Docker, jako narzędzie do konteneryzacji, odegrało kluczową rolę w ułatwianiu procesu tworzenia, testowania i wdrażania modeli. Sporo z nich na pewno już czujesz po samym opisie zalet konteneryzacji. Poniżej przedstawiam kilka aspektów, w których Docker okazuje się niezwykle pomocny w pracy osób zajmujących się modelami ML.

  1. Izolacja środowiska
    • Docker umożliwia tworzenie izolowanych kontenerów, co pozwala na jednolite środowisko pracy zarówno na etapie tworzenia modelu, jak i wdrażania go na produkcję.
    • Dzięki Dockerowi eliminuje się problemy związane z różnicami w konfiguracjach systemów operacyjnych i zależnościami.
  2. Reprodukcja wyników:
    • Docker pozwala na zdefiniowanie dokładnego środowiska, w którym model został trenowany. To ułatwia innym członkom zespołu reprodukowanie wyników, co jest najważniejsze w środowisku badawczym.
    • Dzięki użyciu Docker’a możliwe jest łatwe udostępnianie projektu innym członkom zespołu lub społecznościom, zapewniając spójność w środowisku pracy.
  3. Skalowanie i elastyczność:
    • Kontenery Docker umożliwiają łatwe skalowanie zasobów, co jest istotne przy pracy z dużymi zbiorami danych lub złożonymi modelami.
    • Elastyczność Docker’a ułatwia dostosowywanie środowiska do różnych potrzeb, zarówno podczas treningu, jak i wdrożenia modelu.
  4. Wdrożenie na produkcję:
    • Docker ułatwia przenoszenie modelu z etapu eksperymentalnego na produkcję poprzez zapewnienie jednolitego środowiska na wszystkich etapach cyklu życia modelu.
    • Standardowe kontenery Docker’a ułatwiają integrację z systemami orkiestracji, takimi jak Kubernetes, co wspomaga zarządzanie i skalowanie modelami w środowisku produkcyjnym.

Jak widzicie korzystanie z Docker’a w procesie uczenia maszynowego przynosi wiele korzyści, wpływając pozytywnie na spójność, elastyczność i skalowalność projektów.

Instalacja Dockera

Instalacja Dockera jest naprawdę prosta i składa się tylko z kilku nieskomplikowanych kroków. Warto mieć jednak na uwadze, iż należy zainstalować narzędzie stworzone przez Docker, Inc zwane Docker Desktop. Jest to kompleksowe darmowe narzędzie stworzone z myślą o ułatwieniu procesu konteneryzacji dla programistów i inżynierów, umożliwiając im szybkie i efektywne korzystanie z technologii kontenerowej na ich lokalnych maszynach.

Pełną aktualną dokumentację instalacji możecie zobaczyć tutaj: https://docs.docker.com/desktop/.

https://www.docker.com/products/docker-desktop/

Tworzenie pierwszego kontenera.

Krok 1. Przygotowanie gotowego API z modelem w Python

Oczywiście jest to najdłuższy krok, o którym napisano niejedną epopeję i powieść. W naszym przypadku skorzystamy z ostatniego wpisu na blogu, gdzie przygotowaliśmy wspólnie API dla modelu. Z ostatniego wpisu pobrałem tylko pliki potrzebne do odpalenia ostatecznej aplikacji, jaką tam stworzyliśmy (predykcja i przebudowa modelu).

Link do kodu na git.

Dla porządku wszystkie pliki z naszej wcześniejszej aplikacji umieszczam w podfolderze ./app.

Uwaga. Zmiana ścieżki nieznacznie wpłynęła na wcześniejszy kod, gdzie były np. ścieżki do pliku z modelem. Zatem w odpowiednich miejscach dopisałem "app/", aby ostateczny kod działał poprawnie.

Krok 2. Wyszukanie gotowego obrazu Docker.

Jak wszyscy wiemy, nie ma sensu wynajdywać koła na nowo. Dlatego nie trzeba w przypadku Dockerów tworzyć wszystkich instalacji od podstaw. Istnieje mnóstwo gotowych obrazów Docker dostępnych w publicznych repozytoriach, które zawierają narzędzia, środowiska i biblioteki znacznie ułatwiające życie! Wystarczy pobrać i działać.

Gdzie szukać? Polecam dwa miejsca:

  1. Docker Hub: To moje ulubione miejsce do poszukiwań. Znajdziecie tutaj setki tysięcy publicznych obrazów, w tym wiele dostosowanych specjalnie do potrzeb data scientistów. Polecam odwiedzić Docker Hub i zacząć od wyszukiwania.
  2. GitHub: Czasem społeczność data scientistów jest niesamowicie hojna. Na GitHubie znajdziecie wiele repozytoriów zawierających Dockerfile’y gotowe do użycia.

Ja osobiście najczęściej korzystam z oficjalnych obrazów, ponieważ w przypadku moich projektów były wystarczające.

W naszym przypadku wystarczy podstawowy obraz python w odpowiedniej wersji.

Tworzenie dockerfile

Aby utworzyć Docker musimy najpierw przygotować tak zwany dockerfile, czyli plik skryptowy, zawierający instrukcje krok po kroku, których Docker używa do automatycznego tworzenia obrazu kontenera. To coś w rodzaju kuchennego planu, który mówi Dockerowi, jakie składniki (czyli pliki, zależności, komendy) dodać, jakich narzędzi użyć i w jaki sposób nasza aplikacja powinna działać.

Dockerfile budujemy z kilku elementów:

1. From

Instrukcja FROM określa bazowy obraz, na którym zbudowany zostanie kontener. Jest to punkt wyjścia, podobny do używania gotowego bulionu w gotowaniu. Python? R? Node.js? To zależy od smaku Twojego projektu.

2. COPY/ADD

Instrukcje COPY/ADD umożliwiają przenoszenie plików z lokalnego systemu plików do systemu plików kontenera, zapewniając niezbędne zasoby dla aplikacji. Na przykład możemy przenieść nasz kod pythonowy i potrzebne dodatkowe pliki do odpalenia aplikacji. Poprawne wykorzystanie tej instrukcji pozwala na efektywne i bezpieczne tworzenie kontenerów, które zawierają wszystko, co jest potrzebne do uruchomienia aplikacji.

3. WORKDIR

Instrukcja WORKDIR ustala katalog, w którym będą wykonywane kolejne instrukcje dockerfile, czyli innymi słowami mówimy Dockerowi, w którym dokładnie garnku mamy gotować.

4. RUN

Instrukcja RUN pozwala na wykonanie komendy lub polecenia w kontenerze podczas procesu budowania obrazu. Jest jednym z kluczowych elementów, który umożliwia konfigurację i instalację oprogramowania, narzędzi, czy bibliotek w środowisku kontenera. Uwaga! Podczas korzystania z instrukcji RUN, warto pamiętać o czytelności kodu. Dłuższe i bardziej skomplikowane komendy można podzielić na kilka linii, co ułatwia zrozumienie i utrzymanie pliku dockerfile.

5. CMD

Instrukcja CMD w pliku dockerfile określa domyślne polecenie, które zostanie wykonane po uruchomieniu kontenera. Jest to po prostu polecenie startowe dla kontenera. Mamy dwa główne sposoby użycia instrukcji CMD:

a) CMD jako forma JSON array: W tej formie, CMD jest zapisane jako tablica JSON, co umożliwia bezpośrednie uruchomienie polecenia zdefiniowanego w postaci listy argumentów.

CMD ["python", "app.py", "--debug"]

b) CMD jako forma string: CMD może również przyjmować formę ciągu znaków, zwłaszcza gdy polecenie jest jednolitym wyrażeniem.

CMD python app.py --debug

Uwaga! Instrukcja CMD może być zastępowana przy uruchamianiu kontenera dzięki opcji docker run, co pozwala na dostosowywanie zachowania kontenera bez konieczności zmiany samego pliku dockerfile.

Dockerfile dla naszej aplikacji

Znamy już podstawy! Stwórzmy teraz dockerfile dla naszej aplikacji z modelem.

1. Wybór wersji Pythona

Zaczynamy od wyboru wersji Pythona, której chcemy użyć w naszym kontenerze. W tym przypadku wybrałem 3.9.10, co zapewni nam konkretną i stabilną wersję interpretera Pythona.

# choose python version FROM python:3.9.10

Instrukcja FROM przyjmuje argument w postaci nazwy obrazu oraz opcjonalnej etykiety (tagu), która określa konkretną wersję obrazu. W przypadku braku etykiety, Docker użyje domyślnej wersji oznaczonej jako „latest”.

2. Konfiguracja katalogu roboczego

Następnie ustawiamy katalog roboczy w kontenerze, czyli miejscu, w którym będą przechowywane wszystkie pliki i foldery związane z naszą aplikacją. W tym przypadku ustawiamy go na /code wewnątrz kontenera. Teraz wszystkie późniejsze instrukcje będą miały katalog /code jako swoją podstawę.

# Set up a working directory WORKDIR /code
3. Kopiowanie plików zależności Pythona

Kopiujemy plik requirements.txt, który zawiera wszystkie zależności Pythona potrzebne do uruchomienia naszej aplikacji, do katalogu roboczego w naszym kontenerze.

# copy python requirements COPY app/requirements.txt /code/requirements.txt
4. Aktualizacja pip

Wykonujemy aktualizację narzędzia pip, które jest menedżerem pakietów Pythona, aby mieć pewność, iż będziemy korzystać z najnowszej wersji.

# update pip RUN pip install --no-cache-dir --upgrade pip
5. Instalacja zależności

Instalujemy zależności z pliku requirements.txt, używając pip. Ta część procesu pozwoli nam zainstalować wszystkie wymagane biblioteki Pythona potrzebne do uruchomienia naszej aplikacji.

# install requirements RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
6. Kopiowanie naszej aplikacji FastAPI

Kiedy mamy już zainstalowane zależności, możemy skopiować naszą aplikację FastAPI do katalogu roboczego w kontenerze. Dzięki temu kod będzie dostępny wewnątrz kontenera.

# copy our FastApi application COPY ./app /code/app

7. Uruchomienie aplikacji

Na koniec, gdy wszystkie kroki budowania zakończyły się sukcesem, definiujemy komendę CMD, która będzie uruchamiana, gdy kontener zostanie wystartowany. W naszym przypadku używamy uvicorn, aby uruchomić aplikację FastAPI. Opcja -reload pozwala na automatyczne przeładowanie aplikacji przy zmianach w kodzie, co jest przydatne podczas procesu rozwoju. Określamy również host lokalny (czyli 0.0.0.0) oraz port, na którym aplikacja będzie nasłuchiwać, czyli 8080.

# run app CMD ["uvicorn", "app.step5-rebuild:app", "--reload", "--host", "0.0.0.0", "--port", "8080"]

Tak więc, ten dockerfile dokładnie definiuje, jak zbudować środowisko uruchomieniowe dla naszej aplikacji FastAPI, instalując wszystkie zależności i konfigurując środowisko w kontenerze Dockerowym, aby aplikacja działała poprawnie.

A tutaj cały kod:

# choose python version FROM python:3.9.10 # Set up a working directory WORKDIR /code # copy python requirements COPY app/requirements.txt /code/requirements.txt # update pip RUN pip install --no-cache-dir --upgrade pip # install requirements RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt # copy our FastApi application COPY ./app /code/app # run app CMD ["uvicorn", "app.step5-rebuild:app", "--reload", "--host", "0.0.0.0", "--port", "8080"]

Budowanie kontenera

Kiedy mówimy „budowanie kontenera” lub inaczej „budowanie dockerfile’a„, adekwatnie mamy na myśli to, iż używamy powyżej przygotowanego przepisu, aby Docker stworzył paczkę, którą możemy przenieść gdziekolwiek chcemy.

Jest to jak proces tworzenia eliksiru, który zamienia nasz kod w coś, co może być używane wszędzie, gdzie znajduje się Docker. I kiedy to magiczne budowanie zostanie ukończone, będziemy mieć gotowy do użycia obraz Dockerowy, który może być uruchamiany na różnych maszynach, serwerach lub w chmurze, co czyni go naprawdę potężnym narzędziem dla naszych aplikacji!

Aby zbudować obraz Dockera z plikiem dockerfile, musimy wykonać dwa proste kroki:

  1. Upewnić się, iż znajdujemy się w katalogu, który zawiera nasz plik dockerfile. Możemy użyć polecenia cd w terminalu, aby przejść do odpowiedniego katalogu i poprzez komendę np. ls sprawdzić, czy jesteśmy w dobrym katalogu. Ja skorzystam dla ułatwienia z wbudowanego terminala w PyCharma:

2. Gdy jesteśmy we właściwym katalogu, możemy użyć polecenia docker build, aby zbudować obraz Dockera. Komenda ta przyjmuje argument t, który pozwala nazwać i oznaczyć obraz oraz ścieżkę do katalogu zawierającego plik dockerfile. Na przykład:

docker build -t my_first_docker_image .
gdzie:
  • docker build: to polecenie Dockerowe, które mówi Dockerowi, iż chcemy zbudować nowy obraz.
  • -t myimage: to opcja t, która pozwala nazwać nasz obraz. Tutaj my_first_docker_image jest nazwą, którą nadajemy naszemu obrazowi. Możemy użyć tej nazwy później, aby odwoływać się do naszego obrazu w innych komendach Dockerowych.
  • .: kropka na końcu komendy oznacza bieżący katalog, czyli miejsce, gdzie znajduje się plik dockerfile. Docker będzie szukał pliku dockerfile w bieżącym katalogu i użyje go do budowy obrazu.

Tak więc, gdy wykonujemy to polecenie, Docker przeczyta plik dockerfile w bieżącym katalogu, zbuduje obraz zgodnie z instrukcjami zawartymi w pliku dockerfile i nazwie ten obraz „my_first_docker_image„.

Po zakończeniu tych kroków, jeżeli wszystko przebiegło pomyślnie, będziemy mieli zbudowany obraz Dockerowy gotowy do uruchomienia kontenera.

Możecie go znaleźć dzięki polecenia docker images:

Możemy też sprawdzić, czy stworzono obraz z poziomu aplikacji np. Docker Desktop:

Uruchomienie kontenera

Spróbujmy jeszcze uruchomić nasz Docker lokalnie i potwierdzić, iż rzeczywiście wszystko działa. Robimy to poleceniem docker run.

Natomiast żeby zobaczyć swoją aplikację, która działa wewnątrz Dockera w przeglądarce, musimy się upewnić, iż wszystko jest dobrze skonfigurowane. Zatem musimy jeszcze zmapować port kontenera Dockera na port naszego komputera.

Wyobraźcie sobie to jak dawanie wskazówek. Mówimy Dockerowi: „Hej Docker, kiedy ktoś puka na port 8080 na moim komputerze, po prostu przekieruj ich prosto do portu 8080 u ciebie, gdzie działa moja aplikacja. Rozumiesz Docker?” Robimy to uruchamiając Dockera z taką flagą:

docker run -p 8080:8080 my_first_docker_image

Teraz, kiedy Docker wie, jak dotrzeć do naszej aplikacji, po prostu otwieramy przeglądarkę internetową i wpisujemy http://localhost:8080. To jak pukanie do własnych drzwi komputera pod adresem portu 8080, a Docker odpowiada: „O, cześć! Wejdź, aplikacja jest tutaj!”

No i wsio. Wszystko działa!

Podsumowanie

Brawo! Mamy przetestowany i działający lokalnie Docker, który na przykład możemy wysłać do Azure czy AWS, aby go hostować i udostępnić światu!

Natomiast to dopiero początek poznawania świata i możliwości Dockera. Zaawansowane zagadnienia obejmują skalowanie zasobów, zarządzanie danymi w kontenerze czy integrację z systemami zarządzania danymi. W praktyce Docker w Data Science znajduje zastosowanie w rozwoju i testowaniu modeli, wdrażaniu modeli do produkcji oraz ułatwianiu współpracy zespołowej. Natomiast wyzwania to bezpieczeństwo, zarządzanie zależnościami czy optymalizacja wydajności!

Mam nadzieję, iż troszkę przybliżyłem Wam czym jest Docker i zachęciłem do zgłębienia jego tajników!

Pozdrawiam z całego serducha,

Idź do oryginalnego materiału