Docker od zera: uruchom pierwszą aplikację w kontenerze w 15 minut

0
125
2.8/5 - (6 votes)

Nawigacja:

O co chodzi z Dockerem i kontenerami – prosty obrazek bez marketingu

Kontener w praktyce: zwykły proces, a nie mini-maszyna

Kontener Dockera to po prostu proces uruchomiony na twoim systemie, który jest sprytnie odizolowany: ma własny system plików, własne zmienne środowiskowe, widzi tylko to, na co mu pozwolisz (np. porty, katalogi). Nie jest to osobny system operacyjny, nie ma własnego kernela, nie instaluje się na nim „Windowsa w środku Windowsa”.

Najbliższe praktyczne skojarzenie: wirtualne środowisko w Pythonie albo osobny katalog node_modules w Node.js, tylko dużo mocniej odizolowane i lepiej powtarzalne. Kontener bierze obraz (template z systemem plików), uruchamia w nim proces (np. serwer HTTP), a reszta to kwestia konfiguracji.

Dla ciebie jako początkującego najważniejsze jest to, że kontener:

  • działa tak samo na twoim laptopie, na komputerze kolegi i na serwerze w chmurze,
  • nie miesza zależności – różne projekty mogą mieć różne wersje Pythona, Node.js itd.,
  • da się łatwo wyrzucić i odtworzyć – jak coś popsujesz, usuwasz kontener i uruchamiasz nowy.

Mit „Docker to mała wirtualka” kontra rzeczywistość

Popularny mit brzmi: „Kontener to taka mała wirtualna maszyna”. Brzmi sensownie, ale technicznie jest to fałsz i rodzi złe nawyki. Rzeczywistość: kontenery używają wspólnego jądra systemu operacyjnego hosta. To dlatego są lekkie i uruchamiają się w ułamku sekundy.

Maszyna wirtualna emuluje całe środowisko: BIOS, sprzęt, kernel, system operacyjny. Kontener jedynie nakłada „ramki” na proces. Stąd:

  • VM zużywa więcej RAM i CPU, bo ładuje cały system operacyjny gościa,
  • kontener startuje bardzo szybko, bo jest tylko procesem w istniejącym systemie,
  • w Dockerze nie instalujesz „Dockera w Dockerze” tak jak systemu w systemie, tylko kolejne aplikacje.

Ten mit bywa groźny, gdy ktoś zaczyna „konfigurować” kontener jak zwykły serwer, ręcznie instalując pakiety i grzebiąc w środku, zamiast zbudować właściwy obraz. Efekt: niepowtarzalne środowiska, których potem nie da się odtworzyć.

Po co początkującemu Docker: realne korzyści

Najbardziej namacalne korzyści dla początkującej osoby:

  • Przewidywalne środowisko – obraz raz zbudowany działa tak samo wszędzie. Koniec z „u mnie działa, u ciebie nie”.
  • Łatwe uruchamianie cudzych aplikacji – ktoś podaje ci polecenie docker run ..., wpisujesz, działa, bez ręcznej instalacji całego stosu technologicznego.
  • Szybkie testy – możesz bez bólu sprawdzić inną wersję bazy, inną wersję Node.js czy Pythona, bez niszczenia głównego systemu.

Na początku najwięcej daje po prostu umiejętność odpalenia gotowej aplikacji: bazy danych, serwera HTTP, narzędzia CLI. Z czasem zaczniesz pakować w kontenery swoje własne projekty.

Gdzie Docker wchodzi w codzienną pracę

Przykładowe, bardzo typowe scenariusze:

  • Prosty backend – mikroserwis w Node.js, Pythonie czy Go z API HTTP. Zamiast instalować na serwerze Node.js, NPM, itd., po prostu wrzucasz obraz z gotowym serwerem.
  • Baza danych do developmentu – PostgreSQL, MySQL czy Redis uruchamiasz jednym poleceniem. Nie zaśmiecasz systemu globalną instalacją, możesz z łatwością mieć kilka wersji równolegle.
  • Narzędzia CLI – np. klienty do różnych chmur, narzędzia do migracji bazy albo testowania wydajności. Często są dostarczane jako gotowe obrazy Dockera, bez potrzeby klasycznej instalacji.

