Docker dla początkujących: uruchom pierwszą aplikację w kontenerze

0
55
4/5 - (2 votes)

Nawigacja:

O co chodzi z Dockerem i kontenerami – obrazowe wprowadzenie

Problem „u mnie działa” i skąd się wziął Docker

Każdy, kto choć chwilę programował lub wdrażał aplikacje, prędzej czy później trafia na klasyk: „u mnie działa”. Na komputerze dewelopera wszystko jest w porządku, testy przechodzą, aplikacja startuje w sekundę. Na serwerze testowym – wyjątki, błędy, dziwne zachowania. Na produkcji – już lepiej nie mówić.

Źródło problemu jest zwykle bardzo proste: inne środowisko. Inna wersja Pythona, Javy czy Node.js, brakująca biblioteka systemowa, różne wydania tej samej bazy danych, inne zmienne środowiskowe, inny system operacyjny. Te drobiazgi składają się na jeden efekt: kod, który powinien działać tak samo, zachowuje się inaczej na każdym serwerze.

Docker powstał właśnie po to, żeby ten chaos uporządkować. Zamiast tłumaczyć się: „zainstaluj to, doinstaluj tamto, użyj takiej wersji”, można przygotować kontener, który zawiera wszystko, czego aplikacja potrzebuje: runtime, biblioteki, konfigurację, skrypty startowe. Potem taki kontener można uruchamiać praktycznie wszędzie, gdzie działa Docker, z dużą szansą, że efekt będzie powtarzalny.

Prosta metafora kontenera (pudełka z aplikacjami)

Najprostsza metafora Dockerowego kontenera to gotowe pudełko z aplikacją. W środku masz:

  • samą aplikację (kod + skompilowane pliki),
  • wszystkie potrzebne biblioteki,
  • konkretną wersję środowiska (np. Node 18, a nie „jakiś Node”),
  • ustaloną strukturę katalogów,
  • komendę, która startuje aplikację.

Odbiorca nie musi znać zawartości pudełka w szczegółach. Wystarczy, że umie „pudełko” uruchomić. To w praktyce oznacza: ma zainstalowanego Dockera i wie, jak wydać proste polecenie docker run. Cała reszta – konfiguracja środowiska, zależności, drobne niuanse systemowe – jest ukryta w obrazie kontenera.

W tradycyjnej instalacji aplikacji trzeba osobno instalować pakiety, biblioteki, sprawdzać zgodność wersji, aktualizować system. W kontenerze aplikacja i środowisko są tym samym produktem. Ty uruchamiasz obraz, Docker tworzy kontener, a cała magia dzieje się „w pudełku”.

Tradycyjna instalacja vs kontener – gdzie jest różnica

Przy klasycznej instalacji:

  • instalujesz pakiety bezpośrednio w systemie (np. przez apt, yum, instalator MSI),
  • wszystko trafia do wspólnego systemu plików (biblioteki współdzielone),
  • instalacja jednej biblioteki w wersji X może zepsuć inną aplikację, która wymaga wersji Y,
  • upgrade systemu potrafi „przewrócić” produkcję, jeśli coś zmieni się w API lub konfiguracji.

Przy Dockrze:

  • tworzysz obraz, który ma swoją, odizolowaną przestrzeń plików,
  • wersje bibliotek są określone wewnątrz obrazu,
  • możesz mieć obok siebie wiele aplikacji korzystających z różnych wersji tego samego runtime’u,
  • aktualizacje systemu gospodarza w dużo mniejszym stopniu mieszają w środowisku aplikacji.

Różnica sprowadza się do tego, że kontener jest samowystarczalny. System gospodarza (host) „dorzuca” jedynie jądro systemu i zasoby (CPU, RAM, sieć, dysk). Cała reszta żyje w izolowanym środowisku.

Jak Docker radzi sobie z zależnościami i wersjami

Docker wykorzystuje obrazy jako niezmienne szablony. Obraz jest zbudowany w oparciu o plik Dockerfile, gdzie krok po kroku określasz:

  • na jakim obrazie bazowym się opierasz (np. node:18-alpine),
  • jakie pakiety doinstalować,
  • jak skopiować kod aplikacji,
  • jak przygotować konfigurację.

Efekt końcowy to plik-„matrioszka” zawierający wszystko, co jest potrzebne. Gdy budujesz obraz (komenda docker build), Docker tworzy warstwy, które są zapamiętywane. Budowa kolejna raz z tymi samymi krokami nie musi ściągać i instalować wszystkiego od zera – wykorzystuje cache.

Dzięki temu:

  • łatwo kontrolujesz dokładne wersje (obrazy bazowe, biblioteki, runtime),
  • jeśli coś działa, obraz możesz „zamrozić” w konkretnej wersji taga,
  • łatwo cofnąć się do poprzedniego obrazu, jeśli nowszy jest problematyczny.

Gdzie Docker spotyka się na co dzień

