Blog JSystems - uwalniamy wiedzę!
Blog JSystems - uwalniamy wiedzę!
Docker to jedno z tych pojęć, które pada na każdej rozmowie o nowoczesnym wytwarzaniu oprogramowania, a mimo to wielu osobom trudno powiedzieć wprost, czym właściwie jest. W tym przewodniku wyjaśnimy to od podstaw: czym jest konteneryzacja, czym kontener różni się od maszyny wirtualnej, a potem krok po kroku uruchomimy pierwszy kontener, zbudujemy własny obraz i połączymy kilka usług w jedną aplikację. Wszystkie przykłady uruchomiliśmy naprawdę, więc na zrzutach widzisz autentyczne wyniki z terminala i z przeglądarki, a nie rysunki poglądowe.
Docker to narzędzie do konteneryzacji, czyli pakowania aplikacji razem ze wszystkim, czego potrzebuje do działania (kodem, bibliotekami, ustawieniami), w jeden przenośny obraz, który uruchamia się identycznie na każdym komputerze. Taki zapakowany, działający pakiet nazywamy kontenerem (ang. container). Zamiast instalować aplikację i ręcznie doinstalowywać dziesiątki zależności na każdym serwerze, budujemy obraz raz i uruchamiamy go wszędzie tak samo.
Najprostsza analogia to kontener transportowy. Zanim upowszechniły się stalowe kontenery, każdy towar pakowano i przeładowywano inaczej, a połowa pracy w porcie polegała na kombinowaniu, jak to wszystko poukładać. Znormalizowany kontener zmienił zasady: nie ma znaczenia, co jest w środku, bo z zewnątrz każdy wygląda tak samo i pasuje na każdy statek, ciężarówkę i dźwig. Docker robi dokładnie to samo z oprogramowaniem. Twoja aplikacja może być w Pythonie, Javie czy Node.js, ale opakowana w kontener wygląda z zewnątrz identycznie i uruchamia się tak samo na laptopie, na serwerze i w chmurze.
Żeby zrozumieć, dlaczego to takie przełomowe, trzeba zobaczyć, czym kontener różni się od maszyny wirtualnej (ang. virtual machine), bo to z nią najczęściej się go myli.
Różnica jest fundamentalna. Maszyna wirtualna udaje cały, osobny komputer razem z własnym systemem operacyjnym, więc typowo waży kilka gigabajtów i uruchamia się w minuty. Kontener nie udaje komputera. Współdzieli jądro systemu gospodarza i dokłada tylko to, co odróżnia jedną aplikację od drugiej, dlatego waży zwykle megabajty i startuje w sekundy. Za chwilę zobaczymy to na prawdziwych liczbach: uruchomione przez nas kontenery zajmowały pojedyncze dziesiątki megabajtów pamięci.
Warto od razu rozwiać jedno nieporozumienie. Docker nie jest maszyną wirtualną i nie jest wirtualizacją w klasycznym sensie. To mechanizm izolacji procesów wbudowany w system: kontener to zwykły proces działający na gospodarzu, tyle że odgrodzony od reszty tak, że widzi tylko swój własny, mały świat. Dodajmy jeszcze, że Docker to nie to samo co Kubernetes, o którym zapewne słyszałeś. Docker uruchamia pojedyncze kontenery, a Kubernetes zarządza tysiącami kontenerów na wielu serwerach naraz. Docker jest fundamentem, Kubernetes nadbudową. Naukę zaczyna się od Dockera, a różnice między nimi i moment, w którym warto sięgnąć po orkiestrację, rozwijamy w osobnym porównaniu Kubernetes vs Docker - czym się różnią i kiedy co wybrać.
Najłatwiej docenić Dockera, przypominając sobie ból, który usuwa. Prawie każdy, kto pisał lub wdrażał oprogramowanie, zna zdanie „przecież u mnie działa". Aplikacja chodzi bez zarzutu na komputerze programisty, ale po przeniesieniu na serwer testowy albo produkcyjny nagle przestaje. Powód jest zawsze podobny: gdzieś indziej jest inna wersja języka, brakuje jakiejś biblioteki systemowej, inny jest plik konfiguracyjny albo zmienna środowiskowa. Im więcej takich drobnych różnic, tym więcej godzin schodzi na szukanie, dlaczego „to samo" zachowuje się inaczej.
Docker rozwiązuje ten problem w najprostszy możliwy sposób: skoro kłopotem są różnice w środowisku, to pakujemy środowisko razem z aplikacją. Obraz zawiera nie tylko nasz kod, ale też konkretną wersję języka, dokładnie te biblioteki, których używamy, i całą konfigurację. Kiedy uruchamiamy go na serwerze, dostajemy dokładnie to samo, co działało na laptopie. Nie „prawie to samo", tylko dosłownie ten sam pakiet.
Zanim przejdziemy do praktyki, uporządkujmy pięć słów, które będą wracać w każdej komendzie. Bez nich dokumentacja Dockera wygląda jak żargon, a z nimi wszystko układa się w logiczną całość.
Zależność między nimi jest prosta i warto ją zapamiętać jako jedno zdanie: z Dockerfile budujemy obraz, obraz przechowujemy w rejestrze, a po uruchomieniu obrazu powstaje kontener, który swoje trwałe dane trzyma w wolumenie. Mając te pojęcia w głowie, możemy wreszcie coś uruchomić.
Na komputer osobisty najprościej zainstalować Docker Desktop, dostępny na Windows, macOS i Linux. To jeden instalator, który dokłada wszystko, czego potrzeba: silnik Dockera, narzędzia wiersza poleceń i prosty panel graficzny. Na serwerze zwykle instaluje się sam silnik (ang. Docker Engine) z repozytorium systemu. Po instalacji sprawdzamy, czy wszystko gra, pytając Dockera o wersję, a potem uruchamiamy tradycyjny pierwszy kontener testowy o nazwie hello-world.
To pozornie błahe polecenie pokazuje w miniaturze cały mechanizm Dockera. Napisaliśmy tylko docker run hello-world, a silnik po kolei: sprawdził, czy ma obraz lokalnie, a gdy go nie znalazł, pobrał go z rejestru, następnie utworzył z niego kontener i wykonał zaszytą w nim komendę. Dokładnie ten sam schemat zadziała za chwilę dla prawdziwego serwera WWW, tyle że zamiast wypisać tekst, kontener zacznie obsługiwać ruch sieciowy.
docker run <nazwa-obrazu> to najważniejsze polecenie Dockera. Jeśli obrazu nie ma lokalnie, Docker sam go pobierze. Cała reszta to dokładanie do tego opcji, na przykład numeru portu albo nazwy.Uruchommy teraz coś użytecznego: serwer WWW nginx, jeden z najpopularniejszych obrazów na świecie. Najpierw obejrzymy, jakie obrazy mamy już na dysku, a potem uruchomimy nginx tak, żeby był dostępny w przeglądarce. Kluczowe będą dwie opcje polecenia run: -d uruchamia kontener w tle (ang. detached), a -p łączy port na naszym komputerze z portem wewnątrz kontenera.
# Uruchom nginx w tle; port 8088 na naszym komputerze -> port 80 w kontenerze
PS C:\demo> docker run -d -p 8088:80 --name web nginx:alpine
docker images pokazuje obrazy na dysku (zwróć uwagę, jak lekki jest nginx w wariancie alpine), docker run uruchamia kontener, a docker ps wypisuje to, co aktualnie działa, wraz z mapowaniem portów.Warto zwrócić uwagę na dwie rzeczy. Po pierwsze, dopisek :alpine po nazwie obrazu to tag, czyli wskazanie konkretnego wariantu. Wariant alpine jest zbudowany na wyjątkowo lekkiej dystrybucji Linuksa i dlatego zajmuje kilkadziesiąt megabajtów zamiast kilkuset. Po drugie, docker ps pokazał kolumnę z portami w postaci 0.0.0.0:8088->80. To właśnie efekt opcji -p: cokolwiek wejdzie na port 8088 naszego komputera, trafi na port 80 wewnątrz kontenera, gdzie nasłuchuje nginx. Sprawdźmy to w przeglądarce.
Skoro kontener działa, możemy zajrzeć do jego wnętrza, nie zatrzymując go. Polecenie docker logs pokazuje, co kontener wypisał, a docker exec pozwala uruchomić polecenie w środku działającego kontenera, na przykład sprawdzić wersję nginx albo podejrzeć, jaki system jest w środku. Jeśli praca w wierszu poleceń to dla Ciebie świeży temat, warto najpierw oswoić sam terminal - pokazujemy to w artykule Linux w codziennej pracy programisty - dlaczego warto znać terminal.
docker logs wyświetla jego dziennik (widać wpis z naszej wizyty w przeglądarce, kod 200), a docker exec uruchamia polecenia w jego wnętrzu. Kontener okazuje się miniaturowym Linuksem (Alpine) z jednym zadaniem: obsłużyć nginx.Gdy skończymy, kontener zatrzymujemy poleceniem docker stop web i usuwamy przez docker rm web (albo od razu docker rm -f web). Po usunięciu nie zostaje żaden ślad: żadnych rozsianych po systemie plików, wpisów w rejestrze usług czy zależności. To jedna z najbardziej wyzwalających cech Dockera. Można próbować, psuć i zaczynać od nowa bez obawy, że zaśmiecimy sobie komputer.
Uruchamianie cudzych obrazów to dopiero połowa Dockera. Prawdziwa moc pojawia się, gdy pakujemy w obraz własną aplikację. Zrobimy to na malutkiej aplikacji webowej w Pythonie (framework Flask), która wyświetla powitanie i pokazuje, w jakim kontenerze działa. Najpierw sam kod aplikacji:
import os, platform
from flask import Flask
app = Flask(__name__)
@app.route("/")
def strona_glowna():
return f"""
<h1>Witaj z wnetrza kontenera Docker!</h1>
<p>Nazwa hosta (kontenera): {os.uname().nodename}</p>
<p>Python w kontenerze: {platform.python_version()}</p>
"""
if __name__ == "__main__":
# 0.0.0.0 - nasluchuj na wszystkich interfejsach, inaczej port
# nie bedzie widoczny spoza kontenera
app.run(host="0.0.0.0", port=5000)
Aplikacja korzysta tylko z frameworka Flask, więc plik requirements.txt ma dosłownie jedną linię. Przypięta wersja (==3.0.3) sprawia, że obraz za każdym razem zbuduje się identycznie:
flask==3.0.3
Teraz najważniejsze: Dockerfile, czyli przepis, według którego Docker zbuduje obraz z naszą aplikacją. Każda linia to jeden krok. Kolejność nie jest przypadkowa i za chwilę wyjaśnimy, dlaczego zależności instalujemy przed skopiowaniem kodu.
# 1. Obraz bazowy - lekki Python. Na nim budujemy nasz wlasny obraz.
FROM python:3.12-slim
# 2. Katalog roboczy wewnatrz kontenera. Kolejne kroki dzialaja wzgledem niego.
WORKDIR /app
# 3. Najpierw sama lista zaleznosci. Docker zapamieta ten krok w cache
# i nie bedzie instalowal pakietow od nowa przy kazdej zmianie kodu.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 4. Dopiero teraz kopiujemy kod aplikacji.
COPY app.py .
# 5. Informujemy, ze aplikacja w kontenerze nasluchuje na porcie 5000.
EXPOSE 5000
# 6. Polecenie uruchamiane przy starcie kontenera.
CMD ["python", "app.py"]
Te trzy pliki muszą leżeć obok siebie w jednym katalogu projektu (u nas nazwaliśmy go moja-apka). To dokładnie wszystko, czego Docker potrzebuje, żeby zbudować obraz:
Nazwy plików muszą być dokładnie takie: Dockerfile pisany wielką literą i bez żadnego rozszerzenia, a requirements.txt to ta sama nazwa, którą podaliśmy w instrukcji COPY. Teraz w terminalu wchodzimy do katalogu moja-apka i stamtąd budujemy obraz jednym poleceniem. Kropka na końcu oznacza „buduj z bieżącego katalogu" (dlatego musimy stać wewnątrz moja-apka), a opcja -t nadaje obrazowi czytelną nazwę i wersję.
# jesteśmy w katalogu moja-apka, tym samym, w którym leży Dockerfile
PS C:\moja-apka> docker build -t moja-apka:1.0 .
Podczas budowania Docker nie tworzy jednego wielkiego pliku. Buduje obraz warstwa po warstwie, gdzie każda instrukcja z Dockerfile dokłada jedną warstwę na poprzednią. To nie szczegół techniczny, tylko powód, dla którego kolejność w Dockerfile ma znaczenie.
Ta pozornie drobna sztuczka z kolejnością potrafi skrócić budowanie z minut do sekund. Przy pierwszym budowaniu Docker wykonuje wszystkie kroki, ale przy kolejnych, jeśli zmieniliśmy tylko app.py, ponownie użyje zapamiętanej warstwy z bibliotekami i przebuduje jedynie ostatnie kroki. To jeden z tych detali, które odróżniają osobę „klikającą w Dockerze" od kogoś, kto rozumie, co się dzieje pod spodem.
Zbudowany obraz uruchamiamy tak samo jak wcześniej nginx, tyle że podajemy nazwę naszego obrazu. Sprawdźmy efekt najpierw w terminalu, a potem w przeglądarce.
Chcesz budować własne obrazy i wdrażać je na produkcji pod okiem praktyka? Szkolenie Konteneryzacja Docker i Kubernetes ma terminy gwarantowane.
Prawdziwe aplikacje rzadko składają się z jednego kontenera. Zwykle jest ich kilka: serwer aplikacji, baza danych, pamięć podręczna, może kolejka. Uruchamianie każdego z osobna i ręczne łączenie ich w sieć szybko robi się męczące. Od tego jest Docker Compose: opisujemy całą aplikację w jednym pliku compose.yaml, a potem stawiamy wszystko naraz jednym poleceniem. To, na ile usług w ogóle warto podzielić aplikację, zależy od jej architektury - rozwijamy ten wątek w artykule Monolit vs Mikroserwisy - kiedy warto dzielić aplikację.
Rozbudujemy naszą aplikację o drugą usługę: bazę Redis, która policzy odwiedziny strony. Zacznijmy od kodu aplikacji. Względem poprzedniej wersji dochodzą tylko trzy rzeczy: import biblioteki redis, połączenie z bazą po nazwie usługi (czyli po prostu redis) oraz zwiększanie licznika przy każdym wejściu na stronę.
import os
import redis
from flask import Flask
app = Flask(__name__)
# "redis" to nazwa uslugi z compose.yaml - Docker sam zamienia
# ja na adres kontenera w wewnetrznej sieci Compose.
cache = redis.Redis(host="redis", port=6379)
@app.route("/")
def strona_glowna():
licznik = cache.incr("odwiedziny") # +1 przy kazdym wejsciu; stan trzyma Redis
return f"""
<h1>Witaj z kontenera Docker!</h1>
<p>Ta strona zostala wyswietlona {licznik} raz(y).</p>
<p>Licznik przechowuje kontener Redis, a strone serwuje
kontener aplikacji: {os.uname().nodename}</p>
<p>Odswiez strone - liczba rosnie, bo stan zyje w Redisie, nie w aplikacji.</p>
"""
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Aplikacja potrzebuje więc dodatkowej biblioteki, dlatego requirements.txt ma teraz dwie linie:
flask==3.0.3
redis==5.0.8
Teraz opisujemy całą aplikację w jednym pliku compose.yaml. Zwróć uwagę, że jedną usługę budujemy z naszego Dockerfile, a drugą bierzemy gotową z Docker Hub.
services:
# Usluga 1 - nasza aplikacja, budowana z lokalnego Dockerfile
web:
build: .
ports:
- "5000:5000"
depends_on:
- redis
# Usluga 2 - gotowy Redis prosto z Docker Hub, bez wlasnego Dockerfile
redis:
image: redis:7-alpine
Plik compose.yaml kładziemy w tym samym katalogu projektu, obok kodu aplikacji i Dockerfile. Usługa web ma przecież build: ., więc Compose zbuduje ją z tego samego Dockerfile co wcześniej. Cały projekt wygląda teraz tak:
W środku aplikacja odwołuje się do bazy po prostu jako redis, czyli po nazwie usługi. To jedna z najwygodniejszych rzeczy w Compose: Docker sam tworzy prywatną sieć i zamienia nazwy usług na adresy, więc kontenery znajdują się nawzajem bez ręcznego wpisywania adresów IP. Całość uruchamiamy jednym poleceniem, wywołanym z wnętrza katalogu moja-apka, w którym leży compose.yaml.
# w katalogu moja-apka; -d uruchamia w tle, --build przebudowuje obraz web
PS C:\moja-apka> docker compose up -d --build
docker compose ps potwierdza, że oba kontenery działają.
Wspomnieliśmy, że kontenery są ulotne. Gdy kontener usuniemy, znika razem z całą swoją zawartością. To świetne przy testach, ale fatalne dla bazy danych, której nie chcemy tracić przy każdym restarcie. Rozwiązaniem jest wolumen: wydzielone miejsce na dane, zarządzane przez Dockera, które istnieje niezależnie od kontenerów. Pokażmy to najprościej, jak się da. Zapiszemy plik w wolumenie z jednego kontenera, usuniemy ten kontener, a potem odczytamy plik z zupełnie nowego.
Druga strona medalu to sieci. Widzieliśmy już, że Compose sam tworzy sieć dla naszych usług. Docker pozwala tworzyć wiele odizolowanych sieci, dzięki czemu można na przykład odgrodzić bazę danych tak, żeby była widoczna tylko dla aplikacji, a nie z zewnątrz. W skrócie: wolumeny odpowiadają za to, gdzie mieszkają dane, a sieci za to, kto z kim może rozmawiać. To dwa filary, na których stoją poważniejsze wdrożenia.
Sieci, wolumeny i uruchamianie wielu usług to już codzienność wdrożeń produkcyjnych, którą najszybciej opanujesz na żywej infrastrukturze. Szkolenie Docker i Kubernetes: od zera do bohatera ma terminy gwarantowane.
Kilka razy pobieraliśmy obrazy (nginx, redis, python), nie zastanawiając się skąd. Domyślnym źródłem jest Docker Hub, czyli publiczny rejestr z setkami tysięcy gotowych obrazów. Znajdziemy tam praktycznie każde popularne oprogramowanie: bazy danych, serwery WWW, języki programowania, gotowe aplikacje. Wystarczy docker run, a Docker sam pobierze obraz właśnie stąd.
Docker Hub działa w obie strony. Możemy nie tylko pobierać obrazy, ale i publikować własne poleceniem docker push, żeby zespół albo serwer produkcyjny mógł je pobrać. Firmy zwykle stawiają dodatkowo własny, prywatny rejestr na swoje wewnętrzne obrazy. Mechanizm jest ten sam, zmienia się tylko adres, spod którego obraz jest pobierany.
Docker ma dziesiątki poleceń, ale w codziennej pracy wraca ta sama garść. Poniższa ściąga to praktyczne minimum, które pozwala swobodnie uruchamiać, podglądać i sprzątać kontenery.
| Polecenie | Co robi |
|---|---|
docker run -d -p 8080:80 nginx | Uruchamia kontener w tle i wystawia jego port na naszym komputerze |
docker ps / docker ps -a | Pokazuje kontenery działające / wszystkie, także zatrzymane |
docker images | Wypisuje obrazy pobrane na dysk |
docker build -t nazwa:1.0 . | Buduje obraz z Dockerfile w bieżącym katalogu |
docker pull / docker push | Pobiera obraz z rejestru / wysyła własny do rejestru |
docker logs nazwa | Pokazuje dziennik (wyjście) kontenera |
docker exec -it nazwa bash | Wchodzi do wnętrza działającego kontenera |
docker stop / docker rm | Zatrzymuje / usuwa kontener |
docker compose up -d / down | Stawia / zatrzymuje całą aplikację z pliku compose.yaml |
docker system df / prune | Pokazuje zajęte miejsce / usuwa nieużywane obrazy i kontenery |
Dwa ostatnie polecenia dotyczą sprzątania, bo Docker z czasem potrafi zająć sporo miejsca obrazami i nieużywanymi warstwami. Poleceniem docker system df sprawdzimy, ile dokładnie, zanim cokolwiek skasujemy.
docker system df podpowiada, ile miejsca da się odzyskać sprzątaniem.docker system prune. To polecenie usuwa wszystkie nieużywane obrazy, kontenery i sieci naraz. Bywa bardzo pomocne, ale na komputerze, na którym trzymasz też inne projekty, potrafi skasować więcej, niż zakładasz. Zanim je uruchomisz, dodaj opcję pozwalającą zobaczyć, co zniknie, albo usuwaj konkretne zasoby po nazwie.Konkretnie: prune nie ma trybu „na próbę", więc najpierw flagą -v podglądamy, co i ile zajmuje (polecenie wypisuje każdy obraz, kontener i wolumen z osobna wraz z kolumną „do odzyskania"), a potem zamiast masowego kasowania usuwamy po nazwie tylko to, czego naprawdę nie potrzebujemy:
# 1. Zobacz szczegolowo, co zajmuje miejsce i ile da sie odzyskac
PS C:\demo> docker system df -v
# 2. Wypisz kandydatow do usuniecia
PS C:\demo> docker ps -a # kontenery (tez zatrzymane)
PS C:\demo> docker images # obrazy na dysku
PS C:\demo> docker volume ls # wolumeny
# 3. Usun po nazwie tylko to, czego juz nie potrzebujesz
PS C:\demo> docker rm web # konkretny kontener
PS C:\demo> docker rmi moja-apka:1.0 # konkretny obraz
PS C:\demo> docker volume rm moje-dane # konkretny wolumen
Na koniec zebraliśmy pułapki, w które łatwo wpaść na starcie. Znajomość ich z góry oszczędza sporo zdziwienia. Kilka z nich napotkaliśmy zresztą sami, przygotowując przykłady do tego artykułu.
-p 8088:80 zamiast -p 8080:80, albo zatrzymaj to, co zajmuje port.docker images pokazuje obrazy, docker ps pokazuje kontenery. Usunięcie kontenera nie kasuje obrazu i odwrotnie. To rozróżnienie porządkuje większość początkowych nieporozumień.Docker jest świetny, ale nie jest odpowiedzią na wszystko. Warto wiedzieć, gdzie daje najwięcej.
Z drugiej strony nie w każdym przypadku Docker jest wart zachodu. Prosty skrypt uruchamiany raz na jakiś czas nie potrzebuje konteneryzacji. Aplikacje z ciężką grafiką albo wymagające bezpośredniego dostępu do sprzętu bywają kłopotliwe. Konteneryzacja dokłada też pewną warstwę wiedzy, którą trzeba opanować, więc dla jednorazowego, malutkiego projektu może być po prostu nadmiarowa. Jak zawsze w inżynierii, chodzi o dobranie narzędzia do zadania, a nie o używanie go wszędzie.
Docker rozwiązuje bardzo konkretny problem: różnice między środowiskami, przez które oprogramowanie „działa u mnie, a nie działa na serwerze". Robi to, pakując aplikację razem z jej otoczeniem w przenośny obraz, który wszędzie uruchamia się tak samo. Po drodze przeszliśmy całą podstawową ścieżkę: od uruchomienia pierwszego kontenera, przez pracę z gotowymi obrazami, budowanie własnego obrazu z Dockerfile, aż po składanie wielu usług w jedną aplikację przez Docker Compose. Wszystko na prawdziwych, uruchomionych przykładach.
To solidny fundament, ale też dopiero początek. Naturalny kolejny krok to orkiestracja, czyli zarządzanie wieloma kontenerami na wielu serwerach, za co odpowiada Kubernetes. Jeśli chcesz przejść tę drogę uporządkowanie i od razu na realnej infrastrukturze, zamiast składać wiedzę z rozproszonych poradników, najszybciej zrobisz to pod okiem praktyka.
Szkolenie Docker i Kubernetes: od zera do bohatera -->
To szkolenie może być dofinansowane dla Ciebie z KFS lub BUR.
★★★★★Średnia ocena naszych szkoleń w Google: 5/5
Szkolenie Kubernetes: orkiestracja kontenerów -->
To szkolenie może być dofinansowane dla Ciebie z KFS lub BUR.
★★★★★Średnia ocena naszych szkoleń w Google: 5/5
Komentarze (0)
Brak komentarzy...