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

0
33
1/5 - (1 vote)

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:

FROM node:18-alpine

Czyli: „weź oficjalny obraz Node 18 w lekkiej wersji Alpine Linux”. Mit bywa taki, że zawsze trzeba brać „najnowsze możliwe” wersje. W praktyce stabilny, dobrze przetestowany Node 18 często będzie lepszym wyborem niż świeżutki 22, zwłaszcza gdy dogadujesz się z zespołem lub istniejącą aplikacją.

Pełny przykład Dockerfile dla „Hello API”

W katalogu z package.json i index.js utwórz plik Dockerfile o takiej treści:

FROM node:18-alpine

# Ustaw katalog roboczy wewnątrz kontenera
WORKDIR /app

# Skopiuj pliki definiujące zależności w pierwszej kolejności
COPY package*.json ./

# Zainstaluj zależności (bez dev-dependencies w tym przykładzie)
RUN npm install --only=production

# Teraz skopiuj resztę plików aplikacji
COPY . .

# Aplikacja nasłuchuje na porcie 3000
EXPOSE 3000

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

Co robi każda linijka Dockerfile

Rozbijając ten plik na kawałki:

FROM node:18-alpine

To punkt wyjścia. Dostajesz minimalny system z zainstalowanym Node 18. Nie musisz znać się na Alpine, żeby go użyć – traktujesz to jako „lekki Linux z Node”.

WORKDIR /app

Ustawia katalog roboczy dla kolejnych instrukcji. Od tego momentu każda komenda (np. RUN, COPY, CMD) będzie wykonywana tak, jakby znajdowała się w katalogu /app. To porządkuje strukturę i unikasz bałaganu typu pliki walające się po /.

COPY package*.json ./

Kopiujesz pliki package.json i ewentualnie package-lock.json do katalogu roboczego. Tylko te dwa pliki, nic więcej. Powód jest prosty – chcesz zbudować warstwę z zależnościami i cachować ją, dopóki nie zmienią się pliki z definicjami paczek.

RUN npm install --only=production

W środku obrazu uruchamiasz instalację zależności. Powstaje nowa warstwa ze wszystkimi modułami NPM. Jeśli nie dotykasz package.json, ta warstwa będzie później wykorzystywana z cache i build znacząco przyspieszy.

COPY . .

Dopiero teraz kopiujesz cały pozostały kod aplikacji. Dzięki temu drobna zmiana w index.js nie wymusza ponownej instalacji całego NPM – Docker wykorzysta cache do warstw z npm install.

EXPOSE 3000

Informacyjnie oznaczasz, że aplikacja nasłuchuje na porcie 3000. To nie otwiera portu na hoście (od tego jest -p w docker run), ale pomaga narzędziom i innym ludziom zorientować się, który port jest istotny.

CMD ["npm", "start"]

Ustawiasz domyślną komendę, która ma zostać uruchomiona po starcie kontenera. Użycie formatu JSON (tablica) jest bezpieczniejsze niż wersja w postaci „jednego stringa”, bo unikasz dziwnych problemów ze spacjami i powłoką.

Budowanie obrazu z Dockerfile

Mając Dockerfile, budujesz własny obraz:

Budowanie obrazu: nadanie nazwy i tagu

Czas „spakować” aplikację do obrazu Dockera. W katalogu z Dockerfile uruchom:

docker build -t hello-api:1.0.0 .

Znaczenie:

  • docker build – zbuduj obraz na podstawie Dockerfile,
  • -t hello-api:1.0.0 – nadaj obrazowi czytelną nazwę i tag (jak wersja),
  • . – kontekst builda, czyli „weź bieżący katalog jako źródło plików”.

Mit bywa taki, że tag musi odzwierciedlać dokładną wersję aplikacji jak w korporacyjnym wydaniu. Na start spokojnie wystarczy coś prostego typu :1.0.0, :dev czy :local – ważne, żebyś wiedział, co jest czym.

Po udanym buildzie sprawdzisz lokalne obrazy:

docker images

Wśród nich powinien pojawić się hello-api z tagiem 1.0.0.

Uruchomienie „Hello API” w kontenerze

Skoro obraz jest gotowy, pora go odpalić:

docker run -d --name hello-api-container -p 3000:3000 hello-api:1.0.0

W skrócie:

  • -d – serwer działa w tle,
  • --name hello-api-container – własna nazwa kontenera,
  • -p 3000:3000 – wystaw port 3000 kontenera na port 3000 hosta,
  • hello-api:1.0.0 – obraz, który przed chwilą zbudowałeś.

Otwórz przeglądarkę i wejdź na http://localhost:3000. Jeśli wszystko poszło zgodnie z planem, pojawi się Twój JSON:

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