Kontenery z Dockerem weszły pod strzechy szybciej, niż niejeden framework. Najczęstsze miejsca, gdzie je spotkasz:

  • projekty open source – wiele popularnych narzędzi ma gotowe obrazy na Docker Hub, które pozwalają uruchomić całość jednym poleceniem,
  • praca zespołowa – zamiast kilkunastostronicowej instrukcji „jak postawić środowisko”, nowy programista odpala docker compose up i po kilku minutach ma cały ekosystem,
  • CI/CD – narzędzia typu GitLab CI, GitHub Actions czy Jenkins bardzo często budują, testują i wdrażają aplikacje w kontenerach,
  • środowiska testowe – można w parę minut podnieść osobny stack do eksperymentów, bez brudzenia systemu lokalnego.

Krótko: jeśli planujesz rozwój w stronę backendu, DevOps, czy po prostu chcesz sprawniej stawiać i przenosić aplikacje, Docker zacznie pojawiać się na Twojej drodze bardzo szybko.

Kontener vs maszyna wirtualna – co faktycznie się uruchamia

Architektura – gdzie w tym wszystkim jest Docker Engine

Żeby dobrze zrozumieć, czym różni się kontener od maszyny wirtualnej, przyda się prosty „schemat słowny”. Pomińmy szczegóły, a skupmy się na ogólnej architekturze.

W przypadku maszyny wirtualnej (VM) wygląda to tak:

  • sprzęt (fizyczny komputer/serwer),
  • system operacyjny gospodarza (host),
  • hypervisor (np. VirtualBox, Hyper-V, VMware),
  • wewnątrz – pełny system operacyjny gościa (guest OS),
  • na nim dopiero Twoje aplikacje.

W przypadku kontenerów Docker:

  • sprzęt,
  • system operacyjny gospodarza,
  • Docker Engine (demon Dockera),
  • na nim wiele izolowanych kontenerów, które współdzielą jądro systemu hosta.

Docker Engine działa jako usługa (demon), która:

  • zarządza obrazami (pobiera, przechowuje, usuwa),
  • tworzy i uruchamia kontenery,
  • izoluje ich system plików, sieć i procesy,
  • komunikuje się z Tobą przez CLI (polecenia docker ...).

Wspólne jądro systemu vs osobny system – konsekwencje

Kluczowa różnica: kontenery korzystają z tego samego jądra systemu co host, natomiast maszyny wirtualne mają własne jądro i pełny system operacyjny. Co to oznacza praktycznie?

  • kontener startuje szybko, bo nie musi uruchamiać całego systemu operacyjnego – startuje po prostu nowy proces w odizolowanym środowisku,
  • VM musi „postawić” pełny system (boot, usługi, logowanie itd.), więc start trwa dłużej,
  • kontener ma mniejszy narzut na zasoby (RAM, CPU),
  • VM daje silniejszą izolację – ma własne jądro, sterowniki, pełne odseparowanie.

W praktyce kontener jest bliżej „procesu z supermocami” niż małej wirtualnej maszyny. Nadal jest odizolowany (namespaces, cgroups), ale dużo lżejszy.

Wydajność i zużycie zasobów

Dzięki temu, że kontenery nie mają własnego jądra:

  • startują w ułamku sekundy,
  • można ich uruchomić dziesiątki na tym samym serwerze, gdzie kilka VM-kek już zaczyna się męczyć,
  • koszt „pustego” kontenera jest bardzo niski – to niemal tylko dodatkowy proces.

Maszyna wirtualna to natomiast:

  • kilkaset MB – kilka GB RAM „na jałowym biegu” (system, usługi),
  • dłuższy start systemu,
  • własne sterowniki i stos sieciowy, co czasem bywa zaletą, ale kosztuje zasoby.

Przy typowych projektach developerskich kontenery dają dużo lepszy stosunek możliwości do zużycia zasobów. Łatwiej też nimi zarządzać – można je szybko tworzyć i kasować bez większego żalu.

Kiedy kontener, a kiedy wirtualka ma więcej sensu

Kontenery sprawdzają się szczególnie w sytuacjach:

  • usług sieciowych (API, backend, workers),
  • mikroserwisów,
  • krótkotrwałych zadań (joby, migracje danych),
  • środowisk testowych i deweloperskich.

Maszyny wirtualne wciąż mają swoje zastosowania:

  • potrzebujesz innego systemu operacyjnego (np. testy na Windows Server, a host to Linux),
  • chcesz bardzo silnej izolacji bezpieczeństwa (np. różne działy firmy, różne systemy o wysokich wymaganiach),
  • musisz użyć specyficznych sterowników lub funkcji jądra niedostępnych na hoście.

Docker nie ma ambicji zastąpić wszystkich maszyn wirtualnych. To po prostu inne narzędzie, wygodniejsze w codziennym użyciu do aplikacji server-side i środowisk developerskich.

Mit: „Docker to VM w ładnym opakowaniu”

Częsty mit brzmi: „Docker to po prostu maszyna wirtualna z ładnym interfejsem”. To nieprawda z kilku powodów:

  • kontener nie ma pełnego systemu operacyjnego,
  • obraz kontenera to głównie system plików i konfiguracja, nie całe jądro i sterowniki,
  • kontenery korzystają bezpośrednio z jądra hosta,
  • narzut wydajnościowy jest znacznie mniejszy niż przy pełnej wirtualizacji.

