Docker dla początkujących: obrazy, kontenery i pierwsza aplikacja w 15 minut

0
276
3/5 - (5 votes)

Nawigacja:

Po co w ogóle używać Dockera i kiedy to ma sens

„Działa u mnie” kontra „działa wszędzie tak samo”

Programista uruchamia aplikację na swoim laptopie i wszystko śmiga. Tester próbuje to samo na swoim komputerze i dostaje błąd, bo ma inną wersję Pythona, Node.js albo brakującą bibliotekę. Administrator wrzuca aplikację na serwer i debugowanie zaczyna się od zera. Ten klasyczny problem streszcza się w jednym zdaniu: „u mnie działa”.

Docker rozwiązuje to w prosty sposób: aplikacja jest spakowana razem z całym swoim środowiskiem. Zamiast instalować na każdym komputerze odpowiednie języki, biblioteki i narzędzia, uruchamiasz gotowy obraz. Ten obraz zawiera wszystko, czego aplikacja potrzebuje – system bazowy, zależności, konfigurację startową. Dzięki temu to samo środowisko działa na Windowsie, macOS i Linuksie, o ile tylko jest dostępny Docker.

Intuicja jest prosta: Docker to sposób na powtarzalność. Jeśli zbudujesz obraz raz i udostępnisz go zespołowi, każdy będzie widział aplikację w takiej samej wersji, z tymi samymi bibliotekami i ustawieniami.

Tradycyjna instalacja vs. kontener – lekka „wirtualka”, ale inna

Klasyczny sposób: instalujesz Pythona, Nginxa, bazę danych, biblioteki systemowe bezpośrednio w systemie operacyjnym. Po kilku miesiącach w systemie panuje chaos – nie wiesz, która aplikacja korzysta z której biblioteki, bo wszystko miesza się globalnie. Konflikty wersji to norma.

Kontener Dockera zachowuje się trochę jak lekka wirtualna maszyna, ale nie jest pełną wirtualką. Najważniejsze różnice:

  • Wirtualna maszyna ma własny system operacyjny (np. pełny Linux), jądro, sterowniki – jest ciężka w uruchomieniu i zużywa sporo zasobów.
  • Kontener współdzieli jądro systemu z gospodarzem. Korzysta z separacji na poziomie procesów i przestrzeni nazw, dzięki czemu jest lekki i startuje w ułamku sekundy.

W praktyce oznacza to, że możesz uruchomić na jednym serwerze dziesiątki kontenerów, a każdy będzie od siebie odizolowany: osobne procesy, sieć, system plików. Łatwiej też je usunąć – kasujesz kontener i po środowisku nie zostaje ślad.

Gdzie Docker naprawdę błyszczy: dev, testy, mikroserwisy, nauka

Najczęstsze, praktyczne zastosowania Dockera wyglądają tak:

  • Środowiska deweloperskie – każdy członek zespołu uruchamia tę samą bazę danych, ten sam serwer aplikacyjny i te same zależności w kontenerach.
  • Testy automatyczne – pipeline CI/CD (np. GitLab CI, GitHub Actions) odpala aplikację w kontenerze, uruchamia testy i po zakończeniu po prostu sprząta kontenery.
  • Mikroserwisy – każda usługa ma swój obraz, własne zależności, można je niezależnie aktualizować i wdrażać.
  • Nauka narzędzi – chcesz przetestować nową bazę danych albo serwer? Uruchamiasz gotowy obraz Dockera i nie brudzisz nic w systemie.

Dobry przykład z codziennej pracy: zespół ma komputery z Windowsem, macOS i różnymi dystrybucjami Linuksa. Zamiast pisać trzy różne instrukcje instalacji, przygotowuje jeden obraz Dockera. Każdy włącza kontener przez jedno polecenie i skupia się na kodzie, a nie na konfiguracji.

Kiedy Docker pomaga, a kiedy może przeszkadzać

Docker świetnie nadaje się do:

  • aplikacji serwerowych (HTTP API, strony WWW, mikrousługi),
  • bazy danych używanej lokalnie do dev/testów,
  • projektów, które mają wiele elementów (backend, frontend, kolejka, cache),
  • powtarzalnych zadań: np. okresowe generowanie raportów w tym samym środowisku.

Są jednak sytuacje, gdzie Docker tylko komplikuję sprawę:

  • Mały jednorazowy skrypt, który ma działać tylko na twoim komputerze, bez zależności – szybciej uruchomić go bez kontenera.
  • Aplikacje z ciężkim GUI (np. pełne programy desktopowe) – da się, ale jest to niewygodne dla początkującego.
  • Gdy nie znasz jeszcze podstaw systemu (uprawnienia, porty, sieć) – Docker może zamaskować problemy, zamiast je wyjaśnić.