Dla osoby na poziomie juniora lub początkującego DevOpsa „umiesz uruchomić aplikację w kontenerze i zdiagnozować problemy” to jedna z najbardziej praktycznych umiejętności, jaką możesz pokazać.

Programista skupiony przy komputerze w nowoczesnym biurze
Źródło: Pexels | Autor: cottonbro studio

Szybki słownik Dockera: obrazy, kontenery, rejestry i warstwy

Obraz i kontener: szablon kontra uruchomiony egzemplarz

Podstawowe pojęcia, które trzeba mieć w małym palcu:

  • Obraz (image) – niezmienny szablon. Zawiera system plików (binarki, biblioteki, twój kod) i instrukcję, co uruchomić (CMD/ENTRYPOINT). Nic się w nim nie „dopisywuje” w czasie uruchomienia.
  • Konteneruruchomiony obraz, żywy proces. Możesz mieć dziesięć kontenerów z tego samego obrazu, każdy z osobnymi danymi i konfiguracją.

Dobrze działa analogia do programowania obiektowego: obraz = klasa, kontener = obiekt. Obraz definiuje, jak wygląda środowisko i co potrafi, a kontener to konkretne uruchomienie, które zapisuje swoje dane, logi i stan.

Rejestr i Docker Hub: skąd biorą się obrazy

Obrazy przechowuje się w rejestrach (registry). Najpopularniejszy publiczny rejestr to Docker Hub, dostępny pod adresem hub.docker.com. Gdy wykonujesz:

docker run nginx:alpine

a lokalnie nie masz obrazu nginx:alpine, Docker domyślnie pobierze go właśnie z Docker Hub. Nazwa obrazu często ma postać:

  • nazwa – np. nginx,
  • nazwa:tag – np. nginx:alpine, node:18,
  • uzytkownik/obraz:tag – np. moj-login/hello-api:1.0.

Tag w praktyce oznacza wersję lub wariant. Możesz mieć python:3.11 i python:3.9, często także specjalne odmiany typu -alpine (odchudzone obrazy bazujące na Alpine Linux).

Warstwy obrazu: dlaczego kolejne buildy są szybsze

Obraz Dockera składa się z warstw (layers). Każda instrukcja w Dockerfile (np. RUN, COPY) tworzy nową warstwę. Warstwy są:

  • niezmienne – raz zbudowana warstwa nie jest modyfikowana,
  • współdzielone – różne obrazy mogą używać tych samych warstw, co oszczędza miejsce,
  • cache’owane – jeśli zawartość się nie zmienia, Docker nie buduje ich ponownie.

Praktyczny efekt: jeśli w Dockerfile najpierw instalujesz zależności (np. z package.json), a dopiero potem kopiujesz kod aplikacji, to przy małych zmianach w kodzie Docker wykorzysta cache dla warstw z zależnościami, a przebuduje tylko końcówkę obrazu. Dzięki temu kolejne buildy są dużo szybsze.

Tag latest kontra konkretne wersje

Tag latest to uprzejma sugestia, a nie żadna magiczna „najbezpieczniejsza” wersja. Najczęściej oznacza „domyślny wariant z punktu widzenia autora obrazu”, który po jakimś czasie może zostać podmieniony na inną główną wersję.

Mit brzmi: „:latest to zawsze aktualna i najlepsza wersja”. W praktyce:

  • dzisiaj node:latest może oznaczać Node 20, za pół roku Node 22,
  • aplikacja zbudowana na „latest” może któregoś dnia przestać działać po ponownym zbudowaniu obrazu,
  • trudniej odtworzyć błąd „sprzed kilku miesięcy”, bo nie wiadomo, jaka wersja obrazu wtedy była użyta.

Dlatego do poważniejszej pracy używa się konkretnych tagów, np. node:18.20, a :latest zostawia się co najwyżej do krótkich testów lokalnych albo szybkich eksperymentów.

Przygotowanie środowiska: instalacja Dockera bez bólu

Docker Desktop kontra Docker Engine

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

  • Docker Engine – właściwy silnik kontenerów. To demon (proces w tle) i narzędzie docker w terminalu. Na Linuksie instaluje się głównie sam Engine.
  • Docker Desktop – aplikacja dla Windows i macOS, która w pakiecie zawiera Engine, prosty GUI, integrację z systemem i (na Windows) WSL2 lub lekką maszynę wirtualną.