Docker oczywiście może korzystać z maszyn wirtualnych jako z „warstwy pośredniej” – tak jest np. na Windows i macOS, gdzie pod spodem działa lekka maszyna z Linuxem. Kontener pozostaje jednak lżejszym, procesowym bytem, a nie pełnoprawną maszyną.

Dłonie piszące na klawiaturze laptopa w ciemnym pomieszczeniu
Źródło: Pexels | Autor: Sora Shimazaki

Przygotowanie środowiska – instalacja Dockera na popularnych systemach

Sprawdzenie wymagań systemu przed instalacją

Zanim pojawi się pierwsze docker run, dobrze przebrnąć przez krótki checklist:

  • prawa administratora – na Windows i macOS potrzebujesz możliwości instalacji oprogramowania systemowego; na Linuxie dostępu do sudo,
  • włączona wirtualizacja sprzętowa – Intel VT-x/AMD-V w BIOS/UEFI (szczególnie ważne na Windows),
  • system 64-bitowy – Docker na desktop wymaga 64-bit,
  • wspierana wersja systemu – aktualne Windows 10/11, macOS wciąż w supportcie, na Linuxie najłatwiej z Ubuntu/Debianem/Fedorą.

Szybkie sprawdzenie wirtualizacji na Windows: w Menedżerze zadań (Ctrl+Shift+Esc) na zakładce „Wydajność” w sekcji CPU powinna pojawić się informacja „Wirtualizacja: Włączona”. Jeśli jest „Wyłączona”, trzeba ją aktywować w BIOS/UEFI.

Windows – Docker Desktop i WSL2

Na Windows standardem jest Docker Desktop, który w nowszych wydaniach korzysta z WSL2 (Windows Subsystem for Linux). Kroki w skrócie:

  1. Upewnij się, że masz włączoną funkcję WSL2 i „Platforma maszyn wirtualnych” (Panel sterowania → Programy i funkcje → Włącz lub wyłącz funkcje systemu Windows).
  2. Zainstaluj dystrybucję Linuxa dla WSL (np. Ubuntu) z Microsoft Store – Docker Desktop zwykle sam zaproponuje tę operację.
  3. Pobierz instalator Docker Desktop z oficjalnej strony dockera.
  4. Przejdź przez instalator, pozostawiając domyślne opcje (WSL2 jako backend).

Po instalacji i restarcie systemu uruchom Docker Desktop. Jeśli wszystko się udało, w trayu pojawi się ikonka wieloryba, a w terminalu (PowerShell, cmd, terminal WSL) polecenie:

docker version

powinno zwrócić informacje o kliencie i serwerze Dockera.

Najczęstsze problemy na Windows:

Linux – klasyczna instalacja Docker Engine

Na Linuxie masz dwie drogi: oficjalne repozytoria Dockera albo paczki z repozytorium dystrybucji. Druga opcja bywa prostsza, ale często daje starszą wersję. Jeśli chcesz uczyć się na możliwie aktualnym Dockerze, lepiej użyć oficjalnego źródła.

Przykład: Ubuntu / Debian

Na Ubuntu/Debianie instalacja z oficjalnego repozytorium zwykle sprowadza się do kilku kroków:

  1. Usuń ewentualne stare paczki:
    sudo apt remove docker docker-engine docker.io containerd runc
  2. Zainstaluj wymagane narzędzia:
    sudo apt update
    sudo apt install ca-certificates curl gnupg lsb-release
  3. Dodaj klucz GPG i repozytorium Dockera (przykład dla Ubuntu):
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg
    echo 
      "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] 
      https://download.docker.com/linux/ubuntu 
      $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  4. Zainstaluj Dockera:
    sudo apt update
    sudo apt install docker-ce docker-ce-cli containerd.io

Na końcu sprawdź status usługi:

sudo systemctl status docker

Jeśli usługa jest aktywna, możesz przejść do testu:

sudo docker run hello-world

Jeżeli wszystko działa, terminal przywita Cię krótką wiadomością od Dockera.

Uruchamianie Dockera bez sudo

Domyślnie każda komenda wymaga sudo. W codziennej pracy jest to irytujące, więc zwykle dodaje się użytkownika do grupy docker:

sudo groupadd docker   # jeśli jeszcze nie istnieje
sudo usermod -aG docker $USER

Następnie wyloguj się i zaloguj ponownie (albo zrestartuj sesję). Po tym:

docker ps

powinno działać bez sudo. Na serwerach produkcyjnych można podchodzić do tego ostrożniej, ale na lokalnym środowisku developerskim jest to standard.

macOS – Docker Desktop i alternatywy

Na macOS najszybciej wystartujesz z Docker Desktop. Proces instalacji przypomina ten z Windows:

  1. Pobierz Docker Desktop dla macOS (Intel lub Apple Silicon) ze strony Dockera.
  2. Otwórz obraz .dmg i przeciągnij aplikację Docker do folderu Aplikacje.
  3. Uruchom Dockera, zaakceptuj uprawnienia (sieć, dostęp do plików itd.).

Po chwili w górnym pasku pojawi się wieloryb, a w terminalu:

docker version