Jeśli zadanie da się łatwo rozwiązać bez Dockera, a nie planujesz wdrożenia na serwer czy pracy w zespole – użycie kontenerów może być przerostem formy nad treścią. Ale w momencie, gdy projekt ma trafić na produkcję lub korzysta z kilku usług naraz, Docker zaczyna realnie oszczędzać czas.

Podstawowe pojęcia po ludzku: obrazy, kontenery, rejestry

Obraz Dockera – „zamrożone” środowisko aplikacji

Obraz Dockera to szablon, z którego powstają kontenery. Można go porównać do pliku ISO systemu operacyjnego albo do zamrożonego stanu środowiska. Zawiera:

  • bazowy system (np. minimalny Linux),
  • zainstalowane pakiety i zależności,
  • pliki aplikacji,
  • instrukcję, jaką komendę uruchomić na starcie (np. serwer HTTP).

Obrazy są budowane warstwowo: jeśli zmienisz jeden plik w projekcie, Docker nie musi budować wszystkiego od zera, ale dołoży kolejną warstwę. To przyspiesza kolejne buildy i pozwala współdzielić część warstw między różnymi obrazami.

Kontener – działająca instancja obrazu

Kontener to uruchomiona instancja obrazu. Analogią może być:

  • obraz (image) – plik ISO systemu,
  • kontener – konkretny, działający system z tego pliku ISO w maszynie wirtualnej.

Ten sam obraz możesz uruchomić wiele razy, tworząc kilka kontenerów. Każdy ma:

  • własny system plików (kopię warstw obrazu + swoje zmiany),
  • własne procesy,
  • własne interfejsy sieciowe i porty.

Gdy kasujesz kontener, zmiany wewnątrz zwykle znikają (chyba że używasz wolumenów). Obraz pozostaje w systemie i możesz tworzyć na jego podstawie kolejne kontenery.

Rejestry obrazów: Docker Hub i spółka

Obrazy muszą gdzieś „mieszkać”, żeby inni mogli je pobierać. Od tego są rejestry obrazów (registry). Najpopularniejszy to:

  • Docker Hub – publiczne repozytorium setek tysięcy obrazów, dostępne dla każdego.

Poza tym istnieją m.in.:

  • GitHub Container Registry,
  • GitLab Container Registry,
  • Amazon ECR, Google Artifact Registry, Azure Container Registry,
  • prywatne rejestry wewnątrz firm.

Domyślnie, gdy wpisujesz docker pull nginx, Docker kontaktuje się z Docker Hubem, pobiera obraz library/nginx i zapisuje go lokalnie.

Jak czytać nazwy obrazów: repozytorium i tag

Pełna nazwa obrazu ma postać:

rejestr/organizacja/repozytorium:tag

Kilka przykładów:

  • nginx:latest – domyślne repozytorium (Docker Hub), oficjalny obraz Nginx, tag latest.
  • python:3.11-slim – oficjalny Python, wersja 3.11, wariant „slim”.
  • mycompany/app-backend:1.0.0 – własny obraz z tagiem wersji.
  • registry.example.com/team/service:dev – obraz w prywatnym rejestrze.

Tag to etykieta wskazująca wersję lub wariant obrazu. Jeśli tagu brak (np. nginx), Docker przyjmuje domyślnie :latest. To wygodne do nauki, ale na produkcji bywa groźne – latest może się zmienić, a wraz z nim zachowanie aplikacji.

Oficjalne obrazy vs. obrazy społeczności

Na Docker Hubie widać dwa główne typy obrazów:

  • Oficjalne – utrzymywane we współpracy z twórcami danego projektu (np. nginx, mysql, python). Mają przejrzoną dokumentację, często lepszą kontrolę jakości.
  • Obrazy społeczności – tworzone przez użytkowników, firmy, projekty open source. Mogą być bardzo dobre, ale też przypadkowe.

Przy wybieraniu obrazu warto zwrócić uwagę na:

  • oznaczenie „Official Image” na Docker Hubie,
  • liczbę pobrań i „gwiazdek”,
  • aktualność (data ostatniego pushu),
  • opis i link do dokumentacji źródłowej.
Czerwony kontenerowiec zacumowany w porcie w Hamburgu pod dźwigami
Źródło: Pexels | Autor: Wolfgang Weiser

Szybki start: instalacja Dockera na Windows, macOS i Linux

Docker Desktop vs Docker Engine – co jest czym

Pod nazwą „Docker” kryją się dwa powiązane elementy:

  • Docker Engine – silnik kontenerów działający w tle (daemon). To on faktycznie uruchamia kontenery.
  • Docker CLI – narzędzie wiersza poleceń docker, którym rozmawiasz z silnikiem.