Jeśli strona się nie ładuje, sprawdź logi kontenera:

docker logs hello-api-container

Zmiana portu bez przerabiania kodu

Załóżmy, że na porcie 3000 masz już inną aplikację. Z perspektywy Dockera nie ma dramatu – przekierowanie portu załatwia sprawę:

docker run -d --name hello-api-alt -p 8080:3000 hello-api:1.0.0

W kontenerze aplikacja nadal nasłuchuje na porcie 3000, a na Twoim komputerze trafisz do niej pod adresem http://localhost:8080. Bez zmian w kodzie, bez zabawy w konfigurację serwera.

Możesz też nadpisać port od strony aplikacji (zmienna środowiskowa PORT), ale w tym prostym przykładzie sama gra portami w -p zwykle wystarczy.

Szybkie debugowanie działającego kontenera

Czasem kontener wstaje, ale „coś nie działa”. Najprostsza ścieżka: wejdź do środka i zobacz to na własne oczy:

docker exec -it hello-api-container sh

Jeżeli w obrazie masz inną powłokę niż sh, użyj jej (np. bash). W środku możesz:

  • sprawdzić katalog /app i upewnić się, że pliki się skopiowały,
  • podejrzeć, co rzeczywiście znajduje się w node_modules,
  • spróbować ręcznie uruchomić npm start, aby zobaczyć ewentualne błędy.

Mit: „kontener to czarna skrzynka i jak coś nie działa, to trzeba go przebudować w ciemno”. Rzeczywistość: kontener to zwykły proces + system plików, do którego możesz wejść, pogrzebać, sprawdzić logi i pliki.

Aktualizacja obrazu po zmianie kodu

Zmieniłeś komunikat w index.js i chcesz zobaczyć efekt w kontenerze. Bez żadnych sztuczek kolejność jest taka:

  1. zatrzymaj i usuń stary kontener,
  2. przebuduj obraz,
  3. uruchom kontener z nowego obrazu.

Przykładowo:

docker stop hello-api-container
docker rm hello-api-container

docker build -t hello-api:1.0.1 .
docker run -d --name hello-api-container -p 3000:3000 hello-api:1.0.1

Jeśli zmieniasz tylko kod (a nie zależności w package.json), build będzie szybki, bo warstwa z npm install zostanie wzięta z cache.

W projektach deweloperskich często mapuje się kod z hosta do kontenera (-v), żeby nie budować obrazu po każdej zmianie. Na start jednak dobrze jest poczuć pełny cykl zmiana → build → uruchomienie, bo to dokładnie to, co później wykona CI/CD.

Lokalne środowisko deweloperskie z wolumenem

Jeśli chcesz kodować na żywo i widzieć efekty bez przebudowy obrazu, możesz połączyć dockera z voluemem tak:

docker run -d 
  --name hello-api-dev 
  -p 3000:3000 
  -v $(pwd):/app 
  -w /app 
  node:18-alpine 
  sh -c "npm install && npm start"

Co tu się dzieje:

  • używasz surowego obrazu node:18-alpine,
  • mapujesz katalog projektu do /app w kontenerze,
  • ustawiasz katalog roboczy na /app,
  • instalujesz zależności i uruchamiasz aplikację na żywo z Twojego systemu plików.

Zmiana w index.js na hoście od razu jest widoczna w kontenerze. W połączeniu z narzędziami typu nodemon możesz mieć pełny live-reload w środku kontenera.

To podejście pokazuje też, że Docker nie musi być tylko narzędziem do „produkcji”. Dla wielu osób staje się codziennym „izolowanym środowiskiem developerskim”, zamiast instalowania dziesięciu wersji Node/Pythona na hoście.

Dlaczego warstwy mają znaczenie w codziennej pracy

Dockerowy build to nie jedna wielka operacja, tylko seria kroków zamienianych w warstwy. Każda instrukcja RUN, COPY czy ADD tworzy nową warstwę, która może zostać użyta ponownie przy kolejnym buildzie.

Przy prostym „Hello API” może nie brzmi to groźnie, ale przy większym projekcie:

  • instalacja zależności potrafi trwać długo,
  • kopiowanie gigabajtów kodu i assetów spowalnia buildy,
  • każda niepotrzebna zmiana w plikach wejściowych może unieważnić cache.

Dlatego kolejność w Dockerfile ma realny wpływ na Twoją prędkość pracy. Klasyczny wzorzec:

  1. najpierw kopiuj „rzadko zmieniające się” pliki (definicje zależności),
  2. instaluj zależności,
  3. na końcu kopiuj dynamiczny kod aplikacji.

Mit: „optymalizacja Dockerfile ma sens dopiero w dużych projektach”. W praktyce nawet przy małym API różnica między 2-sekundowym a 20-sekundowym buildem robi się odczuwalna, gdy robisz to kilkadziesiąt razy dziennie.