powinno zwrócić klienta i serwer. Jeśli wolisz lżejsze rozwiązania, istnieją alternatywy typu Colima (Docker na bazie containerd i Lima), ale na start Docker Desktop jest najbardziej „bezproblemowy”.

Typowe problemy przy starcie Dockera

Przy pierwszej instalacji często pojawiają się powtarzalne kłopoty. W skrócie:

  • Demon Dockera nie działa – na Linuxie komunikat „Cannot connect to the Docker daemon” zwykle oznacza, że usługa nie jest uruchomiona:
    sudo systemctl start docker
    sudo systemctl enable docker
  • Brak uprawnień – błąd „permission denied” przy /var/run/docker.sock to najczęściej brak użytkownika w grupie docker (omówione wyżej).
  • Windows: konflikt z Hyper-V / VirtualBox – czasem inne narzędzia do wirtualizacji blokują backend Dockera. Pomaga wyłączenie jednego z nich lub przełączenie Dockera na WSL2.

Jeśli masz wątpliwości, zawsze zacznij od prostego:

docker info

Ten raport często jasno pokazuje, czy demon jest osiągalny, jaki backend jest używany i ile masz zasobów.

Kluczowe pojęcia Dockera wyjaśnione „po ludzku”

Obraz – wzorzec kontenera

Obraz (image) to gotowy „przepis” na kontener. Zawiera system plików (biblioteki, narzędzia, Twoją aplikację) oraz metadane: jaki proces uruchomić, jakie porty wystawić, jaką zmienną środowiskową ustawić.

Można o nim myśleć jak o zamrożonym snapshotcie. Na jego bazie tworzone są kontenery, ale sam obraz jest niezmienny – raz zbudowany nie będzie „doklejał” plików w trakcie działania.

Przydatne cechy obrazów:

  • warstwowe – kolejne zmiany budują się na poprzednich warstwach,
  • można je wersjonować za pomocą tagów (np. node:20-alpine),
  • da się je łatwo przesyłać pomiędzy maszynami (rejestry, pliki .tar).

Kontener – żywa instancja obrazu

Kontener to uruchomiona instancja obrazu. Startujesz nowy kontener – powstaje proces, który działa w odizolowanym środowisku. Zatrzymujesz kontener – proces znika, a kontener przechodzi w stan „stopped” (dopóki go nie usuniesz).

Najważniejsze właściwości kontenera:

  • ma własny system plików (pochodzący z obrazu),
  • ma swoją konfigurację sieciową (adres IP, porty),
  • działa jako konkretny proces (lub grupa procesów) w systemie hosta.

Dobry skrót myślowy: obraz to klasa, kontener to obiekt. Tego porównania używają nawet osoby, które już dawno przestały pisać w Javie.

Registry – „GitHub dla obrazów”

Obrazy trzeba gdzieś przechowywać i skądś pobierać. Do tego służą rejestry Dockera (Docker registries). Najpopularniejszy to publiczny Docker Hub pod adresem hub.docker.com.

Z perspektywy użytkownika wygląda to tak:

  • komenda docker pull nginx pobiera obraz nginx z domyślnego rejestru (Docker Hub),
  • możesz mieć też własne, prywatne registry, np. registry.gitlab.com/twoj-projekt/app.

Większe firmy trzymają swoje obrazy w prywatnych rejestrach (często na własnej infrastrukturze lub w chmurze), a Docker Hub służy głównie do obrazów bazowych i open source.

Dockerfile – przepis na budowę obrazu

Dockerfile to zwykły plik tekstowy, który opisuje, jak zbudować obraz. Zawiera instrukcje typu:

  • FROM – wybór obrazu bazowego,
  • COPY / ADD – kopiowanie plików do obrazu,
  • RUN – komendy wykonywane w trakcie budowania (np. apt install),
  • CMD / ENTRYPOINT – co ma się uruchomić po starcie kontenera.

Można to traktować jak powtarzalny skrypt: niezależnie od tego, kto go uruchomi, zbuduje ten sam obraz. Bez „a u mnie działa, bo ja wczoraj ręcznie dograłem jeszcze trzy pakiety”.

Warstwy i cache – dlaczego kolejność ma znaczenie

Docker buduje obraz krok po kroku, tworząc warstwy. Każda instrukcja w Dockerfile to potencjalnie nowa warstwa. Jeżeli podczas ponownej budowy nic się nie zmieniło w danym kroku, Docker użyje cache i pominie jego wykonanie.

Przykład:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]

Jeśli zmienisz tylko kod aplikacji, ale nie package.json, Docker wykorzysta cache dla warstwy z npm install. Dzięki temu przebudowa trwa kilka sekund zamiast kilku minut.

Dlatego kolejność instrukcji ma znaczenie – warto (ups, jednak padło) tak układać Dockerfile, żeby rzadko zmieniające się kroki były jak najwyżej.

Volume – trwałe dane poza kontenerem

Sam kontener jest z natury ulotny. Skasujesz go i wszystkie dane zapisane w jego systemie plików znikają. Do przechowywania danych służą wolumeny (volumes):

  • są tworzone i zarządzane przez Dockera,
  • mogą być podłączane do wielu kontenerów,
  • przeżyją usunięcie kontenera.