Na Linuksie zwykle instaluje się sam Docker Engine (plus CLI). Na Windows i macOS używa się Docker Desktop, który:

  • instaluje silnik kontenerów w lekkiej maszynie wirtualnej,
  • zapewnia graficzny interfejs do zarządzania kontenerami,
  • konfiguruje integrację z systemem (np. udostępnianie plików).

Wymagania wstępne i uwagi systemowe

Przed instalacją Docker Desktop na Windows/macOS przydaje się szybka kontrola:

  • system w wersji wspieranej (np. Windows 10/11 Pro lub Home z WSL2, aktualny macOS),
  • włączona w BIOS/UEFI sprzętowa wirtualizacja (Intel VT-x / AMD-V),
  • uprawnienia administratora (instalator musi wprowadzić zmiany w systemie).

Na Linuksie kluczowe jest:

  • konto z uprawnieniami sudo,
  • kompatybilna dystrybucja (np. Ubuntu, Debian, Fedora, CentOS, Arch – każda ma swoją instrukcję),
  • czasem dodanie użytkownika do grupy docker, by nie używać stale sudo.

Instalacja Dockera na Windows i macOS – zarys kroków

Na Windows i macOS wystarczą ogólne kroki:

  1. Pobierz Docker Desktop ze strony docker.com dla swojego systemu.
  2. Uruchom instalator i przejdź przez kreatora (zwykle „Next, Next, Finish”).
  3. Na Windows w razie potrzeby włącz WSL2 i funkcję „Virtual Machine Platform” (instalator może to zasugerować).
  4. Po zakończeniu instalacji uruchom Docker Desktop i poczekaj, aż ikona „w tacce” przestanie się animować – oznacza to gotowość.
  5. Otwórz terminal (PowerShell, cmd, Terminal na macOS) i sprawdź docker --version.

Jeśli wersja się wyświetli, Docker działa. Jeśli pojawia się błąd, zwykle Docker Desktop jeszcze nie wystartował albo polecenie nie jest w ścieżce systemowej (PATH).

Instalacja Dockera na Linuksie – przykład dla Ubuntu

Na Linuksie instalacja różni się w zależności od dystrybucji, ale na Ubuntu często wygląda to z grubsza tak (schematycznie):

  1. Odinstalowanie starej wersji, jeśli istnieje:
    sudo apt-get remove docker docker-engine docker.io containerd runc
  2. Dodanie oficjalnego repozytorium Dockera (skrótowo):
    sudo apt-get update
    sudo apt-get install ca-certificates curl gnupg
    # dodanie klucza GPG i repozytorium zgodnie z dokumentacją Dockera
    
  3. Instalacja Dockera:
    sudo apt-get install docker-ce docker-ce-cli containerd.io
  4. Dodanie użytkownika do grupy docker (opcjonalnie):
    sudo usermod -aG docker $USER

Po ponownym zalogowaniu możesz używać docker bez sudo (jeżeli dodałeś się do grupy).

Test instalacji: docker –version i hello-world

Szybka checklista po instalacji:

  • Sprawdzenie wersji:
    docker --version
  • Uruchomienie testowego kontenera:
    docker run hello-world

Po komendzie docker run hello-world Docker:

Co faktycznie robi docker run hello-world

Uruchomienie:

docker run hello-world

wykonuje kilka rzeczy krok po kroku:

  1. Docker sprawdza, czy lokalnie istnieje obraz hello-world.
  2. Jeśli go nie ma, łączy się z domyślnym rejestrem (Docker Hub) i pobiera obraz.
  3. Tworzy nowy kontener na podstawie tego obrazu.
  4. Uruchamia w nim komendę startową (w tym przypadku – prosty program wypisujący komunikaty).
  5. Po zakończeniu działania proces w kontenerze się kończy, a sam kontener przechodzi w stan „exited”.

To drobiazgowy test: sprawdza pobieranie obrazów, tworzenie kontenerów i komunikację z silnikiem Dockera. Jeśli widzisz przyjazny komunikat „Hello from Docker!”, można przejść dalej.

Pierwszy kontakt z kontenerami: docker run krok po kroku

Najprostszy możliwy kontener

Dobrym startem jest uruchomienie czegoś bardzo prostego, np. obrazu alpine – minimalnej dystrybucji Linuksa:

docker run alpine echo "Cześć z kontenera"

Co tutaj się dzieje:

  • alpine – nazwa obrazu, który ma zostać użyty,
  • echo "Cześć z kontenera" – komenda, którą Docker uruchomi wewnątrz kontenera.

Docker pobierze obraz (jeśli go nie ma), stworzy kontener, uruchomi komendę i wypisze jej wynik w Twoim terminalu. Kontener po zakończeniu działania komendy się wyłączy.

Tryb interaktywny: mała „maszyna” w terminalu

Kontener może działać jak osobna, maleńka maszyna, do której można się „wlogować”:

docker run -it alpine sh

Użyte opcje:

  • -i – tryb interaktywny (stdin pozostaje otwarty),
  • -t – pseudoterminal, dzięki czemu powłoka zachowuje się normalnie,
  • sh – powłoka, którą uruchamiamy w kontenerze.

Po tej komendzie trafisz do promptu wewnątrz kontenera, np.:

/ #

Możesz uruchamiać komendy Linuksa:

uname -a
ls
cat /etc/os-release

Aby wyjść, wpisz exit lub użyj skrótu Ctrl+D. Kontener zakończy działanie.

Lista działających i zakończonych kontenerów

Żeby zobaczyć, co teraz żyje w Dockerze, przydają się dwie komendy:

  • tylko działające kontenery:
    docker ps
  • wszystkie, także zakończone:
    docker ps -a

W tabeli zobaczysz m.in. identyfikator kontenera, użyty obraz, komendę startową, status oraz mapowanie portów.

Zatrzymywanie, uruchamianie i usuwanie kontenerów

Jeśli kontener nadal działa w tle (np. serwer), można nim sterować jak procesem:

  • zatrzymanie działającego kontenera:
    docker stop NAZWA_LUB_ID
  • ponowne uruchomienie zatrzymanego (status Exited):
    docker start NAZWA_LUB_ID
  • usunięcie kontenera:
    docker rm NAZWA_LUB_ID

Nazwę lub ID możesz wziąć z docker ps -a. Docker sam nadaje losowe nazwy, jeśli nie podasz swojej (czasem dość zabawne połączenia słów).

Daemon a jednorazowe procesy – różne style pracy kontenera

W praktyce spotykasz dwa typy kontenerów:

  • krótkotrwałe – uruchamiają jedną komendę, po jej zakończeniu wychodzą (np. migrują bazę, generują raport),
  • długotrwałe – uruchamiają usługę (serwer HTTP, bazę danych) i działają, dopóki ich nie zatrzymasz.

W kolejnych przykładach skupimy się głównie na kontenerach z usługami, bo to najczęstszy scenariusz przy aplikacjach webowych.

Kobieta kupuje bilet w automacie biletowym za pomocą karty płatniczej
Źródło: Pexels | Autor: Ono Kosuki

Praca z obrazami: docker pull, docker images, docker rmi

Pobieranie obrazów: docker pull

Obraz można pobrać „na zapas”, zanim zacznie się z niego korzystać:

docker pull nginx:latest

Ta komenda:

  • łączy się z rejestrem (tu: Docker Hub),
  • pobiera warstwy obrazu,
  • zapisuje go lokalnie, tak by kolejne uruchomienia były szybkie.

Jeśli pominiesz tag, Docker przyjmie :latest:

docker pull nginx

Przegląd lokalnych obrazów

Po kilku testach w systemie pojawia się kolekcja obrazów. Można ją podejrzeć:

docker images

Wynik to tabela z m.in. nazwą repozytorium, tagiem, rozmiarem oraz ID obrazu. To Twój lokalny magazyn klocków, z których powstają kontenery.

Usuwanie niepotrzebnych obrazów: docker rmi

Z czasem obrazy zaczynają zajmować sporo miejsca. Jeżeli coś już nie jest potrzebne, można to usunąć:

docker rmi nginx:latest

lub używając ID obrazu:

docker rmi ID_OBRAZU

Obrazów używanych aktualnie przez działające kontenery nie da się usunąć, dopóki nie zatrzymasz i nie usuniesz tych kontenerów. Czasami wygodny jest też automatyczny „odkurzacz”:

docker image prune

który usuwa obrazy, do których nie ma już żadnych referencji (tzw. „dangling images”).

Sprawdzanie szczegółów obrazu

Jeśli chcesz podejrzeć, co dokładnie zawiera obraz (np. jakie ma zmienne środowiskowe, jaki jest domyślny port), możesz użyć:

docker inspect nginx:latest

To pokaże spory JSON z metadanymi. Przydaje się, gdy trzeba sprawdzić parametr bez zaglądania do dokumentacji na stronie rejestru.

Twoja pierwsza aplikacja w Dockerze w 15 minut

Plan: prosty serwer HTTP w kontenerze

Najprostszy, a jednocześnie bardzo praktyczny przykład to miniaturowa aplikacja webowa. Można ją napisać w Pythonie, Node.js, Go czy czymkolwiek innym. Dla czytelności użyjemy Pythona z wbudowanym modułem http.server, dzięki czemu nie trzeba instalować dodatkowych bibliotek.

Przygotowanie katalogu projektu

Najpierw utwórz katalog na dysku, w którym będą pliki aplikacji:

mkdir moja-pierwsza-aplikacja
cd moja-pierwsza-aplikacja

