Wróć na blog
docker devops konteneryzacja narzędzia

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.

Franciszek Sikora

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 clone i docker 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.

Kontenery kontra maszyny wirtualne — porównanie warstw

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ęcieCo 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.
DockerfilePrzepis 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.

Stos z docker-compose: aplikacja, baza i cache w jednej sieci

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 ps pokazuje, że żyje, ale localhost nie odpowiada. Prawie zawsze chodzi o brakujące albo źle ustawione -p host:kontener.
  • Mylenie portu hosta z portem kontenera. W 8080:3000 to aplikacja wewnątrz musi nasłuchiwać na 3000. 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 aplikacji localhost to ten kontener, nie baza. Hostem bazy jest nazwa usługi z Compose, czyli db.
  • Gigantyczne obrazy. Brak .dockerignore i bazowanie na pełnym obrazie zamiast wariantu alpine potrafi 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.