Przykład: uruchamiasz bazę PostgreSQL w Dockerze i chcesz, żeby dane nie znikały przy każdym docker rm. Tworzysz wolumen:

docker volume create pgdata

a potem:

docker run -d 
  --name pg 
  -e POSTGRES_PASSWORD=haslo 
  -v pgdata:/var/lib/postgresql/data 
  postgres:16

Baza zapisuje dane w pgdata, niezależnie od tego, ile razy zamienisz kontener na nowszy obraz.

Bind mount – mapowanie katalogu z hosta

W środowisku developerskim często wygodniejsze są bind mounty. Pozwalają zamapować katalog z systemu hosta do kontenera:

docker run --rm -it 
  -v $(pwd):/app 
  node:20-alpine 
  sh

W tym przykładzie:

  • bieżący katalog ($(pwd)) jest montowany jako /app w kontenerze,
  • wszelkie zmiany w kodzie widzisz od razu w kontenerze,
  • nie trzeba za każdym razem przebudowywać obrazu po drobnej zmianie pliku.

To typowy sposób pracy przy aplikacjach webowych – edytujesz kod na hoście, a kontener go uruchamia.

Porty i sieć – jak dostać się do aplikacji w kontenerze

Kontener działa w swojej, odizolowanej sieci. Żeby dostać się do aplikacji z przeglądarki lub innego klienta, trzeba zmapować port kontenera na port hosta.

Komenda:

docker run -d -p 8080:80 nginx

oznacza:

  • kontener słucha na porcie 80 wewnątrz,
  • Docker mapuje ten port na 8080 na hoście,
  • wchodzisz w przeglądarce na http://localhost:8080 i widzisz Nginxa.

Ogólna składnia to -p <port_hosta>:<port_kontenera>. Dzięki temu kilka kontenerów może używać tego samego portu wewnątrz (np. 80), a na hoście każdy dostaje inny port (8080, 8081, 8082…).

Kobieta programistka przy kilku monitorach w przyciemnionym pomieszczeniu
Źródło: Pexels | Autor: cottonbro studio

Pierwsze kroki w terminalu – podstawowe komendy Docker

Sprawdzenie wersji i stanu Dockera

Na rozgrzewkę dwa polecenia kontrolne:

docker version
docker info

Pierwsze pokazuje wersję klienta i serwera (dockerd). Drugie daje bardziej szczegółowy raport: liczbę obrazów, kontenerów, konfigurację storage drivera, limity pamięci itd. Jeśli docker info nie działa, reszta też nie ruszy.

Pobieranie obrazu – docker pull

Zanim uruchomisz kontener, Docker musi mieć obraz lokalnie. Możesz poczekać, aż sam go pobierze przy docker run, albo zrobić to świadomie:

docker pull nginx
docker pull nginx:1.25
docker pull node:20-alpine

Brak tagu oznacza :latest. Rzadko jest to faktycznie „najnowsza” wersja w sensie daty – to po prostu domyślny tag ustalony przez maintainerów obrazu.

Uruchomienie pierwszego kontenera – docker run

Sztandarowy przykład:

docker run hello-world

Ten obraz wypisze prostą informację i zakończy działanie. Docker utworzy kontener, uruchomi go, a po zakończeniu proces przejdzie do historii (kontener pozostanie w stanie „exited”).

Bardziej praktyczny przykład – Nginx:

docker run -d -p 8080:80 --name moj-nginx nginx

Tutaj:

  • -d – uruchom w tle (detached),
  • -p 8080:80 – mapowanie portu 80 na hostowy 8080,
  • --name moj-nginx – nadaj kontenerowi czytelną nazwę.

Po chwili możesz wejść na http://localhost:8080 i zobaczyć domyślną stronę Nginxa.

Lista kontenerów – docker ps

By zobaczyć, co aktualnie działa:

docker ps

Dodać kontenery zatrzymane:

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

Rozszerzona wersja listy kontenerów to:

docker ps -a

Pokazuje także kontenery zakończone (status Exited). Często po kilku dniach nauki lista wygląda jak małe muzeum eksperymentów – wtedy dobrze jest posprzątać:

docker rm <ID_lub_nazwa>

Aby usunąć kilka na raz:

docker rm kontener1 kontener2 kontener3

lub nawet hurtowo:

docker rm $(docker ps -aq)

Ta ostatnia komenda usunie wszystkie zatrzymane kontenery, więc lepiej uruchamiać ją świadomie, a nie z przyzwyczajenia.

Zatrzymywanie i wznawianie – docker stop, start, restart

Kontener można „uśpić” bez jego usuwania:

docker stop moj-nginx

Kontener przechodzi w stan Exited, ale dalej wisi w systemie. Wznowienie:

docker start moj-nginx

Czasem szybciej zrestartować:

docker restart moj-nginx

Pod spodem to po prostu stop + start, ale w jednej komendzie.

Podgląd logów kontenera – docker logs

Gdy coś nie działa, pierwszym odruchem powinny być logi:

docker logs moj-nginx

Aby śledzić logi „na żywo” (jak tail -f):

docker logs -f moj-nginx