Na Windows i macOS najprostsza droga to Docker Desktop. Na Linuksie używa się systemowych repozytoriów lub oficjalnych pakietów Dockera dla wybranej dystrybucji.

Instalacja Dockera na Windows (WSL2, Docker Desktop, typowe problemy)

Podstawowe kroki na Windows 10/11:

  1. Upewnij się, że system ma włączoną w BIOS/UEFI wirtualizację (Intel VT-x/AMD-V).
  2. Włącz funkcję WSL2 (Windows Subsystem for Linux) oraz „wirtualizację”/„Hyper-V” jeśli system tego wymaga.
  3. Pobierz instalator Docker Desktop z oficjalnej strony Dockera i zainstaluj jak zwykły program.
  4. Podczas instalacji wybierz tryb WSL2, jeśli jest sugerowany – jest lżejszy i wygodniejszy niż klasyczny Hyper-V.

Najczęstsze problemy:

  • Komunikat o braku włączonej wirtualizacji – trzeba wejść do BIOS/UEFI i ją aktywować.
  • Docker Desktop „zjada” sporo RAM/CPU – zwykle to efekt zbyt dużych limitów w ustawieniach. Warto ograniczyć liczbę rdzeni i przydzielony RAM, jeśli komputer jest słabszy.
  • Konflikt z innymi narzędziami używającymi Hyper-V/VirtualBox – czasem trzeba wybrać jedno narzędzie lub tryb WSL2.

Instalacja Dockera na macOS

Na macOS sprawa jest nieco prostsza:

  • Pobierz Docker Desktop for Mac z oficjalnej strony.
  • Przeciągnij aplikację do katalogu Applications jak klasyczną aplikację.
  • Uruchom Docker Desktop i przyznaj wymagane uprawnienia (sieć, system plików).

Docker Desktop na macOS również działa w oparciu o lekką maszynę wirtualną, ale większość tego jest ukryta – używasz standardowych komend docker w terminalu, a narzędzie samo zarządza środowiskiem.

Instalacja Dockera na Linux (Ubuntu, Debian, Fedora – przykłady)

Na Linuksie typowa ścieżka to instalacja z oficjalnych repozytoriów Dockera (często są nowsze niż systemowe). Przykładowo, skrótowo dla Ubuntu/Debiana (polecenia uruchamiane jako użytkownik z uprawnieniami sudo):

sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | 
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo 
  "deb [arch=$(dpkg --print-architecture) 
  signed-by=/etc/apt/keyrings/docker.gpg] 
  https://download.docker.com/linux/ubuntu 
  $(lsb_release -cs) stable" | 
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

Na Fedorze i pochodnych komendy będą inne, zwykle w stylu:

sudo dnf install docker-ce docker-ce-cli containerd.io
sudo systemctl enable docker
sudo systemctl start docker

Po instalacji zwykle warto dodać swojego użytkownika do grupy docker, aby nie używać za każdym razem sudo:

sudo usermod -aG docker $USER

Sprawdzenie działania Dockera: podstawowe komendy kontrolne

Po zainstalowaniu lubi przydać się krótka checklista:

  • docker version – pokazuje wersję klienta i serwera (Engine). Jeśli widzisz oba, to dobra oznaka.
  • docker info – mnóstwo szczegółów o konfiguracji, sterowniku storage, sieci, liczbie obrazów i kontenerów.
  • docker run hello-world – klasyczny test, który pobiera mały obraz, uruchamia kontener i wyświetla komunikat powitalny.

Jeśli docker run hello-world działa, to masz działający silnik kontenerów, komunikację z rejestrem (Docker Hub) i działające sieci w Dockerze na podstawowym poziomie.

Czy Docker spowalnia komputer?

