Blog JSystems - uwalniamy wiedzę!

Szukaj
obraz zbudowany raz Laptop programisty to samo działa Serwer / produkcja to samo działa Chmura to samo działa
Zbuduj raz, uruchom wszędzie: ten sam obraz działa identycznie na każdym środowisku.
Sedno Dockera w jednym obrazku. Aplikację pakujemy w jeden obraz, a potem uruchamiamy go bez zmian na laptopie, na serwerze i w chmurze. Koniec z tłumaczeniem, że „u mnie działało".

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.

Czym właściwie jest Docker

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.

Maszyna wirtualna

App Abiblioteki
App Bbiblioteki
App Cbiblioteki
System gościa
System gościa
System gościa
Hypervisor
System gospodarza
Sprzęt (serwer)
Każda aplikacja niesie własny, pełny system operacyjny. Stąd gigabajty miejsca i start liczony w minutach.

Kontenery (Docker)

App Abiblioteki
App Bbiblioteki
App Cbiblioteki
Docker Engine (silnik)
Jedno wspólne jądro systemu gospodarza
Sprzęt (serwer)
Kontenery współdzielą jądro gospodarza i niosą tylko aplikację z zależnościami. Stąd megabajty miejsca i start w sekundy.
Maszyna wirtualna emuluje cały komputer, więc każda z nich dźwiga własny system operacyjny. Kontenery współdzielą jądro (ang. kernel, rdzeń systemu) gospodarza, dlatego są znacznie lżejsze i szybsze. To jedna z głównych przyczyn popularności Dockera.

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ć.

Problem, który Docker rozwiązuje: koniec z „u mnie działa"

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.

Bez Dockera

Laptop: Python 3.12, wszystkie biblioteki na miejscu działa
Serwer testowy: Python 3.9, brakuje biblioteki nie startuje
Produkcja: inna konfiguracja, inna zmienna błąd na wdrożeniu
Każde środowisko trochę inne. „U mnie działa" kontra „na serwerze nie". Godziny na diagnozę.

Z Dockerem

Laptop: ten sam obraz działa
Serwer testowy: ten sam obraz działa
Produkcja: ten sam obraz działa
Środowisko podróżuje razem z aplikacją w obrazie. Wszędzie identyczne, bo to dosłownie ten sam pakiet.
Docker likwiduje różnice między środowiskami u samego źródła. Zamiast konfigurować każdy serwer osobno, zamykamy aplikację i jej otoczenie w jednym obrazie, który wszędzie zachowuje się tak samo.

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.

Podstawowe pojęcia: obraz, kontener, rejestr, Dockerfile

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ść.

Obraz (image)
Niezmienny szablon aplikacji, coś jak plik instalacyjny. Zawiera kod, biblioteki i konfigurację. Sam z siebie nic nie robi, dopóki go nie uruchomimy.
Kontener (container)
Działająca instancja obrazu, czyli obraz „ożywiony" i uruchomiony. Z jednego obrazu można odpalić wiele kontenerów, tak jak z jednego pliku instalacyjnego wiele kopii programu.
Rejestr (registry)
Magazyn gotowych obrazów w sieci. Najpopularniejszy to Docker Hub. Stamtąd pobieramy obrazy poleceniem pull i tam publikujemy własne poleceniem push.
Dockerfile
Przepis na zbudowanie obrazu, zapisany jako zwykły plik tekstowy z listą kroków. To on sprawia, że obraz jest powtarzalny: każdy zbuduje z niego dokładnie to samo.
Wolumen (volume)
Miejsce na dane, które ma przetrwać dłużej niż kontener. Kontenery są ulotne, więc wszystko, co ma zostać (baza danych, pliki), trzymamy w wolumenie.
Demon (Docker daemon)
Usługa działająca w tle, która faktycznie buduje obrazy i uruchamia kontenery. My wydajemy jej polecenia komendą docker, a ona wykonuje całą pracę.
Sześć pojęć, które wystarczą, żeby swobodnie czytać dokumentację Dockera. Cała reszta to warianty tych podstaw. Zapamiętaj zwłaszcza różnicę obraz kontra kontener, bo to najczęstsze źródło nieporozumień.

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ć.