Przy aplikacjach webowych to podstawowy sposób na sprawdzenie, czy serwer w ogóle się podniósł, czy może wywalił się przy starcie.

Wejście do środka działającego kontenera – docker exec

Czasem trzeba zajrzeć do wnętrza kontenera: podejrzeć pliki, logi aplikacji albo odpalić migawkowy skrypt. Służy do tego:

docker exec -it moj-nginx sh

lub, gdy wewnątrz jest bash:

docker exec -it moj-kontener bash

Przełącznik -it daje interaktywną powłokę (terminal). Po wyjściu z shella (exit) kontener nadal działa – zamykasz tylko swoją sesję.

Lista i usuwanie obrazów – docker images, docker rmi

Po kilku tygodniach testów katalog Dockera zwykle przybiera rozmiary, które dyskom się nie podobają. Podstawowe komendy do pracy z obrazami:

docker images

Dla usunięcia nieużywanego obrazu:

docker rmi nginx:1.25

Jeżeli obraz jest używany przez jakiś kontener (choćby zatrzymany), usunięcie się nie uda – najpierw trzeba skasować kontener.

Bardziej „odważne” sprzątanie:

docker image prune

Czyści wiszące, nieużywane obrazy (dangling images). Można dodać -a, aby usunąć wszystkie nieużywane obrazy:

docker image prune -a

Tu już przydaje się odrobina rozwagi – po takim czyszczeniu kolejne docker run będą znów pobierać obrazy z sieci.

Przygotowanie pierwszej aplikacji do konteneryzacji

Minimalna aplikacja – prosty serwer HTTP

Na starcie dobrze mieć przykład, który da się uruchomić w kilka minut. Załóżmy prosty serwer HTTP w Node.js:

mkdir moja-pierwsza-apka
cd moja-pierwsza-apka
npm init -y
npm install express

Tworzymy plik index.js:

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send('Hello z Dockera!');
});

app.listen(port, () => {
  console.log(`Serwer działa na porcie ${port}`);
});

W package.json dodaj prosty skrypt startowy:

"scripts": {
  "start": "node index.js"
}

Budowa Dockerfile dla aplikacji Node.js

W katalogu projektu tworzysz plik Dockerfile:

FROM node:20-alpine

# Ustawiamy katalog roboczy
WORKDIR /app

# Najpierw tylko pliki z zależnościami
COPY package*.json ./

# Instalacja zależności
RUN npm install --omit=dev

# Skopiowanie reszty kodu
COPY . .

# Aplikacja użyje zmiennej PORT; otwieramy 3000
EXPOSE 3000

# Domyślna komenda startowa
CMD ["npm", "start"]

Kilka istotnych detali:

  • najpierw kopiowane są package*.json i wykonywane npm install – dzięki temu cache zadziała przy zmianach w kodzie,
  • --omit=dev usuwa zależności developerskie, zostawiając tylko runtime,
  • EXPOSE dokumentuje port używany przez aplikację (to informacja dla ludzi i narzędzi, nie mapowanie na hosta).

Budowanie obrazu aplikacji – docker build

Mając Dockerfile, można złożyć obraz:

docker build -t moja-apka:1.0 .

Składnia:

  • -t moja-apka:1.0 – nadaje nazwę i tag obrazowi,
  • kropka na końcu oznacza bieżący katalog jako kontekst budowania (wszystko, co Docker może skopiować do obrazu).

Po zakończeniu:

docker images | grep moja-apka

powinno pokazać świeżo zbudowany obraz.

Uruchomienie aplikacji w kontenerze

Czas na właściwy test:

docker run -d 
  -p 3000:3000 
  --name moja-apka-kontener 
  moja-apka:1.0

Teraz można wejść w przeglądarce na http://localhost:3000 i zobaczyć komunikat z aplikacji. Dla kontroli:

docker logs -f moja-apka-kontener

powinno pokazać logi startu serwera.

Tryb developerski z bind mountem

Przy codziennej pracy przebudowywanie obrazu przy każdej zmianie pliku szybko nudzi. Wtedy wchodzi bind mount, cały na biało:

docker run --rm -it 
  -p 3000:3000 
  -v $(pwd):/app 
  -w /app 
  node:20-alpine 
  sh

Co tu się dzieje:

  • -v $(pwd):/app – kod z hosta jest widoczny w kontenerze pod /app,
  • -w /app – ustawienie katalogu roboczego (jak cd),
  • wewnątrz kontenera możesz uruchomić npm install i npm start.

Zmiany w plikach na hoście są natychmiast widoczne w kontenerze. Przy prostym serwerze Node wystarczy np. użyć nodemon wewnątrz i masz przyjemny, automatyczny reload.

Dodanie pliku .dockerignore

Tak jak w Gitcie nie chcesz commitować wszystkiego, tak Docker nie musi widzieć całego katalogu. Warto dodać plik .dockerignore:

node_modules
npm-debug.log
Dockerfile
docker-compose.yml
.git
.gitignore

Dzięki temu:

  • czas budowania skraca się, bo Docker nie przerzuca zbędnych plików,
  • obraz jest mniejszy,
  • nie przenosisz lokalnych śmieci (np. logów) do kontenera.