Kolejny popularny mit: „Docker zawsze spowalnia komputer, więc nie używaj go na słabych maszynach”. Rzeczywistość jest bardziej zniuansowana:

  • na Linuksie Docker to po prostu dodatkowy demon i kilka procesów – przy rozsądnej liczbie kontenerów wpływ jest znikomy,
  • Docker na słabszym sprzęcie i laptopach

    Na laptopie z 8 GB RAMu i kilkoma kontenerami Docker zwykle jest mniej „ciężki” niż kilka równolegle odpalonych środowisk typu Node + baza + Redis instalowanych lokalnie. Kontenery „śpią”, gdy nic w nich się nie dzieje, a zużycie CPU spada. Problem zaczyna się dopiero, gdy:

  • utrzymujesz kilkanaście–kilkadziesiąt działających kontenerów naraz,
  • masz bardzo mocno obciążające usługi (np. kilka baz danych, ElasticSearch, ciężkie buildy) w Dockerze,
  • na Windows/macOS masz źle ustawione limity RAM/CPU w Docker Desktop.

Prosty trik: jeśli Docker Desktop zaczyna „mielić” wentylatorami, ogranicz w ustawieniach liczbę rdzeni CPU i RAM (np. 2 rdzenie, 4 GB RAM), a do częściej używanych kontenerów dołóż lekkie limity zasobów w docker run lub w kompozycji (np. docker-compose.yml). Często to wystarczy, żeby laptop odetchnął.

Mit bywa taki, że „na starym laptopie Docker jest nieużywalny”. W praktyce na kilkuletniej maszynie spokojnie da się odpalać pojedyncze usługi developerskie (np. PostgreSQL, Redis, prosty backend) i pracować komfortowo – pod warunkiem, że nie traktujesz Dockera jak poligonu do stawiania całej serwerowni na raz.

Programistka pracuje przy kilku monitorach w ciemnym pokoju
Źródło: Pexels | Autor: cottonbro studio

Pierwsze spotkanie z docker run: jedna komenda, dużo mocy

Najprostszy możliwy przykład

Pierwszy kontakt z Dockerem dobrze zacząć od komendy, którą da się „przeczytać” jak zdanie. Klasyczny przykład:

docker run nginx

Co tu się dzieje:

  • docker – klient Dockera, Twoje narzędzie w terminalu,
  • run – powiedz: „uruchom nowy kontener z podanego obrazu”,
  • nginx – nazwa obrazu (w tym wypadku serwer HTTP nginx z Docker Hub).

Po wpisaniu tego polecenia, jeśli obraz nginx nie jest pobrany, Docker ściągnie go z rejestru, utworzy kontener i spróbuje go uruchomić. Efekt bywa rozczarowujący: kontener niby działa, ale w przeglądarce nic nie widać. Powód jest prosty – nie powiedziałeś nic o przekierowaniu portów ani o trybie pracy.

Tryb interaktywny kontra „w tle”

Podstawowy rozjazd oczekiwań: czy kontener ma żyć w tle, czy chcesz „wejść do środka” i z nim pogadać. Służą do tego dwa często używane przełączniki:

  • -it – tryb interaktywny (ang. interactive + TTY). Używasz go, gdy chcesz mieć terminal wewnątrz kontenera.
  • -d – tryb „daemon”, czyli uruchom w tle (ang. detached).

Przykład interaktywny:

docker run -it alpine sh

Działa to jak mini-Linux w konsoli. Wchodzisz do środka obrazu alpine i uruchamiasz powłokę sh. Po wyjściu z powłoki (np. exit) kontener się kończy.

Przykład „w tle”:

docker run -d nginx

Nginx działa jako serwer HTTP w kontenerze, a Ty dostajesz tylko identyfikator kontenera. Możesz kontynuować pracę w terminalu. Jednak wciąż nie widać go z zewnątrz, bo nie ma mapowania portów.

Porty: jak zobaczyć kontener w przeglądarce

Kontener ma własną sieć i swoje porty. Host (Twój komputer) to inna maszyna. Żeby dostać się do usługi z kontenera, trzeba zmapować port kontenera na port hosta. Służy do tego opcja -p:

docker run -d -p 8080:80 nginx

Czytając z lewej na prawą:

  • -d – uruchom w tle,
  • -p 8080:80 – połącz port 8080 na hoście z portem 80 w kontenerze,
  • nginx – obraz serwera HTTP.

Po takim uruchomieniu wchodzisz w przeglądarce na http://localhost:8080 i widzisz domyślną stronę nginx. Jeśli zmienisz port po lewej stronie (np. -p 5000:80), adres w przeglądarce stanie się http://localhost:5000.