Dodaj prosty plik HTML, który będziemy serwować:

echo "<h1>Docker działa!</h1>" > index.html

W katalogu powinien teraz leżeć plik index.html z prostą zawartością.

Serwer HTTP „na żywo” w kontenerze

Zamiast instalować serwer na swoim systemie, można skorzystać z gotowego obrazu Pythona i uruchomić serwer bezpośrednio w kontenerze:

docker run --rm -it 
  -p 8000:8000 
  -v "$PWD":/usr/src/app 
  -w /usr/src/app 
  python:3.11-slim 
  python -m http.server 8000

Rozbijmy to na części:

  • --rm – po wyłączeniu kontenera usuń go automatycznie (nie będzie śmieci),
  • -p 8000:8000 – mapowanie portów: port 8000 kontenera jest widoczny jako port 8000 na Twoim komputerze,
  • -v "$PWD":/usr/src/app – podmontowanie bieżącego katalogu jako /usr/src/app w kontenerze,
  • -w /usr/src/app – ustawienie katalogu roboczego kontenera na /usr/src/app,
  • python:3.11-slim – obraz bazowy, który ma zostać użyty,
  • python -m http.server 8000 – komenda startowa uruchamiająca serwer HTTP na porcie 8000.

Teraz w przeglądarce wejdź na adres:

http://localhost:8000

Powinieneś zobaczyć napis „Docker działa!”. Serwer fizycznie działa w kontenerze, a Ty tylko przeglądasz jego wynik.

Porty: jak udostępnić kontener na zewnątrz

Bez opcji -p kontener działałby „w środku” i nie byłby dostępny z Twojej przeglądarki. Parametr:

-p HOST_PORT:CONTAINER_PORT

spina port Twojego komputera (hosta) z portem wewnątrz kontenera. Praktyczne przykłady:

  • -p 8080:80 – ruch na localhost:8080 trafia na kontenerowy port 80 (typowe przy serwerach WWW),
  • -p 5432:5432 – wystawia bazę PostgreSQL na ten sam port, co zwykle.

W środowiskach produkcyjnych często to reverse proxy (np. Nginx) łączy się z kontenerami po wewnętrznej sieci Dockera, ale na start -p w zupełności wystarcza.

Udostępnianie plików: wolumeny i bind mounty

Opcja -v podmontowała Twój katalog do środka kontenera. Dzięki temu:

  • pliki edytujesz lokalnie w ulubionym edytorze,
  • serwer w kontenerze od razu widzi zmiany (bo to ten sam katalog).

Taki sposób montowania (mapowanie konkretnego katalogu z hosta) nazywa się bind mountem. Istnieją też wolumeny zarządzane przez Dockera, które nadają się do trwałych danych, np. baz. Do pierwszych eksperymentów w zupełności wystarczy bind mount.

Automatyczne restartowanie i tryb „daemonowy”

Serwer przed chwilą działał na pierwszym planie – dopóki nie przerwiesz go w terminalu (Ctrl+C). Można też uruchomić kontener w tle:

docker run -d --name moj-serwer 
  -p 8000:8000 
  -v "$PWD":/usr/src/app 
  -w /usr/src/app 
  python:3.11-slim 
  python -m http.server 8000

Nowe elementy:

  • -d – tryb „detached”, czyli uruchom kontener w tle,
  • --name moj-serwer – nadanie własnej nazwy kontenerowi (łatwiej się nim zarządza).

Teraz można kontenerem sterować:

docker ps                # zobacz, czy działa
docker stop moj-serwer   # zatrzymaj
docker start moj-serwer  # uruchom ponownie
docker rm moj-serwer     # usuń, gdy już niepotrzebny
Pasażer na tylnym siedzeniu taksówki płaci kartą w terminalu
Źródło: Pexels | Autor: Tim Samuel

Dockerfile od kuchni: jak opisać swoją aplikację

Dlaczego sam docker run to za mało

Przed chwilą korzystałeś z gotowego obrazu python:3.11-slim i ręcznie montowałeś kod z hosta. W projekcie, który ma wylądować na serwerze, taki układ się nie sprawdza. Potrzebny jest powtarzalny przepis na obraz aplikacji: krok po kroku, jak ją zbudować i uruchomić. Tym przepisem jest plik Dockerfile.

Najprostszy możliwy Dockerfile

Pozostańmy przy prostym serwerze Pythona, ale tym razem umieśćmy wszystko w obrazie. W katalogu projektu (moja-pierwsza-aplikacja) utwórz plik app.py:

cat << 'EOF' > app.py
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer

PORT = 8000

handler = SimpleHTTPRequestHandler

with TCPServer(("", PORT), handler) as httpd:
    print(f"Serwer działa na porcie {PORT}")
    httpd.serve_forever()
EOF

