Docker w praktyce: czym są kontenery i dlaczego mają znaczenie
„U mnie działa” było kiedyś żartem właśnie dlatego, że zdarzało się tak często – aplikacja działała bez zarzutu u jednego programisty i psuła się w chwili, gdy trafiała na inny serwer, inną wersję systemu czy laptop kolegi. Docker powstał po to, by ta wymówka stała się nieaktualna – pakuje aplikację razem ze wszystkim, czego ta potrzebuje, żeby faktycznie działać.
Czym naprawdę jest kontener
Kontener Dockera łączy kod aplikacji z jej środowiskiem uruchomieniowym, bibliotekami, konfiguracją i zależnościami systemowymi w jedną całość zwaną obrazem. Taki obraz działa identycznie na waszym laptopie, na komputerze kogoś z zespołu, na serwerze stagingowym i na produkcji – bo nie polega na tym, co akurat jest zainstalowane na maszynie hosta. Przynosi ze sobą własne środowisko.
Często zakłada się, że kontener to lekka maszyna wirtualna, ale to porównanie nie oddaje, na czym polega różnica. Maszyna wirtualna wirtualizuje cały komputer, łącznie z własnym jądrem – dlatego jej uruchomienie może trwać minutę, a zanim aplikacja w ogóle wystartuje, potrafi zużyć gigabajty pamięci. Kontener współdzieli jądro maszyny hosta i izoluje tylko warstwę aplikacji, więc startuje w sekundę lub dwie i zużywa ułamek pamięci. Na sprzęcie, który ledwo poradziłby sobie z kilkoma pełnymi maszynami wirtualnymi, można swobodnie uruchomić dziesiątki kontenerów.
Dlaczego ma to znaczenie na co dzień
- Spójność między środowiskami – kontener, który przeszedł testy w CI, to dokładnie ten sam artefakt, który działa na produkcji, a nie ponownie zbudowana wersja, która może się różnić.
- Szybka i tania izolacja – bazę danych, cache i aplikację można uruchomić jako osobne kontenery na jednej maszynie, bez konfliktów o wersje zależności.
- Powtarzalny onboarding – nowy inżynier uruchamia jedno polecenie i ma cały stos działający lokalnie, zamiast strony w wiki z instrukcją konfiguracji, która w połowie jest już nieaktualna.
- Proste skalowanie poziome – potrzebujecie trzech instancji aplikacji za load balancerem? To ten sam obraz uruchomiony trzy razy, a nie trzy różne instalacje, które trzeba utrzymywać w zgodzie.
Dockerfile jako właściwy kontrakt
Każdy obraz budowany jest z pliku Dockerfile – zwykłego pliku tekstowego z dokładną listą kroków składających środowisko: od jakiego obrazu bazowego zacząć, co zainstalować, co skopiować, jakie polecenie uruchamia aplikację. Ten plik trafia do systemu kontroli wersji razem z kodem, co oznacza, że samo środowisko jest wersjonowane i podlega przeglądowi, zamiast być wiedzą plemienną w czyjejś głowie albo w nieaktualnym README.
Gdzie ludzie się potykają
Najczęstszym błędem na początku jest traktowanie kontenera jak lekkiej maszyny wirtualnej, do której łączy się przez SSH i którą edytuje się ręcznie – instalując coś po uruchomieniu, zamiast umieścić te kroki w Dockerfile. To niweczy cały sens: w chwili restartu kontenera te ręczne zmiany znikają i wracacie do „pięć minut temu działało”. Wszystko, czego kontener potrzebuje, musi być wbudowane w sam obraz.
Druga częsta pułapka to obrazy znacznie większe, niż to konieczne, bo każdy zbędny pakiet wciągnięty podczas budowania zostaje w obrazie końcowym. Budowanie wieloetapowe (multi-stage builds) – w którym kompilacja lub budowanie odbywa się w jednym etapie, a do smukłego obrazu końcowego kopiuje się tylko gotowy artefakt – zwykle radykalnie zmniejsza rozmiar obrazu, co ma znaczenie zarówno dla szybkości wdrożeń, jak i dla powierzchni ataku.
Sam Docker nie rozwiązuje wszystkiego – uruchomienie jednego kontenera jest proste, ale niezawodne uruchamianie dziesiątek kontenerów, z obsługą sieci, skalowania i przełączania awaryjnego, to zupełnie inny problem, który zwykle rozwiązuje się za pomocą orkiestratora, takiego jak Kubernetes. Zrozumienie, czym właściwie jest kontener i dlaczego fundamentalnie różni się od maszyny wirtualnej, to jednak fundament, na którym opiera się cała reszta nowoczesnej infrastruktury.
Właściwa konteneryzacja aplikacji – smukły obraz, rozsądny proces budowania i pipeline CI/CD, który ją wdraża – to jedna z pierwszych rzeczy, które konfigurujemy, gdy w Burncode przejmujemy prace z zakresu infrastruktury i DevOps, bo od tego elementu zależy cała reszta niezawodnego wdrożenia.