Instalacja i pierwszy kontener

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.

Terminal: docker --version pokazuje Docker 29.2.1, a docker run hello-world pobiera obraz i wypisuje Hello from Docker
Pierwszy kontener w akcji. Ponieważ obrazu hello-world nie było jeszcze na dysku, Docker sam pobrał go z Docker Hub (linie „Pulling", „Pull complete"), utworzył z niego kontener i uruchomił. Wypisany komunikat to dowód, że cała maszyneria Dockera działa poprawnie.

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.

Zapamiętaj wzorzec: 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.

Obrazy i kontenery w praktyce

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
Terminal: docker images pokazuje obrazy nginx, redis i hello-world, docker run uruchamia nginx, a docker ps pokazuje działający kontener web na porcie 8088
Trzy najczęściej używane polecenia w jednym miejscu. 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.

Przeglądarka pod adresem localhost:8088 pokazuje stronę powitalną Welcome to nginx serwowaną przez kontener
Pod adresem localhost:8088 odpowiada nginx uruchomiony w kontenerze. Nie instalowaliśmy żadnego serwera WWW na swoim komputerze, nie konfigurowaliśmy niczego ręcznie. Jedno polecenie i działający serwer, który równie łatwo usuniemy, nie zostawiając po sobie śladu.

Zaglądanie do środka i sprzątanie

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.

Terminal: docker logs pokazuje logi nginx z wpisem GET 200, docker exec sprawdza wersję nginx i zawartość pliku os-release w kontenerze
Zaglądamy do działającego kontenera. 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.

Budowanie własnego obrazu z pliku Dockerfile

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:

moja-apka/
├─app.pykod aplikacji (Flask)
├─requirements.txtlista zależności (flask==3.0.3)
└─Dockerfileprzepis na obraz, nazwa pliku bez rozszerzenia

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 .
Terminal: docker build wykonuje kolejno kroki 1 do 5 z Dockerfile, każdy zakończony statusem DONE, i tworzy obraz moja-apka:1.0
Budowanie obrazu krok po kroku. Docker wykonuje kolejne instrukcje z Dockerfile (numery 1 do 5), a każdą kończy statusem DONE wraz z czasem. Efektem jest gotowy, nazwany obraz moja-apka:1.0, który możemy teraz uruchomić albo wysłać na inny serwer.

Warstwy obrazu i pamięć podręczna

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.

CMDuruchom python app.py (warstwa startowa)
COPY app.pynasz kod aplikacji, zmienia się najczęściej
RUN pip installzainstalowane biblioteki, zmieniają się rzadko
COPY requirementslista zależności
WORKDIR /appkatalog roboczy
FROM pythonobraz bazowy: system i Python
Obraz to stos warstw, budowany od dołu do góry. Docker zapamiętuje każdą warstwę i przy kolejnym budowaniu używa gotowych, jeśli nic się w nich nie zmieniło. Dlatego zależności (rzadko zmieniane) kopiujemy przed kodem (często zmienianym): dzięki temu zmiana jednej linijki kodu nie wymusza ponownej instalacji wszystkich bibliotek.

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.