Teraz stwórz plik Dockerfile:

FROM python:3.11-slim

WORKDIR /app

COPY . /app

EXPOSE 8000

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

Co oznaczają instrukcje w Dockerfile

Każda linijka pełni tu konkretną rolę:

  • FROM python:3.11-slim – wybór obrazu bazowego. To punkt startu: system + zainstalowany Python.
  • WORKDIR /app – ustawienie katalogu roboczego wewnątrz obrazu. Wszystkie kolejne instrukcje będą wykonywane względem /app.
  • COPY . /app – skopiowanie bieżącego katalogu (kodu aplikacji) do katalogu /app wewnątrz obrazu.
  • Budowanie własnego obrazu: docker build

    Gdy plik Dockerfile jest gotowy, można z niego stworzyć własny obraz. Będąc w katalogu projektu, uruchom:

    docker build -t moja-pierwsza-aplikacja:latest .

    Parametry układają się w prosty schemat:

  • -t moja-pierwsza-aplikacja:latest – nadanie nazwy (i tagu) budowanemu obrazowi,
  • . – kontekst budowania, czyli katalog, z którego Docker ma wziąć pliki (tu: bieżący katalog).

Docker przeanalizuje Dockerfile, krok po kroku zbuduje obraz i zapisze go lokalnie. Po chwili nowa pozycja powinna pojawić się w:

docker images

Uruchamianie własnego obrazu

Gotowego obrazu używa się już tak samo jak oficjalnych z Docker Hub. Przykład:

docker run --rm -p 8000:8000 moja-pierwsza-aplikacja:latest

Po kilku sekundach serwer powinien nasłuchiwać na porcie 8000. Wejdź w przeglądarce na:

http://localhost:8000

Żadnego montowania katalogów, żadnego ręcznego instalowania zależności – wszystko siedzi w obrazie. W praktyce oznacza to, że ten sam obraz możesz zanieść na inny komputer (albo do chmury) i aplikacja zachowa się tak samo.

Warstwy obrazu i cache: dlaczego kolejny build jest szybszy

Obraz powstaje z warstw, a każda instrukcja w Dockerfile tworzy kolejną. Gdy budujesz obraz ponownie, Docker sprawdza, które warstwy się nie zmieniły, i używa ich z pamięci podręcznej.

Prosty schemat:

  • zmienisz tylko jedną linijkę w app.py – przebudowana będzie wyłącznie warstwa z COPY i wszystko po niej,
  • podmienisz wersję Pythona w FROM – invalidate’ujesz (unieważniasz) cały cache, build potrwa dłużej.

Dzięki temu w codziennej pracy kolejne przebudowy są szybkie, o ile Dockerfile jest ułożony sensownie.

Jak układać Dockerfile, żeby budował się szybko

Kolejność instrukcji ma znaczenie. Warto zacząć od rzeczy, które rzadko się zmieniają, a dopiero na końcu kopiować kod aplikacji. Typowy wzorzec przy większych projektach Pythonowych wygląda na przykład tak:

FROM python:3.11-slim

WORKDIR /app

# Najpierw zależności (rzadziej się zmieniają)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Dopiero potem reszta kodu (częściej się zmienia)
COPY . .

EXPOSE 8000
CMD ["python", "app.py"]

Dzięki temu drobna poprawka w kodzie nie wymusza ponownego instalowania paczek Pythona przy każdym buildzie.

Ignorowanie zbędnych plików: .dockerignore

Podczas budowania Docker wysyła do demona Dockera cały kontekst – czyli pliki z katalogu, który wskazałeś (zwykle .). Jeśli w projekcie są logi, katalog node_modules czy inne śmieci, build staje się wolniejszy, a obraz rośnie bez potrzeby.

Rozwiązaniem jest plik .dockerignore, podobny do .gitignore. Przykład:

.git
.gitignore
__pycache__/
*.pyc
.env
*.log
node_modules/
dist/
build/

Te wpisy mówią Dockerowi, że plików i katalogów z listy nie trzeba brać pod uwagę przy budowaniu obrazu. Zyskujesz mniejszy kontekst, szybszy upload do demona i czystszy obraz.

Zmienne środowiskowe i konfiguracja: ENV i ARG

W obrazie wygodniej jest trzymać mechanizm aplikacji niż jej konfigurację. Ustawienia (hasła, adresy baz, tryb debug) dobrze jest wstrzykiwać przez zmienne środowiskowe.

W Dockerfile można ustawić domyślne wartości:

ENV APP_ENV=production
ENV APP_PORT=8000

W Pythonie masz do nich dostęp przez os.environ. Z kolei podczas uruchamiania kontenera wartości można nadpisać:

docker run -e APP_ENV=dev -e APP_PORT=5000 
  -p 5000:5000 moja-pierwsza-aplikacja:latest