Częsty mit: „aplikacja w kontenerze nasłuchuje na porcie 80, więc wystarczy docker run obraz, żeby działało”. Rzeczywistość – bez mapowania portów ta usługa jest widoczna głównie z innych kontenerów, a nie z Twojego systemu hosta.

Mapowanie katalogów: praca z plikami z hosta

Do zabawy lokalnej przydaje się jeszcze jedno narzędzie: -v (volume). Dzięki niemu możesz „podmontować” katalog z Twojego komputera do kontenera:

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

Znaczenie:

  • -v $(pwd):/app – mapuj bieżący katalog (na hoście) do katalogu /app w kontenerze,
  • alpine sh – uruchom powłokę sh w lekkim obrazie Alpine.

Wewnątrz kontenera w katalogu /app zobaczysz dokładnie te same pliki, które masz w bieżącym katalogu na swojej maszynie. Edytujesz je normalnie w ulubionym edytorze, a kontener je odczytuje tak, jakby były jego własne.

Najważniejsze flagi docker run na start

Zamiast uczyć się całej dokumentacji, wystarczy na początek zrozumieć kilka flag i świadomie je łączyć:

  • -it – interaktywna sesja, przydatna do debugowania i eksploracji obrazu,
  • -d – daemon / uruchom w tle, do usług, które mają działać dłużej,
  • -p host:container – mapowanie portów, aby wystawić usługę na zewnątrz,
  • -v host:container – mapowanie katalogów lub wolumenów, np. na dane lub kod,
  • --name nazwa – nadaj własną, czytelną nazwę kontenerowi, np. --name moj-nginx.

Przykład, który łączy to w jedno:

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

Po kilku takich uruchomieniach komendy zaczynają się układać w głowie w logiczne fragmenty, zamiast w losową „linię z parametrami”.

Programista piszący kod na dwóch monitorach przy drewnianym biurku
Źródło: Pexels | Autor: Lisa from Pexels

Budujemy pierwszą aplikację w kontenerze: „Hello API”

Wybór technologii: Node.js jako wygodny przykład

Do prostego „Hello API” wygodny jest Node.js z biblioteką Express – mało plików, mało konfiguracji i działa na wszystkich platformach tak samo. Nie ma jednak przymusu – te same idee zadziałają z Pythonem (Flask / FastAPI), Go czy Javą.

Przykładowe założenie: mały serwer HTTP z jednym endpointem /, który zwraca JSON:

{
  "message": "Hello from Docker!"
}

Struktura projektu

W nowym, pustym katalogu utwórz podstawowe pliki:

  • package.json – definicja zależności aplikacji,
  • index.js – główny plik z kodem API,
  • Dockerfile – przepis na obraz Dockera (dojdziemy do niego za chwilę).

Minimalny package.json

Zawartość package.json może wyglądać tak:

{
  "name": "hello-api",
  "version": "1.0.0",
  "description": "Proste API w kontenerze Docker",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.19.0"
  }
}

Ten plik mówi Node’owi, że aplikacja startuje komendą npm start, która odpala node index.js, oraz że potrzebuje jednej zależności – Expressa.

Kod prostego serwera HTTP

Teraz index.js z definicją API:

const express = require('express');
const app = express();

// Port może być podany z zewnątrz (zmienna środowiskowa) lub domyślnie 3000
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({ message: 'Hello from Docker!' });
});

app.listen(PORT, () => {
  console.log(`Hello API listening on port ${PORT}`);
});

Aplikacja nasłuchuje na porcie 3000, chyba że przekażesz inny przez zmienną środowiskową PORT. To drobny szczegół, ale później w Dockerze ułatwi elastyczność.

Lokalne uruchomienie bez Dockera (kontrola sanity)

Zanim dodasz warstwę Dockera, warto upewnić się, że aplikacja w ogóle działa lokalnie:

npm install
npm start

Po uruchomieniu otwórz w przeglądarce adres http://localhost:3000. Powinieneś zobaczyć JSON z komunikatem. Jeśli coś tu nie gra, nie ma sensu szukać błędów w Dockerze – najpierw popraw aplikację.

Dockerfile bez magii: jak zapakować „Hello API”

Wybór obrazu bazowego

Dockerfile zaczyna się od instrukcji FROM, która określa „system startowy” dla Twojej aplikacji. Dla Node.js często używa się czegoś w stylu