Docker dla web developera — od zera do docker compose up
Łagodne wprowadzenie do Dockera dla web developera. Kontener vs maszyna wirtualna po ludzku, pierwszy Dockerfile, docker-compose z bazą i wolumeny krok po kroku.
Jeśli kiedykolwiek usłyszałeś (albo wypowiedziałeś) magiczne zdanie „u mnie działa” — ten wpis jest dla Ciebie. Docker to narzędzie, które kończy z gadaniem o tym, że coś działa na jednym komputerze, a na drugim już nie. Pokażę Ci go od zera: bez żargonu, krok po kroku, aż do momentu, w którym jednym poleceniem docker compose up postawisz aplikację razem z bazą danych. Nie potrzebujesz wcześniejszego doświadczenia z DevOps — wystarczy, że piszesz strony albo aplikacje.
Po co w ogóle Docker?
Wyobraź sobie typowy scenariusz. Piszesz aplikację na swoim laptopie. Masz Node w wersji 20, PHP 8.3, PostgreSQL 16 i kilka bibliotek systemowych. Wszystko śmiga. Wysyłasz kod koledze — a u niego nie startuje, bo ma Node 18. Wrzucasz na serwer — pada, bo tam jest inna wersja biblioteki graficznej. Każde środowisko jest trochę inne i te różnice potrafią zjeść godziny.
Docker rozwiązuje to tak: pakuje Twoją aplikację razem z całym jej środowiskiem — konkretną wersją Node czy PHP, bibliotekami systemowymi, zmiennymi — do jednej, przenośnej paczki. Ta paczka zachowuje się identycznie wszędzie: na Twoim laptopie, u kolegi i na produkcji.
Konkretne korzyści dla web developera:
- Koniec z „u mnie działa” — to samo środowisko dla całego zespołu i na serwerze.
- Spójność dev/prod — testujesz na tym, co naprawdę pojedzie na produkcji.
- Szybki start projektu — nowy programista robi
git cloneidocker compose up, zamiast spędzać dzień na konfiguracji. - Czysty system — nie instalujesz piętnastu wersji baz danych bezpośrednio na komputerze. Wszystko siedzi w kontenerach, które kasujesz jednym poleceniem.
Kontener vs maszyna wirtualna — po ludzku
Jeśli kojarzysz maszyny wirtualne (VirtualBox, VMware), pewnie myślisz: „przecież to już potrafię — odpalam całego wirtualnego Linuksa i tam stawiam aplikację”. Różnica jest jednak zasadnicza.
Maszyna wirtualna to pełny, udawany komputer. Ma własny system operacyjny, własne jądro, udawany dysk i pamięć. Żeby postawić jedną aplikację, uruchamiasz cały drugi system — to ciężkie, wolno startuje i zjada dużo RAM-u.
Kontener jest dużo lżejszy. Nie udaje całego komputera — współdzieli jądro systemu z Twoim komputerem (czy serwerem), a izoluje tylko to, co potrzebne: pliki aplikacji, biblioteki, procesy. Dzięki temu startuje w sekundy i zajmuje megabajty zamiast gigabajtów.
Najprościej mówiąc: maszyna wirtualna to osobny dom z własnym fundamentem i instalacją. Kontener to oddzielne, zamykane mieszkanie w bloku — ma swoje cztery ściany i prywatność, ale współdzieli fundament i przyłącza z resztą budynku. Stawia się go szybciej i taniej.
Kluczowe pojęcia
Zanim napiszemy pierwszy plik, ogarnijmy słownik. To tylko sześć słów, ale wracają wszędzie.
| Pojęcie | Co to jest (po ludzku) |
|---|---|
| Obraz (image) | Przepis + gotowa paczka. Niezmienny szablon, z którego powstają kontenery. Np. obraz node:20 albo Twój własny. |
| Kontener (container) | Działająca instancja obrazu. Jak proces uruchomiony z programu. Z jednego obrazu możesz odpalić wiele kontenerów. |
| Dockerfile | Przepis na zbudowanie Twojego obrazu. Plik tekstowy z instrukcjami „weź Node, skopiuj kod, zainstaluj zależności”. |
| Rejestr (registry) | Magazyn obrazów. Docker Hub to taki „npm dla obrazów” — pobierasz stamtąd gotowe obrazy baz, serwerów itd. |
| Wolumen (volume) | Trwały magazyn na dane. Kontenery są ulotne — wolumen sprawia, że dane bazy przeżyją restart i usunięcie kontenera. |
| Sieć (network) | Wirtualna sieć łącząca kontenery. Dzięki niej aplikacja widzi bazę po nazwie, np. db, bez znajomości jej adresu IP. |
Obraz to klasa, kontener to obiekt
Jeśli programujesz obiektowo, najłatwiej zapamiętać to tak: obraz jest jak klasa (definicja), a kontener jak jej instancja (konkretny obiekt). Z jednej klasy robisz wiele obiektów — z jednego obrazu wiele kontenerów.
Pierwszy Dockerfile
Zacznijmy od czegoś prostego — aplikacji w Node. Dockerfile to plik bez rozszerzenia, który kładziesz w głównym katalogu projektu. Oto minimalny, sensowny przykład:
# Bazujemy na oficjalnym, lekkim obrazie Node 20 (wariant alpine = mały rozmiar)
FROM node:20-alpine
# Katalog roboczy wewnątrz kontenera — tu wyląduje nasz kod
WORKDIR /app
# Najpierw kopiujemy tylko pliki z zależnościami.
# Dzięki temu Docker zacache'uje warstwę z npm install i nie powtórzy jej,
# dopóki package.json się nie zmieni.
COPY package*.json ./
RUN npm ci --omit=dev
# Teraz reszta kodu aplikacji
COPY . .
# Informujemy, na którym porcie nasłuchuje aplikacja
EXPOSE 3000
# Polecenie startowe kontenera
CMD ["node", "server.js"]
Każda linia to jedna instrukcja. FROM mówi, od jakiego obrazu zaczynamy. WORKDIR ustawia katalog. COPY przenosi pliki z Twojego dysku do obrazu. RUN wykonuje polecenie w trakcie budowania (np. instalację zależności). CMD to polecenie, które uruchomi się przy starcie kontenera.
A gdybyś pracował w PHP — analogiczny Dockerfile wygląda tak:
# Oficjalny obraz PHP z serwerem Apache
FROM php:8.3-apache
# Instalujemy rozszerzenia PHP potrzebne np. do MySQL/PostgreSQL
RUN docker-php-ext-install pdo pdo_mysql
# Kopiujemy kod do katalogu serwowanego przez Apache
COPY ./src /var/www/html/
EXPOSE 80
Dodaj .dockerignore
Tak jak masz .gitignore, dodaj plik .dockerignore. Bez niego COPY . . wciągnie do obrazu node_modules, .git i inne śmieci — obraz spuchnie i budowanie zwolni. Minimum to wpisać tam node_modules, .git i .env.
Budowanie i uruchamianie — podstawowe komendy
Mając Dockerfile, budujesz obraz i uruchamiasz z niego kontener:
# Zbuduj obraz i nadaj mu nazwę (tag) "moja-app"
docker build -t moja-app .
# Uruchom kontener z tego obrazu.
# -d = w tle (detached)
# -p = mapowanie portu: host:kontener (z zewnątrz 8080, wewnątrz 3000)
docker run -d -p 8080:3000 --name moja-app-1 moja-app
# Zobacz, co działa
docker ps
# Podejrzyj logi kontenera (-f = śledź na żywo)
docker logs -f moja-app-1
# Zatrzymaj i usuń kontener
docker stop moja-app-1
docker rm moja-app-1
Po docker run wejdź w przeglądarce na http://localhost:8080 — port 8080 z Twojego komputera prowadzi do portu 3000 wewnątrz kontenera. To mapowanie portów (-p) jest kluczowe: bez niego kontener działa, ale nie dostaniesz się do niego z zewnątrz.
docker-compose — spinamy aplikację z bazą
Pojedynczy kontener to dopiero połowa sukcesu. Prawdziwa aplikacja to zwykle co najmniej kilka usług: sama aplikacja, baza danych, czasem cache (Redis). Uruchamianie ich ręcznie, jedna po drugiej, z docker run byłoby męczące. Od tego jest Docker Compose — opisujesz cały zestaw usług w jednym pliku docker-compose.yml, a potem stawiasz wszystko jednym poleceniem.
Oto kompletny przykład: aplikacja Node + baza PostgreSQL + cache Redis, wszystko w jednej sieci.
services:
app:
build: . # buduj z Dockerfile w tym katalogu
ports:
- "8080:3000" # host:kontener
environment:
DATABASE_URL: "postgres://app:tajne_haslo@db:5432/appdb"
REDIS_URL: "redis://cache:6379"
depends_on:
- db
- cache
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: tajne_haslo
POSTGRES_DB: appdb
volumes:
- db-data:/var/lib/postgresql/data # trwałe dane bazy
ports:
- "5432:5432" # opcjonalnie: dostęp z hosta np. przez DBeaver
cache:
image: redis:7-alpine
volumes:
db-data:
Zwróć uwagę na trzy rzeczy. Po pierwsze, w DATABASE_URL host bazy to db — czyli nazwa usługi. Compose tworzy wspólną sieć, w której usługi widzą się po nazwach, bez żadnych adresów IP. Po drugie, depends_on mówi Compose, żeby najpierw wystartował bazę i cache, a dopiero potem aplikację. Po trzecie, baza ma podpięty wolumen db-data — i właśnie o nim za chwilę.
Polecenia Compose są krótkie i przyjemne:
# Zbuduj (jeśli trzeba) i uruchom cały stos w tle
docker compose up -d
# Zobacz logi wszystkich usług razem
docker compose logs -f
# Logi tylko jednej usługi
docker compose logs -f app
# Lista działających usług
docker compose ps
# Zatrzymaj i usuń kontenery oraz sieć (wolumeny zostają!)
docker compose down
# To samo, ale skasuj też wolumeny (UWAGA: usuwa dane bazy)
docker compose down -v
Wolumeny — żeby dane przeżyły
To jeden z najważniejszych momentów, w których początkujący się sparzą. Kontener jest ulotny. Jeśli zapiszesz dane bazy wewnątrz kontenera i go usuniesz — dane znikają razem z nim. Zrobiłeś docker compose down i restart, a baza jest pusta? Właśnie dlatego.
Rozwiązaniem jest wolumen: trwały magazyn, który żyje niezależnie od kontenera. W przykładzie wyżej linia db-data:/var/lib/postgresql/data mówi: „katalog, w którym PostgreSQL trzyma dane, podepnij do wolumenu db-data”. Kasujesz i odtwarzasz kontener bazy do woli — dane zostają.
Wolumen kontra bind mount
Są dwa rodzaje montowania. Wolumen (jak db-data wyżej) jest zarządzany przez Dockera — idealny na dane baz. Bind mount podpina konkretny katalog z Twojego dysku do kontenera, np. ./src:/app/src — świetny w trakcie programowania, bo zmiany w kodzie od razu widać w kontenerze bez przebudowy obrazu.
Częste błędy początkujących
Kilka pułapek, na które każdy się kiedyś nadział — dobrze znać je z góry:
- Brak mapowania portów. Kontener działa,
docker pspokazuje, że żyje, alelocalhostnie odpowiada. Prawie zawsze chodzi o brakujące albo źle ustawione-p host:kontener. - Mylenie portu hosta z portem kontenera. W
8080:3000to aplikacja wewnątrz musi nasłuchiwać na3000. Jeśli serwer startuje na innym porcie, mapowanie nie pomoże. - Dane znikają po restarcie. Brak wolumenu pod bazą — patrz wyżej.
- Łączenie się z bazą po
localhost. Wewnątrz kontenera aplikacjilocalhostto ten kontener, nie baza. Hostem bazy jest nazwa usługi z Compose, czylidb. - Gigantyczne obrazy. Brak
.dockerignorei bazowanie na pełnym obrazie zamiast wariantualpinepotrafi rozdąć obraz do setek megabajtów. - Edytujesz kod, a kontener go nie widzi. Bez bind mount kod jest „zapieczony” w obrazie w momencie budowania. Albo przebuduj (
docker compose up --build), albo podepnij katalog przez bind mount na czas developmentu.
Gdzie to się przydaje w praktyce
Najczęstszy realny scenariusz dla web developera to lokalne środowisko developerskie, które stawia się w sekundy i jest identyczne dla całego zespołu. Świetnie sprawdza się to choćby przy stawianiu WordPressa z osobną bazą i warstwą API — pisałem o takim podejściu we wpisie o headless WordPress, gdzie Docker pozwala spiąć backend, bazę i frontend w jeden powtarzalny stos.
Podsumowanie
Docker brzmi groźnie, dopóki nie zrozumiesz, że sprowadza się do kilku prostych klocków: obraz (przepis), kontener (uruchomiona instancja), Dockerfile (jak zbudować obraz), wolumen (trwałe dane) i Compose (spięcie wielu usług). Gdy raz przejdziesz drogę od docker build do docker compose up -d, trudno wrócić do ręcznego instalowania baz na własnym systemie.
Zacznij od małego kroku: weź swój istniejący projekt, napisz prosty Dockerfile i postaw go lokalnie. Potem dołóż bazę przez Compose. Reszta przyjdzie sama.
Jeśli chcesz, żebym pomógł skonteneryzować Twój projekt albo postawić powtarzalne środowisko dla zespołu — napisz do mnie.