Istnieją też zmienne ARG, używane tylko podczas budowania obrazu. Przykład:

ARG PYTHON_VERSION=3.11-slim
FROM python:${PYTHON_VERSION}

W czasie builda możesz podać inną wersję:

docker build --build-arg PYTHON_VERSION=3.10-slim 
  -t moja-apka:py310 .

Instrukcje CMD i ENTRYPOINT: co naprawdę startuje kontener

Na końcu Dockerfile zwykle znajdują się dwie instrukcje odpowiedzialne za komendę startową:

  • CMD – domyślna komenda, którą można łatwo nadpisać przy docker run,
  • ENTRYPOINT – „główne wejście”, trudniej nadpisywalne, przydaje się w narzędziach CLI.

Przykładowy zestaw:

ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8000"]

Po uruchomieniu kontenera Docker złoży z tego jedną komendę:

python app.py --port 8000

Jeśli wywołasz:

docker run moja-pierwsza-aplikacja:latest --port 9000

to część z CMD zostanie zastąpiona przez Twoje argumenty, ale ENTRYPOINT pozostanie bez zmian. W zwykłych aplikacjach webowych często wystarcza samo CMD. ENTRYPOINT świeci pełnym blaskiem przy narzędziach typu linia poleceń (np. własny wrapper do migracji bazy).

Obrazy „ciężkie” i „lekkie”: dobór bazowego FROM

Za kulisami obrazy to po prostu systemy plików z niezbędnymi komponentami. Można użyć pełnego systemu (np. ubuntu), można też postawić na wersje „odchudzone” (slim, alpine).

Przykładowe warianty:

  • python:3.11 – pełny obraz Debiana z Pythonem, wygodny do debugowania, ale większy,
  • python:3.11-slim – przycięty Debian, dobry kompromis między wagą a wygodą,
  • python:3.11-alpine – bardzo mały, oparty na Alpine Linux, czasem wymaga dopisywania dodatkowych pakietów systemowych.

W praktycznych projektach często startuje się na slim, a po opanowaniu zależności schodzi niżej do jeszcze lżejszych baz, jeśli jest taka potrzeba.

Minimalne obrazy produkcyjne: multi-stage build

Podczas budowania aplikacji potrzebne są narzędzia, których w środowisku produkcyjnym w ogóle nie trzeba. Po co wozić z aplikacją kompilatory i build toolsy, jeśli kontener ma tylko serwować HTTP?

Rozwiązaniem jest multi-stage build, czyli budowanie w kilku etapach. Prosty przykład (Node.js, ale idea jest uniwersalna):

# Etap 1: build
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Etap 2: obraz runtime
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
RUN npm install --production
EXPOSE 3000
CMD ["node", "dist/server.js"]

W pierwszym etapie powstaje skompilowana aplikacja. W drugim – budowany jest finalny, dużo mniejszy obraz, który zawiera tylko to, co potrzebne do uruchomienia. Ten wzorzec świetnie sprawdza się przy frontendach, aplikacjach Javy, Go, a także przy Pythonie (np. gdy trzeba najpierw skompilować rozszerzenia w C).

Debugowanie obrazu: interaktywna powłoka w kontenerze

Czasem coś nie działa i trzeba zerknąć do środka obrazu: sprawdzić ścieżki, prawa do plików, zmienne środowiskowe. Najprostszy sposób to uruchomienie powłoki w kontenerze:

docker run --rm -it moja-pierwsza-aplikacja:latest /bin/sh

albo, jeśli jest dostępny Bash:

docker run --rm -it moja-pierwsza-aplikacja:latest /bin/bash

Po wejściu do takiego środka można używać klasycznych komend Linuksa: ls, cat, env, sprawdzić, czy plik naprawdę istnieje tam, gdzie zakładałeś, czy port jest odsłuchiwany, czy logi pojawiają się w odpowiednim miejscu.

Logi z kontenera: gdzie trafia output aplikacji

Standardowy output (to, co printujesz albo wypisujesz na stdout) nie znika w próżni. Docker przechwytuje te dane i traktuje jako logi kontenera. Z zewnątrz widać je tak:

docker logs moj-serwer

Można też śledzić je na żywo:

docker logs -f moj-serwer

Warto więc, by Twoja aplikacja logowała na standardowe wyjście, a nie do plików w środku kontenera. Operatory logów (czyli systemy, które je zbierają) później i tak przyczepią się do tego strumienia.

Tagowanie obrazów: wersje, środowiska, architektury

Nazwa obrazu z początku może wyglądać niewinnie (moja-pierwsza-aplikacja:latest), ale z czasem dobrze jest wprowadzić porządek w tagach. Kilka praktycznych konwencji:

  • moja-apka:1.0.0 – powiązanie z wersją aplikacji,
  • moja-apka:1.0.0-dev, moja-apka:1.0.0-prod – warianty na różne środowiska,
  • moja-apka:1.0.0-amd64, moja-apka:1.0.0-arm64 – różne architektury procesorów (przydatne przy Raspberry Pi czy M1/M2).