Prosta aplikacja w Pythonie jako alternatywny przykład

Jeśli bliżej Ci do Pythona, analogiczny przykład wygląda podobnie. Załóżmy plik app.py:

from flask import Flask
import os

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello z Dockera (Python)!"

if __name__ == "__main__":
    port = int(os.environ.get("PORT", 5000))
    app.run(host="0.0.0.0", port=port)

I plik requirements.txt:

flask==3.0.0

Dockerfile może wyglądać tak:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

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

Budowanie i uruchamianie:

docker build -t moja-flask-apka:1.0 .
docker run -d -p 5000:5000 --name flask-kontener moja-flask-apka:1.0

Po wejściu na http://localhost:5000 powinien przywitać Cię komunikat z aplikacji.

Konfiguracja przez zmienne środowiskowe

Aplikacja w kontenerze nie powinna mieć na sztywno wpisanych haseł, adresów baz czy trybu debug. Do tego służą zmienne środowiskowe. Uruchamiając kontener:

docker run -d 
  -p 3000:3000 
  -e NODE_ENV=production 
  -e API_URL=https://api.example.com 
  --name moja-apka-kontener 
  moja-apka:1.0

W Node.js odczytasz je przez process.env.NODE_ENV, w Pythonie przez os.environ["API_URL"]. Dzięki temu ten sam obraz może działać i na lokalnej maszynie, i w środowisku produkcyjnym – zmienia się tylko konfiguracja przy starcie.

Łączenie aplikacji z bazą danych w innych kontenerach

Typowy scenariusz: aplikacja + baza danych, oba w osobnych kontenerach. Na prostym poziomie można użyć wbudowanej sieci Dockera:

docker network create moja-siec

Następnie baza:

docker run -d 
  --name pg 
  --network moja-siec 
  -e POSTGRES_PASSWORD=haslo 
  -e POSTGRES_USER=app 
  -e POSTGRES_DB=appdb 
  -v pgdata:/var/lib/postgresql/data 
  postgres:16

i aplikacja:

docker run -d 
  --name moja-apka 
  --network moja-siec 
  -e DB_HOST=pg 
  -e DB_USER=app 
  -e DB_PASSWORD=haslo 
  -e DB_NAME=appdb 
  -p 3000:3000 
  moja-apka:1.0

Kontenery w tej samej sieci mogą się widzieć po nazwach (tu: pg jako host bazy). Nie trzeba wymyślać lokalnych adresów IP – Docker robi DNS za Ciebie.

Iterowanie zmian – cykl: popraw kod → zbuduj → uruchom

Typowy rytm pracy przy prostej aplikacji dockeryzowanej wygląda mniej więcej tak:

  1. Edytujesz kod lokalnie.
  2. Budujesz nową wersję obrazu, np. z tagiem 1.1:
    docker build -t moja-apka:1.1 .
  3. Zatrzymujesz stary kontener:
    docker stop moja-apka-kontener
  4. Uruchamiasz nowy kontener z nowszym obrazem:
    docker run -d 
      -p 3000:3000 
      --name moja-apka-kontener 
      moja-apka:1.1

Poprzedni obraz (1.0) nadal jest w systemie i można do niego wrócić, jeśli coś poszło nie tak. W praktyce łatwo w ten sposób zasymulować prosty „rollback”.

Najczęściej zadawane pytania (FAQ)

Co to jest Docker i po co w ogóle używać kontenerów?

Docker to narzędzie, które pakuje aplikację razem z całym potrzebnym „dobrodziejstwem” (runtime, biblioteki, konfiguracja) w kontener. Dzięki temu to samo pudełko z aplikacją możesz uruchomić na różnych maszynach i środowiskach z dużą szansą, że zachowa się identycznie.

Kontenery rozwiązują klasyczny problem „u mnie działa”. Zamiast ręcznie instalować zależności na każdym serwerze, przygotowujesz jeden obraz kontenera, a potem tylko go uruchamiasz tam, gdzie jest Docker. Mniej niespodzianek, mniej konfiguracji, więcej powtarzalności.

Jaka jest różnica między tradycyjną instalacją aplikacji a kontenerem Dockera?

Przy tradycyjnej instalacji wgrywasz biblioteki i pakiety bezpośrednio do systemu. Wszystko ląduje w jednym „wspólnym kotle”: te same biblioteki współdzielą różne aplikacje, wersje potrafią się gryźć, a aktualizacja systemu potrafi wywrócić pół środowiska.

W przypadku Dockera aplikacja żyje w swoim odizolowanym systemie plików. Konkretne wersje bibliotek, struktura katalogów i sposób uruchamiania są zapisane w obrazie. System hosta dostarcza tylko jądro i zasoby (CPU, RAM, dysk), reszta dzieje się w kontenerze. Dzięki temu kilka aplikacji może korzystać z różnych wersji tego samego runtime’u bez wchodzenia sobie w drogę.

Czym różni się kontener Docker od maszyny wirtualnej (VM)?

