Docker funkcjonuje w środowisku programistycznym od lat, ale wciąż dla wielu osób jest czymś odległym i enigmatycznym. W serii postów chciałbym zatem przybliżyć zarówno teorię, jak i praktykę — dowiecie się jak Docker może Wam pomóc w codziennej pracy, jak przygotować środowisko oparte o Dockera oraz jak z tego środowiska korzystać.
Dlaczego warto używać Dockera
Mówiąc krótko: aby zapewnić jednolite, zautomatyzowane środowisko uruchomieniowe dla naszych aplikacji, które będzie mogła uruchomić dowolna osoba rozpoczynająca pracę z projektem. Tylko tyle i aż tyle 😁.
Cofnijmy się o kilka lat, gdy pracowałem w kilkuosobowym zespole deweloperów. Zespół miał w swoim portfolio kilka aplikacji wewnętrznych, ich stabilne i bezawaryjne działanie nie było najwyższym priorytetem — jeśli coś nie działało, użytkownicy nas o tym informowali, my to naprawialiśmy. Każdy z deweloperów pracował na swoim komputerze, wszystkie narzędzia potrzebne do działania aplikacji instalował we własnym zakresie. To doprowadzało do sytuacji, gdy jeden deweloper miał PHP 7.0, inny 7.1, jeden miał MySQL 5.6, inny MySQL 5.7 🤷♂️. Ktoś pracując nad funkcjonalnościami doinstalował sobie rozszerzenie PHP, inne osoby go nie miały. Jakby tego było mało, czasem nawet nie do końca każdy miał świadomość, jakie wersje działają w instancji produkcyjnej i czy środowisko uruchomieniowe posiada wszystkie wymagane rozszerzenia i biblioteki. Można było zatem pisać kod, który działał podczas prac rozwojowych, a nie działał na produkcji. “U mnie działa…”
Z perspektywy czasu jestem w szoku, że to w ogóle jakoś funkcjonowało i szło do przodu, bo różnice w wersjach języka i narzędzi łatwo mogą doprowadzić do poważnych problemów. I czasem doprowadzały, generując masę roboty zarówno w kontekście analizy, jak i naprawy.
Docker wszystkie te problemy eliminuje. Dzięki Dockerowi możemy w prosty sposób uspójnić stack aplikacji, by zawierał wszystko, co potrzeba do jej uruchamiania, i żeby każda osoba pracująca z projektem miała dokładnie to samo środowisko. Jest potrzeba dodania niskopoziomowej biblioteki systemowej, czy też rozszerzenia PHP? Żaden problem, jedna osoba dodaje takie kroki w definicji buildu, pozostałe osoby synchronizują repozytorium i mają u siebie to samo.
Niekwestionowaną zaletą pracy z Dockerem jest to, że pierwszego dnia pracy, dostając nowy, służbowy komputer, możesz zainstalować dwie rzeczy i od razu rozpocząć pracę z projektem. Po prostu instalujesz Dockera i Gita, klonujesz repozytorium i uruchamiasz stack Dockera — gotowe! Mówimy tu oczywiście o uproszczonym scenariuszu, bo istnieją oczywiście bardziej złożone aplikacje, których uruchomienie będzie wymagało ciut więcej 😉. Nie zmienia to jednak faktu, że Docker pozwala nam ujednolicić środowiska uruchomieniowe, mało tego: pozwala w ustandaryzowany sposób przygotować runtime skrojony pod development, jak i runtime produkcyjny, zoptymalizowany pod kątem bezpieczeństwa i wydajności.
Słownik pojęć
Zanim przejdziemy do omawiania właściwych tematów, musimy przybliżyć sobie kilka pojęć, którymi będziemy operować. Ułatwi nam to zrozumienie poszczególnych elementów procesu budowania i uruchamiania kontenerów.
Docker
Docker jest silnikiem uruchomieniowym dla skonteneryzowanych aplikacji. Instaluje się go jako pakiet systemowy, który dostarcza narzędzia CLI, interfejs GUI (Docker Desktop), a także instaluje i konfiguruje procesy działające w tle naszego systemu operacyjnego. Myślmy o Dockerze jako o systemowym frameworku do uruchamiania aplikacji.
Docker Compose
Docker Compose jest rozwinięciem podstawowego narzędzia i służy do definiowania i uruchamiania kompletnych zestawów usług tworzących nasz system. Przykładowo może to być nasza aplikacja, baza danych, system kolejkowy i inne. Zasadniczo chodzi o to, byśmy mogli zasymulować środowisko produkcyjne i stworzyć w pełni działający system na potrzeby prac nad jego rozwojem. Nie znaczy to, że Compose nie może być wykorzystywany poza lokalnym komputerem, ale to już odrębna historia 😉.
Dockerfile
Dockerfile
jest definicją środowiska uruchomieniowego dla naszej aplikacji. Zawiera instrukcje, co musi być wykonane krok po kroku, by zapewnić funkcjonalny runtime. Na jego podstawie przebiega proces budowania obrazów.
compose.yml
Plik w formacie YAML, który definiuje stack aplikacji, czyli wszystkie usługi, wolumeny, sieci i zależności między nimi. Zawarte są tam informacje o tym, jak nazywają się usługi, z jakich obrazów są uruchamiane lub jak są budowane, jak komunikują się między sobą i w jaki sposób są widziane z zewnątrz (z perspektywy lokalnego komputera i systemu operacyjnego).
Obraz
Obraz jest produktem procesu wykonywania instrukcji zawartych w Dockerfile
. Obraz taki powstaje w wyniku wywołania komendy docker build
lub uruchomienia stacku Compose (docker compose up -d
), w którym usługi definiują build context.
Obraz zazwyczaj składa się z systemu operacyjnego, wymaganych bibliotek, środowiska uruchomieniowego dla wymaganego języka programowania, rozszerzeń dla niego, no i oczywiście z kodu aplikacji, jaka ma zostać uruchomiona.
Rejestr
Rejestr obrazów Dockera jest miejscem, do którego wrzucane są zbudowane obrazy, a z którego mogą one zostać pobrane przez dowolną osobę, która ich potrzebuje. Funkcjonuje oczywiście oficjalny i ogólnodostępny Docker Hub, ale tak naprawdę obrazy mogą być przechowywane w wielu innych miejscach: Gitlab ma wbudowany rejestr dla każdego projektu, istnieją dedykowane do tego aplikacje takie jak Harbor.
Kontener
Kontener jest instancją obrazu. Myśląc z perspektywy programisty, możemy o tym myśleć jak o relacji klasa (obraz) → obiekt (kontener). Kontener posiada proces główny (pid 0
), który jest podstawą jego działania.
Wolumen
Wolumeny służą do mapowania plików z lokalnego systemu plików do uruchomionego kontenera. Dzięki takiej operacji możemy nadpisać fragment drzewa plików wewnątrz kontenera (czyli np. pliki, które powstały w wyniku buildu i są częścią obrazu, z jakiego został uruchomiony kontener).
Dzięki temu do lokalnego dewelopmentu możemy zamapować nasz lokalny projekt w miejsce, z którego aplikacja jest uruchamiana wewnątrz kontenera i weryfikować wprowadzane zmiany w czasie rzeczywistym.
Wolumeny służą również do uzyskania trwałości danych tworzonych i modyfikowanych w uruchomionych kontenerach, np. schemy oraz danych MySQL. Stosując wolumeny zachowujemy tego typu dane, nawet gdy zatrzymamy działający stack — gdy wznowimy jego działanie, jego stan będzie dokładnie taki sam.
Sieć
Sieci w Dockerze są bardzo istotne, ponieważ umożliwiają separację usług. Możemy uruchomić obok siebie różna usługi, które wzajemnie nie będą o sobie wiedziały i fizycznie nie będą miały do siebie dostępu.
Compose domyślnie zapewnia sieć, w której są wszystkie usługi w ramach stacku. Nic nie stoi jednak na przeszkodzie, by otwierać komunikację między usługami w odrębnych stackach.
Podsumowanie
Poznaliście właśnie podstawy Dockera 😎! Jeśli teoria do Was nie przemawia, zapraszam na kolejne wpisy w serii — w kolejnym omówimy sobie Dockerfile
bazując na realnych przykładach.