Podczas builda tag można nadać lub zmienić:

docker build -t moja-apka:1.0.0 .

A istniejący obraz otagować ponownie:

docker tag moja-apka:1.0.0 moja-apka:latest

Wysyłanie obrazów do rejestru: docker push

Lokalny obraz jest użyteczny tylko na Twoim komputerze. Aby inne maszyny (lub serwery CI/CD) mogły go pobrać, trzeba go umieścić w rejestrze – np. Docker Hub czy GitHub Container Registry.

Ogólny schemat wygląda następująco:

  1. Tworzysz konto w wybranym rejestrze (np. Docker Hub).
  2. Logujesz się z linii komend:
    docker login
  3. Tagujesz obraz tak, aby zawierał nazwę rejestru i użytkownika:
    docker tag moja-pierwsza-aplikacja:latest 
      nazwa_uzytkownika/moja-pierwsza-aplikacja:latest
  4. Wysyłasz obraz:
    docker push nazwa_uzytkownika/moja-pierwsza-aplikacja:latest

Od tej chwili z innej maszyny wystarczy:

docker pull nazwa_uzytkownika/moja-pierwsza-aplikacja:latest
docker run -p 8000:8000 nazwa_uzytkownika/moja-pierwsza-aplikacja:latest

I ten sam serwer HTTP uruchomi się w kilka sekund, bez kopiowania kodu przez pendrive’a czy SSH.

Bezpieczeństwo obrazów: zależności i użytkownik w kontenerze

Kontener nie jest magiczną tarczą ochronną. Jeśli aplikacja ma luki, kontener je po prostu niesie ze sobą. Warto zwracać uwagę na kilka prostych aspektów:

  • regularnie odświeżać obrazy bazowe (nowszy python:3.11-slim to zwykle załatane biblioteki systemowe),
  • instalować wyłącznie potrzebne pakiety (mniej pakietów = mniejsza powierzchnia ataku),
  • nie uruchamiać aplikacji jako root w kontenerze, jeśli nie jest to konieczne.

Przykład uruchomienia aplikacji jako nieuprzywilejowany użytkownik:

FROM python:3.11-slim

WORKDIR /app

RUN useradd -m appuser
COPY . /app
RUN chown -R appuser:appuser /app

USER appuser

EXPOSE 8000
CMD ["python", "app.py"]

Dzięki temu ewentualne przejęcie kontroli nad procesem daje atakującemu znacznie mniejsze możliwości niż w przypadku kontenera działającego jako root.

Kluczowe Wnioski

  • Docker rozwiązuje problem „u mnie działa” przez spakowanie aplikacji razem z całym środowiskiem (system bazowy, biblioteki, konfiguracja), dzięki czemu ten sam obraz działa tak samo na Windowsie, macOS i Linuksie.
  • Kontener jest lekką alternatywą dla wirtualnej maszyny: współdzieli jądro systemu z gospodarzem, startuje w ułamku sekundy i zużywa mniej zasobów, a jednocześnie zapewnia izolację procesów, sieci i systemu plików.
  • Docker szczególnie pomaga w złożonych projektach: środowiskach deweloperskich, testach automatycznych, mikroserwisach i szybkiej nauce nowych narzędzi, gdzie kluczowa jest powtarzalność i łatwość sprzątania po eksperymentach.
  • Nie każde zadanie wymaga kontenera – małe, jednorazowe skrypty czy aplikacje z ciężkim interfejsem graficznym często szybciej i prościej uruchomić bez Dockera, zwłaszcza gdy projekt nie trafi na serwer ani do zespołu.
  • Obraz Dockera to „zamrożone” środowisko aplikacji budowane warstwowo; zmiana jednego pliku nie wymaga przebudowy wszystkiego, co przyspiesza kolejne buildy i pozwala współdzielić wspólne warstwy między różnymi obrazami.
  • Kontener jest działającą instancją obrazu: możesz uruchomić wiele kontenerów z jednego obrazu, każdy z własnym systemem plików i procesami, a po ich usunięciu środowisko znika bez śladu w systemie gospodarza.
  • Źródła

  • Docker Overview. Docker Inc. – Podstawowe pojęcia: obrazy, kontenery, rejestry, zastosowania Dockera
  • Docker Engine Overview. Docker Inc. – Architektura Dockera, różnice kontenery vs tradycyjne uruchamianie aplikacji
  • Kubernetes Documentation – Containers. Cloud Native Computing Foundation – Definicje kontenerów, izolacja procesów, porównanie z VM