Maszyna wirtualna uruchamia pełny system operacyjny gościa: ma własne jądro, sterowniki, usługi. Kontener Dockera współdzieli jądro systemu gospodarza i jest w praktyce odizolowanym procesem z własnym systemem plików, siecią i przestrzenią procesów.

Efekt jest taki, że kontener startuje w ułamku sekundy i zużywa znacznie mniej zasobów niż VM. Maszyna wirtualna daje silniejszą izolację, ale płacisz za to większym narzutem na RAM i CPU oraz dłuższym czasem startu. Na potrzeby typowego developmentu i mikroserwisów kontenery są zwykle dużo wygodniejsze.

Kiedy lepiej użyć kontenera, a kiedy maszyny wirtualnej?

Kontenery sprawdzają się świetnie przy aplikacjach webowych, API, mikroserwisach, workerach, środowiskach developerskich i testowych. Gdy chcesz szybko podnieść kilka usług, postawić lokalnie cały stack czy odpalić wersję testową aplikacji bez „brudzenia” systemu, Docker robi robotę.

Maszyny wirtualne mają więcej sensu, gdy potrzebujesz pełnego, silnie odseparowanego systemu (np. innego systemu operacyjnego, własnych sterowników, bardziej rygorystycznej izolacji bezpieczeństwa). Jeśli Twoim celem jest lekkie, powtarzalne środowisko aplikacji – w większości przypadków wygra kontener.

Jak Docker rozwiązuje problem różnych wersji zależności i runtime’u?

Docker opiera się na obrazach tworzonych według instrukcji z pliku Dockerfile. Określasz tam m.in. obraz bazowy (np. node:18-alpine), pakiety do zainstalowania, sposób kopiowania kodu i konfiguracji oraz komendę startową. Każdy krok buduje warstwę obrazu, którą Docker może potem cache’ować.

Dzięki temu możesz precyzyjnie przypiąć wersje: obrazu bazowego, bibliotek i samej aplikacji. Gdy dany obraz działa dobrze, tagujesz go konkretną wersją i możesz do niej wrócić, jeśli kolejna iteracja okaże się „średnio udanym eksperymentem”.

Czy muszę znać wszystkie szczegóły Dockera, żeby uruchomić aplikację w kontenerze?

Nie. Żeby po prostu odpalić gotową aplikację, wystarczy mieć zainstalowanego Dockera i znać podstawowe polecenia, głównie docker run (plus ewentualnie docker pull). Gotowy obraz traktujesz jak czarne pudełko: uruchamiasz je z odpowiednimi parametrami i aplikacja działa.

Głębsza wiedza przydaje się, gdy zaczynasz budować własne obrazy, optymalizować Dockerfile, łączyć kilka usług w całość (np. przez Docker Compose) czy integrować Dockera z CI/CD. Na start wystarczy jednak prosty zestaw komend i zrozumienie, że „obraz to szablon, a kontener to jego działająca instancja”.

Gdzie w praktyce najczęściej spotyka się Dockera?

Docker jest dziś standardem w wielu miejscach: od projektów open source (gotowe obrazy na Docker Hub), przez środowiska developerskie, po pipeline’y CI/CD. Nowy programista w zespole często zamiast czytać długą instrukcję instalacji wywołuje po prostu docker compose up i ma lokalnie cały system: backend, frontend, bazę danych, kolejkę itd.

Kontenery są też powszechnie używane w środowiskach testowych i do eksperymentów. Możesz w kilka minut podnieść osobny stack, sprawdzić pomysł, a potem jednym poleceniem wszystko usunąć i zacząć od zera – bez formatowania systemu co trzy sprinty.

Najważniejsze punkty

  • Docker rozwiązuje klasyczny problem „u mnie działa”, dostarczając aplikację razem z całym środowiskiem (runtime, biblioteki, konfiguracja) w jednym, przewidywalnym pakiecie.
  • Kontener można traktować jak gotowe pudełko z aplikacją – użytkownik nie musi znać jego wnętrza, wystarczy, że ma Dockera i potrafi uruchomić polecenie docker run.
  • W tradycyjnej instalacji wszystkie pakiety lądują w jednym systemie i potrafią sobie nawzajem szkodzić; w Dockerze każda aplikacja ma własną, odizolowaną przestrzeń z konkretnymi wersjami bibliotek.
  • Dockerfile pozwala krok po kroku zdefiniować, na jakim obrazie bazować, jakie pakiety doinstalować i jak zbudować środowisko, dzięki czemu da się dokładnie kontrolować wersje i łatwo „zamrażać” działające konfiguracje.
  • Mechanizm warstw i cache sprawia, że kolejne budowy obrazu są szybsze i mniej zasobożerne – Docker nie instaluje w kółko tego samego, jeśli nic się nie zmieniło.
  • Kontenery Dockera współdzielą jądro systemu hosta, w przeciwieństwie do maszyn wirtualnych z pełnym systemem gościa; dzięki temu startują szybciej i zużywają mniej zasobów, więc można ich uruchomić naprawdę sporo.
  • Docker stał się standardem w projektach open source, pracy zespołowej, CI/CD i środowiskach testowych – zamiast kilkunastu kroków konfiguracji wystarczy często jedno docker compose up i można pracować, zamiast walczyć z instalatorem.