Terminal: docker run uruchamia obraz moja-apka na porcie 5000, docker ps potwierdza działanie, a curl zwraca stronę z nazwą hosta kontenera i wersją Pythona
Nasz własny obraz działa. Kontener wystartował na porcie 5000, a zapytanie poleceniem curl zwróciło stronę wygenerowaną w środku kontenera. Widać nazwę hosta (to skrócony identyfikator kontenera) oraz wersję Pythona, która pochodzi z obrazu, a nie z naszego komputera.
Przeglądarka pod adresem localhost:5000 pokazuje stronę naszej aplikacji Flask z identyfikatorem kontenera i wersją Pythona
Ta sama aplikacja w przeglądarce. To już nie cudzy nginx, tylko nasz kod zapakowany we własny obraz. Dokładnie ten obraz moglibyśmy teraz wysłać na serwer albo do rejestru i uruchomić bez zmian, z gwarancją, że zachowa się identycznie. No dobrze, przyznajemy się bez bicia: samą stronę dodatkowo trochę ostylowaliśmy, bo w surowej wersji z powyższego kodu wyglądała naprawdę ubogo ;)

Chcesz budować własne obrazy i wdrażać je na produkcji pod okiem praktyka? Szkolenie Konteneryzacja Docker i Kubernetes ma terminy gwarantowane.

Docker Compose: wiele kontenerów jedną komendą

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:

moja-apka/
├─app.pyaplikacja, teraz zlicza wejścia w Redisie
├─requirements.txtflask oraz redis
├─Dockerfilebez zmian, ten sam co wcześniej
└─compose.yamlnowy plik: opis obu usług naraz

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
Terminal: docker compose up buduje obraz web, tworzy sieć i uruchamia kontenery redis oraz web, a docker compose ps pokazuje obie usługi jako działające
Jedno polecenie, a Docker zbudował obraz aplikacji, utworzył wspólną sieć i wystartował obie usługi we właściwej kolejności (najpierw baza, potem aplikacja, bo tak zapisaliśmy w depends_on). docker compose ps potwierdza, że oba kontenery działają.
Przeglądarka: strona aplikacji pokazuje licznik odwiedzin 10, przechowywany w kontenerze Redis
Efekt współpracy dwóch kontenerów. Aplikacja (kontener web) przy każdym wejściu pyta bazę (kontener redis) o licznik i go zwiększa. Po odświeżeniu liczba rośnie, bo stan żyje w Redisie. Gdyby aplikacja trzymała licznik u siebie, restart kontenera wyzerowałby go za każdym razem.

Wolumeny i sieci: dane oraz komunikacja

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.

Terminal: docker volume create tworzy wolumen, jeden kontener zapisuje do niego plik i znika, a nowy kontener odczytuje ten sam plik z wolumenu
Wolumen w akcji. Pierwszy kontener zapisał notatkę do wolumenu i został usunięty (opcja --rm kasuje kontener po zakończeniu). Drugi, całkiem nowy kontener bez trudu odczytał tę samą notatkę. Dane przetrwały, bo mieszkają w wolumenie, a nie w kontenerze.

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.

Docker Hub: skąd biorą się obrazy

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.

Strona Docker Hub z oficjalnym obrazem nginx: opis, obsługiwane tagi i licznik ponad 20 milionów pobrań tygodniowo
Strona oficjalnego obrazu nginx na Docker Hub. Zwróć uwagę na etykietę „Docker Official Image" (obrazy weryfikowane i utrzymywane w zaufany sposób) oraz na skalę: same tygodniowe pobrania idą w dziesiątki milionów. Warto trzymać się obrazów oznaczonych jako oficjalne lub zweryfikowane, bo uruchamiamy w końcu cudzy kod.

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.

Najważniejsze komendy na co dzień

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.

PolecenieCo robi
docker run -d -p 8080:80 nginxUruchamia kontener w tle i wystawia jego port na naszym komputerze
docker ps / docker ps -aPokazuje kontenery działające / wszystkie, także zatrzymane
docker imagesWypisuje obrazy pobrane na dysk
docker build -t nazwa:1.0 .Buduje obraz z Dockerfile w bieżącym katalogu
docker pull / docker pushPobiera obraz z rejestru / wysyła własny do rejestru
docker logs nazwaPokazuje dziennik (wyjście) kontenera
docker exec -it nazwa bashWchodzi do wnętrza działającego kontenera
docker stop / docker rmZatrzymuje / usuwa kontener
docker compose up -d / downStawia / zatrzymuje całą aplikację z pliku compose.yaml
docker system df / prunePokazuje 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.