Od prostego API do środowiska z bazą danych

Gdy prosty serwer HTTP działa w kontenerze, naturalnym kolejnym krokiem jest dołożenie bazy danych jako osobnego kontenera. Zamiast instalować lokalnego Postgresa czy MongoDB, możesz:

docker run -d 
  --name hello-db 
  -e POSTGRES_PASSWORD=secret 
  -e POSTGRES_USER=hello 
  -e POSTGRES_DB=hello_db 
  -p 5432:5432 
  postgres:16-alpine

Aplikacja w kontenerze Node łączy się wtedy z bazą nie po localhost, tylko po nazwie kontenera (np. hello-db) i odpowiednim porcie. W pojedynkę można to spiąć samym Dockerem, a przy większej konfiguracji dochodzi Docker Compose, który pilnuje całości jako „jednego środowiska”.

To jest moment, w którym kontenery naprawdę zaczynają błyszczeć: możesz skasować całe środowisko jednym poleceniem, stworzyć je od nowa i mieć pewność, że koledzy z zespołu dostaną identyczny zestaw usług.

Częste problemy przy pierwszych uruchomieniach

Kilka rzeczy, które regularnie gryzą początkujących, można rozbroić od razu:

  • „Aplikacja działa lokalnie, ale nie w kontenerze” – najczęściej problem z portem (nasłuchujesz np. tylko na 127.0.0.1) lub z zależnościami systemowymi, których nie ma w obrazie. Logi kontenera zwykle jasno to pokazują.
  • „Brak dostępu do internetu z kontenera” – czasem lokalny firewall blokuje ruch z sieci Docker. Szybki test: uruchom docker run --rm alpine ping -c 3 8.8.8.8 i zobacz, czy wyjdzie w świat.
  • „Zmieniłem kod, a kontener pokazuje starą wersję” – jeśli nie używasz -v, musisz przebudować obraz i uruchomić nowy kontener. Starego warto usunąć, żeby się nie mylił w docker ps -a.

Gdy zaczniesz patrzeć na kontener jak na zwykły proces (z logami, portami i plikami), debugowanie przestaje być „magią Dockera”, a wraca do dobrze znanego schematu: sprawdź logi, konfigurację, sieć.

Najczęściej zadawane pytania (FAQ)

Czym właściwie jest Docker i kontener – czy to mała wirtualna maszyna?

Docker uruchamia kontenery, czyli zwykłe procesy na twoim systemie, odizolowane od reszty: mają własny system plików, własne zmienne środowiskowe i widzą tylko te porty czy katalogi, które im udostępnisz. Kontener nie ma własnego kernela, nie instalujesz w nim „drugiego Windowsa w środku Windowsa”.

Popularny mit mówi, że „kontener to mała wirtualka”. W rzeczywistości kontenery współdzielą jądro systemu z hostem, dlatego są lekkie i startują w ułamku sekundy. Maszyna wirtualna emuluje cały sprzęt i osobny system operacyjny, kontener jedynie nakłada izolację na proces.

Po co początkującemu programiście Docker, skoro wszystko mogę zainstalować lokalnie?

Docker daje przewidywalne środowisko: obraz raz zbudowany działa tak samo na twoim laptopie, na komputerze znajomego i na serwerze w chmurze. Znika klasyczne „u mnie działa, u ciebie nie”, bo wszyscy uruchamiają dokładnie ten sam zestaw binarek i zależności.

Kontenery ułatwiają też szybkie testy: możesz bez ryzyka sprawdzić inną wersję Pythona, Node.js czy bazy danych, bez mieszania w globalnych instalacjach. W praktyce wielu juniorów zaczyna od odpalania gotowych usług (PostgreSQL, Redis, nginx) jednym poleceniem, zamiast walczyć z instalatorami i konfiguracją na systemie.

Jaka jest różnica między obrazem Dockera a kontenerem?

Obraz (image) to niezmienny szablon: zawiera system plików (binarki, biblioteki, twój kod) oraz informację, co ma zostać uruchomione (CMD/ENTRYPOINT). W trakcie działania nic się do obrazu nie „dopisywuje” – jest traktowany jak zamrożona migawka środowiska.

Kontener to uruchomiony obraz, czyli żywy proces z własnym stanem, logami i danymi. Dobra analogia z programowania: obraz to klasa, kontener to obiekt. Z jednego obrazu możesz uruchomić wiele kontenerów, każdy z inną konfiguracją i osobnymi danymi.

Skąd Docker pobiera obrazy i czym jest Docker Hub?