Terminal: docker stats pokazuje niskie zużycie pamięci przez kontenery (od 9 do 26 MiB), a docker system df ile miejsca zajmują obrazy, kontenery i wolumeny
Po lewej realne zużycie zasobów: nasze kontenery zajmowały od 9 do 26 megabajtów pamięci, co dobrze pokazuje, jak lekka jest konteneryzacja w porównaniu z maszynami wirtualnymi. Po prawej docker system df podpowiada, ile miejsca da się odzyskać sprzątaniem.
Uwaga na 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

Najczęstsze błędy początkujących

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.

Port jest już zajęty (port is already allocated). Gdy próbujesz wystawić kontener na porcie, którego używa już inny program lub kontener, Docker odmówi startu. Rozwiązanie: wybierz inny port po lewej stronie dwukropka, na przykład -p 8088:80 zamiast -p 8080:80, albo zatrzymaj to, co zajmuje port.
Aplikacja nasłuchuje na 127.0.0.1 zamiast 0.0.0.0. To najczęstsza przyczyna sytuacji „kontener działa, ale strona się nie otwiera". Wewnątrz kontenera aplikacja musi nasłuchiwać na adresie 0.0.0.0, czyli na wszystkich interfejsach. Ustawiona na 127.0.0.1 rozmawia tylko sama ze sobą i mapowanie portu nic nie da.
Mylenie obrazu z kontenerem. Obraz to szablon na dysku, kontener to jego uruchomiona instancja. 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ń.
Utrata danych po usunięciu kontenera. Jeśli baza danych trzyma pliki wewnątrz kontenera, ich usunięcie oznacza utratę danych. Wszystko, co ma przetrwać, musi trafić do wolumenu. To nie jest opcja dla wygody, tylko podstawowa zasada pracy z danymi w Dockerze.

Kiedy Docker się opłaca, a kiedy niekoniecznie

Docker jest świetny, ale nie jest odpowiedzią na wszystko. Warto wiedzieć, gdzie daje najwięcej.

Docker sprawdza się znakomicie, gdy: chcesz mieć identyczne środowisko na laptopie i na produkcji, szybko postawić bazę danych lub inne narzędzie do testów bez śmiecenia w systemie, uruchamiać aplikację złożoną z wielu usług, albo wdrażać powtarzalnie i bez niespodzianek. To codzienność w zespołach pracujących w duchu DevOps.

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.

Podsumowanie

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.

Baner szkolenia Konteneryzacja Docker i Kubernetes: od zera do bohatera w JSystems, z terminem gwarantowanym

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

Baner szkolenia Kubernetes: orkiestracja kontenerów, kompleksowe szkolenie w JSystems, z terminem gwarantowanym

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

Najczęściej zadawane pytania

Czym różni się kontener od maszyny wirtualnej?
Maszyna wirtualna emuluje cały komputer razem z osobnym systemem operacyjnym, więc waży gigabajty i startuje w minuty. Kontener współdzieli jądro systemu gospodarza i pakuje tylko aplikację z jej zależnościami, dlatego waży megabajty i startuje w sekundy.
Czym jest obraz, a czym kontener w Dockerze?
Obraz to niezmienny szablon aplikacji (jak plik instalacyjny), a kontener to działająca instancja uruchomiona z tego obrazu. Z jednego obrazu można uruchomić wiele kontenerów.
Czy Docker jest darmowy?
Sam silnik Docker (Docker Engine) jest otwartoźródłowy i darmowy. Docker Desktop jest darmowy do użytku osobistego, edukacji i małych firm, a większe organizacje potrzebują płatnej subskrypcji.

Komentarze (0)

Musisz być zalogowany by móc dodać komentarz. Zaloguj się przez Google

Brak komentarzy...