Obrazy Dockera przechowywane są w rejestrach (registry). Najpopularniejszy publiczny rejestr to Docker Hub, dostępny pod adresem hub.docker.com. Gdy wpisujesz polecenie w stylu docker run nginx:alpine i lokalnie nie masz tego obrazu, Docker domyślnie pobierze go właśnie z Docker Hub.

Nazwa obrazu zwykle ma postać nazwa, nazwa:tag albo uzytkownik/obraz:tag. Tag oznacza konkretną wersję lub wariant, np. node:18, python:3.11 czy odchudzone wersje typu -alpine. Mit jest taki, że „jeden obraz do wszystkiego wystarczy”; w praktyce obrazy wersjonuje się tagami, żeby mieć kontrolę nad środowiskiem.

Co oznacza tag latest w Dockerze i czy powinienem go używać?

Tag latest to tylko domyślna etykieta ustawiona przez autora obrazu, a nie żadna gwarancja „najbezpieczniejszej” czy „produkcyjnej” wersji. Dziś node:latest może znaczyć Node 20, za jakiś czas Node 22, bez zmiany nazwy tagu.

Jeśli budujesz coś, co ma działać powtarzalnie, używaj konkretnych tagów, np. node:18.20 albo postgres:16. :latest zostaw raczej do szybkich eksperymentów lokalnych, bo opieranie aplikacji produkcyjnej na „zawsze najnowszym” wariancie kończy się zwykle trudnymi do odtworzenia błędami po ponownym buildzie obrazu.

Jak zainstalować Dockera na Windows i co z tym całym WSL2?

Na Windows 10/11 najprostsza opcja to Docker Desktop. W praktyce wygląda to tak: w BIOS/UEFI włączasz wirtualizację (Intel VT-x/AMD-V), w systemie włączasz WSL2 oraz wymagane funkcje wirtualizacji, a następnie instalujesz Docker Desktop jak zwykły program. Podczas instalacji warto wybrać tryb WSL2 – jest lżejszy niż klasyczny Hyper-V i sprawia mniej kłopotów.

Docker Desktop w pakiecie dostarcza Docker Engine i narzędzie docker, więc nie musisz nic dodatkowo konfigurować. Typowe problemy (komunikaty o braku wirtualizacji czy niewłączonym WSL2) zwykle rozwiązują się po włączeniu odpowiednich opcji w systemie i ponownym uruchomieniu komputera.

Czy powinienem „konfigurować” kontener od środka, jak zwykły serwer?

Kusi, żeby wejść do działającego kontenera, doinstalować pakiety, coś poprawić ręcznie i uznać, że sprawa załatwiona. Technicznie zadziała, ale niszczy całą ideę powtarzalności – takiego kontenera nie odtworzysz później z samych poleceń docker build i docker run.

Zdrowszy nawyk: wszystkie zmiany wprowadzać w Dockerfile, budować nowy obraz i dopiero z niego uruchamiać kontenery. Mit „kontener jak mały serwer do klikania i dłubania” prowadzi do niepowtarzalnych, trudnych w utrzymaniu środowisk. Rzeczywistość jest prostsza: obraz opisujesz w kodzie, a kontener traktujesz jak jednorazowy, łatwy do wyrzucenia i odtworzenia proces.

Najważniejsze punkty

  • Kontener Dockera to zwykły proces na systemie hosta, izolowany systemem plików, zmiennymi środowiskowymi i konfiguracją dostępu – to nie jest osobny system operacyjny ani „Windows w Windowsie”.
  • Mit „kontener = mała wirtualka” jest mylący: maszyna wirtualna emuluje cały system (sprzęt + kernel), a kontener korzysta ze wspólnego jądra hosta, dzięki czemu startuje w ułamku sekundy i zużywa mniej zasobów.
  • Docker rozwiązuje typowe problemy z zależnościami i środowiskami: ten sam obraz działa identycznie na różnych maszynach, projekty nie mieszają swoich wersji Pythona czy Node.js, a zepsuty kontener można po prostu wyrzucić i stworzyć na nowo.
  • Dla początkującego największy zysk to szybkie uruchamianie gotowych usług (bazy danych, backendy, narzędzia CLI) jednym poleceniem docker run ..., bez instalowania całego stosu technologicznego na lokalnym systemie.
  • Obraz to niezmienny szablon (jak klasa), a kontener to jego uruchomiona instancja (jak obiekt); modyfikuje się obrazy przez przebudowanie, a nie przez „ręczne grzebanie w środku” działającego kontenera, bo to łamie powtarzalność.
  • Obrazy są pobierane z rejestrów (np. Docker Hub), identyfikowane przez nazwę i tag (np. nginx:alpine, node:18); tag odzwierciedla wersję lub wariant, więc poleganie ślepo na latest bywa proszeniem się o niespodzianki przy aktualizacjach.