Docker Compose w praktyce: uruchom stos aplikacji w 15 minut bez bólu

0
135
2.9/5 - (7 votes)

Nawigacja:

Po co Docker Compose, skoro jest sam Docker?

Ręczne docker run kontra deklaratywny docker-compose.yml

Uruchomienie pojedynczego kontenera za pomocą docker run jest proste i szybkie. Problem zaczyna się, gdy aplikacja składa się z kilku elementów: backend, baza danych, cache, worker, narzędzia pomocnicze. Wtedy linie poleceń robią się długie, trudne do odtworzenia i podatne na błędy.

Docker Compose wprowadza podejście deklaratywne: konfiguracja całego stosu trafia do jednego pliku docker-compose.yml. Zamiast zapamiętywać dziesięć komend docker run, wystarczy jeden plik i jedno polecenie:

docker compose up

Plik jest wersjonowany razem z kodem, można go przeglądać, recenzować w code review, modyfikować i porównywać między gałęziami. To różnica podobna do ręcznego wywoływania komend SQL w konsoli vs posiadanie migracji w plikach – oba podejścia działają, lecz jedno dużo lepiej skaluje się w czasie i w zespole.

Kiedy wystarczy pojedynczy kontener, a kiedy potrzebny jest stos

Sam Docker i docker run sprawdzają się, gdy:

  • testujesz pojedynczy obraz (np. gotowy serwer Redis lub Nginx),
  • uruchamiasz tymczasowe narzędzia typu alpine lub busybox do jednorazowych zadań,
  • masz prostą aplikację monolityczną bez zależności (np. same statyczne pliki).

Docker Compose jest nieporównanie wygodniejszy, gdy dochodzi choćby jedna zależność: baza danych, broker wiadomości, osobny frontend, osobny worker do zadań w tle. Nawet app + db to już sensowny powód, aby przejść na Compose, bo pojawiają się kwestie:

  • ustawienia sieci (hosty, porty, izolacja),
  • trwałość danych (wolumeny),
  • konfiguracja zmiennych środowiskowych dla kilku usług naraz,
  • kolejność startu i powiązania depends_on.

Im więcej komponentów, tym większa różnica w wygodzie i powtarzalności między zestawem ręcznych komend a jednym opisanym stosie w Compose.

Typowe zastosowania Docker Compose w praktyce

Compose najczęściej pojawia się w kilku scenariuszach:

  • Lokalne środowisko developerskie – każdy developer ma identyczny stos uruchamiany jednym poleceniem; nie trzeba ręcznie instalować baz, MQ, a nawet JDK czy Node na hoście.
  • Testy integracyjne i e2e – pipeline CI może przed testami podnieść cały stos w Compose, odpalić testy i potem go zwinąć.
  • Demo dla klienta – łatwo uruchomić „mini-produkcję” na jednym serwerze czy laptopie, bez budowania pełnej infrastruktury.
  • Środowisko QA / UAT – zespół testerski działa na spójnej konfiguracji, która jest bliska lokalnemu devowi.

W tych scenariuszach Compose jest szybszy do wdrożenia niż pełna orkiestracja, a jednocześnie daje znacznie więcej porządku niż luźne kontenery uruchamiane ręcznie.

Gdzie kończy się Compose, a zaczyna Kubernetes lub Swarm

Docker Compose nie zastępuje pełnoprawnej platformy orkiestracyjnej. Nie zajmuje się automatycznym skalowaniem, samonaprawianiem klastrów, rolloutami, rolling update’ami na dziesiątkach węzłów. Jest zaprojektowany przede wszystkim do:

  • pracy na jednym hoście (lokalnie lub serwer testowy),
  • prostego skalowania w granicach jednego serwera (replicas przez docker compose up --scale),
  • scenariuszy dev / test / demo / mała produkcja.

Gdy pojawia się potrzeba:

  • automatycznego restartu usług na innych serwerach po awarii hosta,
  • zaawansowanych polityk autoskalowania,
  • dynamicznego routingowania ruchu i service discovery w skali wielu maszyn,

na scenę wchodzi Kubernetes (lub w mniejszej skali Swarm). Compose i K8s często współistnieją: Compose na lokalnych laptopach, Kubernetes na produkcji, a konfiguracje są do siebie w pewnym stopniu mapowane.

Krótkie przypomnienie podstaw Dockera – tylko to, co potrzebne

Obraz, kontener, wolumen – układanka, którą wykorzystuje Compose

Zrozumienie trzech podstawowych pojęć bardzo ułatwia korzystanie z Docker Compose:

  • Obraz (image) – szablon systemu plików i metadanych. Tworzy się go zwykle przez docker build na podstawie Dockerfile. Jest niezmienny – jak snapshot.
  • Kontener – działająca instancja obrazu. Ma własny system plików (warstwo­wy), swoje procesy, konfigurację sieci.
  • Wolumen (volume) – miejsce na dane trwalsze niż życie kontenera. Używany dla baz danych, logów, uploadów.

Docker Compose nie zastępuje tych elementów, tylko je opisuje i łączy: określa, z jakich obrazów mają powstać kontenery, jakie wolumeny mają być podpięte i w jakiej sieci mają się komunikować.

Minimum komend Dockera, które porządkuje Compose

Przy pracy bez Compose często używa się kilku podstawowych komend:

  • docker build -t myapp . – budowa obrazu z bieżącego katalogu,
  • docker run -d -p 8080:80 --name myapp myapp – uruchomienie kontenera w tle,
  • docker ps – lista działających kontenerów,
  • docker logs myapp – podgląd logów kontenera,
  • docker stop myapp / docker rm myapp – zatrzymanie i usunięcie kontenera.

Compose agreguje wiele takich operacji. Na przykład docker compose up --build potrafi jednocześnie zbudować obrazy, stworzyć sieci, uruchomić kilka kontenerów i podpiąć wolumeny. Znajomość „gołych” komend pomaga zrozumieć, co Compose robi pod spodem i ułatwia diagnozowanie problemów.

Sieci i porty w Dockerze bez Compose

Domyślnie Docker tworzy sieć typu bridge. Kontenery uruchomione w tej sieci widzą się po adresach IP, ale nie mają dostępu z zewnątrz, dopóki nie opublikuje się portów.

Standardowe mapowanie portu bez Compose wygląda tak:

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

Port 80 wewnątrz kontenera jest wtedy dostępny na porcie 8080 hosta. Przy kilku usługach i różnych portach robi się to mało czytelne. Compose opisuje te mapowania w zorganizowany sposób i dodatkowo nadaje usługom nazwy hostów w obrębie sieci, dzięki czemu backend może odwoływać się do db:5432, a nie do zmiennego adresu IP.

Dlaczego te podstawy są kluczowe dla diagnozy problemów w Compose

Wiele błędów przy korzystaniu z Compose sprowadza się do klasycznych problemów Dockera: brak wolumenu, źle zmapowany port, konflikt nazw kontenerów, kontener wychodzący z błędem jeszcze przed przyłączeniem się do sieci. Gdy wiadomo, jak ręcznie sprawdzić:

  • docker ps – czy kontener faktycznie działa,
  • docker logs nazwa_uslugi-1 – jakie są logi przy starcie,
  • docker inspect – jak wygląda konfiguracja sieci i wolumenów,

diagnostyka z Compose staje się przewidywalna. W praktyce wielu problemów da się uniknąć, myśląc o usługach Compose jak o normalnych kontenerach Dockera zebranych w grupę.

Dwie osoby programują wspólnie przy laptopie w biurze
Źródło: Pexels | Autor: Ron Lach

Anatomia pliku docker-compose.yml – składniki i różnice wersji

Podstawowa struktura: services, volumes, networks

Współczesny Compose w wersji pluginu CLI (Docker Compose V2) nie wymaga już sekcji version w pliku. Klasyczny szkielet wygląda tak:

services:
  app:
    image: myapp:latest

  db:
    image: postgres:15

volumes:
  db-data:

networks:
  default:
    driver: bridge

Sekcja services definiuje kontenery, które mają zostać uruchomione, z ich konfiguracją. Sekcja volumes rezerwuje nazwy i parametry wolumenów, a networks – sieci. Jeżeli sieci nie zdefiniujesz, Compose i tak utworzy domyślną sieć o nazwie opartej na katalogu projektu, więc można zacząć minimalistycznie.

Kluczowe pola usług: image, build, ports, volumes, environment

Definicja jednej usługi składa się z kilku często używanych pól. Przykład:

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=app
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Najważniejsze elementy:

  • image – nazwa obrazu z rejestru (np. Docker Hub) lub lokalnego. Używane, gdy nie budujesz obrazu samodzielnie.
  • build – katalog lub parametry budowania obrazu na podstawie Dockerfile. Często ustawiane na ., gdy Dockerfile leży w katalogu projektu.
  • ports – mapowanie portów w formacie host:kontener. Można podawać jako string lub osobne pola.
  • volumes – podpinanie wolumenów i katalogów: nazwa_wolumenu:ścieżka_w_kontenerze lub ./kod:/app dla bind mount.
  • environment – zmienne środowiskowe dla kontenera, przydatne do przekazywania konfiguracji (hasła, URL-e, feature flagi).
  • depends_on – zależności pomiędzy usługami. Informuje Compose o kolejności startu, ale nie gwarantuje dostępności aplikacyjnej (do tego służą zdrowotne checki lub retry w kodzie).
  • command – nadpisanie domyślnej komendy startowej z obrazu.
  • restart – polityka restartu jak w czystym Dockerze, np. always, on-failure.

Minimalny a rozbudowany przykład – jak rośnie złożoność

Minimalny plik pozwalający uruchomić prostą aplikację webową:

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"

Ten plik zawiera jednoznaczną definicję usługi, ale nic ponad absolutne minimum. Dla porównania, bardziej realny stos ma już konfigurację sieci, wolumenów i środowisk:

services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - API_URL=http://backend:8000
    depends_on:
      - backend

  backend:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
    volumes:
      - ./backend:/app
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=app
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Struktura jest taka sama, tylko ilość konfiguracji rośnie. Dzięki temu można precyzyjnie kontrolować sposób działania całego stosu, od kodu po dane i sieć.

docker-compose vs docker compose – narzędzie a plugin

Przez lata istniały dwa narzędzia: osobny binarny docker-compose (V1) oraz nowszy plugin docker compose (V2), integrujący Compose z głównym CLI Dockera. Obecnie rekomendowany jest właśnie plugin:

Cechadocker-compose (V1)docker compose (V2)
InstalacjaOsobny plik binarnyPlugin wbudowany w Docker Desktop / pakiet Dockera
Składnia wywołaniadocker-compose updocker compose up
WsparcieTryb utrzymania, bez rozwojuAktywnie rozwijany, nowe funkcje
Plik YAMLWymagał sekcji versionMoże działać bez version

W nowych projektach lepiej używać docker compose. Składnia pliku jest w praktyce taka sama, różni się tylko sposób instalacji i wywołania narzędzia.

Pierwszy działający stos w 15 minut: backend + baza danych

Założenia przykładowego stosu

Dla konkretu przyjmijmy prosty scenariusz:

  • backend HTTP (np. Python / Node / Go) nasłuchujący na porcie 8000 wewnątrz kontenera,
  • baza danych Postgres 15,
  • lokalny development – kod ma być montowany z hosta, żeby nie przebudowywać obrazu przy każdej zmianie.

Takie połączenie jest dość uniwersalne: da się podmienić język backendu bez zmiany ogólnej struktury docker-compose.yml, a baza Postgresa zostaje ta sama. Różnica między „gołym” Dockerem a Compose zaczyna być widoczna przy zależnościach i konfiguracji.

Struktura katalogów dla prostego backendu

Dla przejrzystości przyjmijmy układ:

projekt-compose/
  backend/
    Dockerfile
    app.py          # lub index.js, main.go itd.
    requirements.txt
  docker-compose.yml

Backend może być dowolny. Najważniejsze, żeby:

  • miał Dockerfile,
  • pobierał konfigurację bazy z DATABASE_URL lub osobnych zmiennych (DB_HOST, DB_USER itd.),
  • potrafił uruchomić prosty endpoint testowy (np. /health).

Przykładowy Dockerfile backendu

Dla konkretu – prosty backend w Pythonie z FastAPI/Flaskiem, ale analogicznie można wstawić Node/Express lub coś innego:

# backend/Dockerfile
FROM python:3.11-slim

WORKDIR /app

# Instalacja zależności
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Kopia kodu
COPY . .

# Zmienna na port (opcjonalne)
ENV PORT=8000

# Domyślna komenda (np. Uvicorn / Flask)
CMD ["python", "app.py"]

Dockerfile nie musi znać nic o Compose. Buduje po prostu obraz, który uruchamia backend, a całym „orchestration” zajmie się docker compose.

docker-compose.yml: backend + Postgres

Teraz plik Compose w katalogu głównym:

services:
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://app_user:app_pass@db:5432/app_db
    depends_on:
      - db
    volumes:
      - ./backend:/app

  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=app_user
      - POSTGRES_PASSWORD=app_pass
      - POSTGRES_DB=app_db
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Backend backend widzi bazę pod nazwą hosta db, ponieważ Compose tworzy wspólną sieć. Z punktu widzenia kodu nie ma potrzeby znać adresu IP – nazwa usługi w Compose staje się DNS-em.

Start stosu w praktyce: od zera do działającego endpointu

W katalogu projektu wystarczy:

docker compose up --build

Różnice względem pojedynczych komend Dockera:

  • Obrazy dla wszystkich usług, które mają build, zostaną zbudowane w odpowiedniej kolejności.
  • Postgres dostanie własny wolumen db-data, więc dane nie znikną po restarcie kontenerów.
  • Tworzona jest sieć, w której backend i baza komunikują się po nazwach usług.

Po starcie aplikacja backendowa powinna być dostępna pod http://localhost:8000. Gdy któryś kontener się wywali, logi można podejrzeć na dwa sposoby:

docker compose logs backend
docker compose logs db

albo w trybie ciągłym:

docker compose logs -f backend

Zmiany w kodzie bez przebudowy obrazu

Dzięki:

volumes:
  - ./backend:/app

kod hosta jest automatycznie widoczny w kontenerze. Różnica w workflow w porównaniu z klasycznym Dockerem:

  • bez bind mounta – każda zmiana kodu = docker build + restart kontenera,
  • z bind mountem – edycja pliku na hostcie od razu widoczna w kontenerze; wystarczy, że aplikacja ma hot-reload (np. uvicorn --reload, nodemon).

Dla developera przypomina to lokalny run bez kontenerów, ale środowisko (baza, zależności systemowe, wersje bibliotek) jest zamknięte w obrazie Dockera.

Sieci, wolumeny i konfiguracja – przegląd trzech filarów

Przy pojedynczym kontenerze te trzy elementy bywają rozproszone po wielu flagach w docker run. Compose grupuje je w jednym pliku w przewidywalnych sekcjach: services, volumes i networks, a konfigurację aplikacji przekazuje głównie przez environment i env_file.

Sieci: domyślna vs sieci nazwane

Domyślnie:

  • Compose tworzy jedną sieć bridgę per projekt,
  • wszystkie usługi widzą się przez DNS po nazwie usługi,
  • porty wystawia się tylko na hosta, gdy są w polu ports.

Dla prostego stosu backend + db wystarczy domyślna sieć. Różnica zaczyna się, gdy dochodzi więcej komponentów (np. Redis, frontend, worker). Wtedy opłaca się podział na sieci, np.:

services:
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    networks:
      - backend-net
      - db-net
    depends_on:
      - db

  db:
    image: postgres:15
    networks:
      - db-net

  redis:
    image: redis:7
    networks:
      - backend-net

networks:
  backend-net:
  db-net:

Backend jest na dwóch sieciach: rozmawia jednocześnie z db i redis, ale Postgres nie ma powodu widzieć Redisa (i odwrotnie). W porównaniu z jednym wielkim mostem, taki podział lepiej odzwierciedla faktyczne zależności i utrudnia przypadkowe „przestrzelenia się” komponentów.

Wolumeny: dane trwałe vs kod aplikacji

Compose wyróżnia trzy praktyczne typy podpięć:

  • nazwane wolumeny Dockera (np. db-data) – używane głównie na dane baz i cache,
  • bind mounty katalogów z hosta (np. ./backend:/app) – wygodne w developmencie,
  • anonimowe wolumeny (bezpośrednio w volumes usługi, bez zdefiniowania w sekcji głównej) – przydatne, gdy trwałość nie jest priorytetem.

Porównanie na bazie Postgresa:

  • bez wolumenu – każdy restart z docker compose down usuwa dane bazy,
  • z wolumenem nazwanym – dane przetrwają restart kontenera i nawet usunięcie docker compose down, dopóki osobno nie usunie się wolumenu,
  • z bind mountem (np. ./db-data:/var/lib/postgresql/data) – dane leżą w czytelnym katalogu projektu, łatwo je zbackupować, ale trzeba pilnować uprawnień.

Dla środowiska deweloperskiego bazę najczęściej trzyma się w nazwanym wolumenie. Kod aplikacji natomiast wygodniej podpiąć jako bind mount, żeby nie przebudowywać obrazu przy każdej zmianie.

Konfiguracja: environment, env_file i zmienne hosta

Compose wspiera kilka wzorców przekazywania ustawień:

services:
  backend:
    build: ./backend
    environment:
      - LOG_LEVEL=debug
      - DATABASE_URL=${DATABASE_URL}
    env_file:
      - .env.backend

  db:
    image: postgres:15
    env_file:
      - .env.db

Różnice między podejściami:

  • environment z literalami – najprostsze, ale wszystko ląduje w Git, łącznie z hasłami,
  • env_file – konfiguracja wyciągnięta do pliku .env, który można wyłączyć z repo,
  • interpolacja ${VAR} – wartości pobierane z otoczenia (shella / systemu CI).

W praktyce wygodne jest połączenie: konfiguracja sensowna domyślnie w .env.example (commitowana), lokalne sekrety w .env (zignorowane przez Git), a Compose wczytuje je przez env_file. Różnica względem twardo zaszytych haseł w YAML-u jest oczywista – łatwiej zmienić hasło lokalnie bez ingerencji w repozytorium.

Rozszerzenie prostego stosu: migracje bazy i narzędzia pomocnicze

Prosty backend + Postgres szybko rośnie o dodatkowe elementy: narzędzie do migracji (np. Alembic, Prisma, Flyway), panel administracyjny czy worker zadań asynchronicznych. W Compose każde z nich może być osobną usługą, ale korzystać z tego samego obrazu i konfiguracji sieciowej.

services:
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=${DATABASE_URL}
    depends_on:
      - db

  migrate:
    build: ./backend
    command: ["alembic", "upgrade", "head"]
    environment:
      - DATABASE_URL=${DATABASE_URL}
    depends_on:
      - db
    profiles:
      - tools

  db:
    image: postgres:15
    env_file:
      - .env.db
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Usługa migrate nie musi działać cały czas. Można ją odpalić tylko wtedy, gdy potrzeba migracji schematu:

docker compose run --rm migrate

lub przy użyciu profili:

docker compose --profile tools up migrate

Różnica względem ręcznego wchodzenia do kontenera i uruchamiania komendy jest taka, że Compose utrzymuje ten „jednorazowy” kontener w tym samym świecie sieci i konfiguracji co reszta usług. Nie trzeba więc pamiętać o portach, sieciach ani zmiennych środowiskowych.

Profile i warianty stosu: development vs test vs demo

Jednym z częstszych pytań przy Compose jest podział na „warianty” środowiska. Najmniejszy opór stawia osobny plik (np. docker-compose.dev.yml, docker-compose.test.yml), ale nowsza funkcja profili bywa wygodniejsza, gdy różnice są niewielkie.

Porównanie podejść:

  • osobne pliki – pełna niezależność konfiguracji, ale powielanie definicji i większe ryzyko rozjazdów,
  • profile Compose – jedna baza YAML + elementy oznaczone profilem, które włączasz opcjonalnie (--profile),
  • zmienne środowiskowe – jeden plik Compose, ale różne wartości .env zależnie od środowiska.

Przykład użycia profili dla dodatkowego narzędzia developerskiego, np. adminer / pgAdmin:

services:
  backend:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=${DATABASE_URL}
    depends_on:
      - db

  db:
    image: postgres:15
    env_file:
      - .env.db
    volumes:
      - db-data:/var/lib/postgresql/data

  adminer:
    image: adminer
    ports:
      - "8080:8080"
    depends_on:
      - db
    profiles:
      - dev

volumes:
  db-data:

Na codzień możesz odpalać minimalny zestaw:

docker compose up backend db

a gdy chcesz podejrzeć bazę w przeglądarce:

docker compose --profile dev up adminer

Różnica w porównaniu z osobnym plikiem dla Adminera jest taka, że konfiguracja bazy zostaje w jednym miejscu – nie trzeba duplikować definicji Postgresa.

Compose w codziennym developmencie: podstawowy workflow

W praktyce praca z Compose sprowadza się do kilku komend, które zastępują znacznie dłuższe sekwencje przy gołym Dockerze.

  • docker compose up – standardowy start w trybie „zawieszonym” w terminalu z logami na żywo,
  • docker compose up -d – start w tle; terminal pozostaje wolny, logi podglądasz osobno,
  • docker compose down – stop usług i usunięcie kontenerów oraz sieci (wolumeny zostają),
  • docker compose stop – tylko zatrzymanie kontenerów, pozostają w systemie,
  • docker compose restart backend – szybki restart konkretnej usługi po zmianie konfiguracji.

Zmiana kodu z bind mountem nie wymaga up za każdym razem, o ile proces wewnątrz kontenera sam w sobie obsługuje reload (np. npm run dev, uvicorn --reload). Różnica względem „gołych” kontenerów jest głównie w tym, że Compose traktuje zestaw usług jako projekt i zarządza nim jednym poleceniem, a nie pojedynczymi kontenerami.

Debugowanie: porównanie typowych ścieżek diagnozy

Najczęstsze problemy przy użyciu Compose to:

  • backend nie widzi bazy,
  • port nie jest dostępny na hoście,
  • dane bazy znikają po restarcie stosu.
  • Typowe problemy i krótkie ścieżki naprawy

    Każdy z trzech kłopotów z poprzedniej listy da się rozłożyć na kilka prostych kroków diagnostycznych. Różnica względem „gołego” Dockera polega głównie na tym, że dochodzi kontekst projektu (sieci Compose, profile, nazwy usług).

    Backend nie widzi bazy

    Pierwsze rozróżnienie: problem z nazwą hosta, z samym serwerem bazy czy z siecią.

  1. Sprawdzenie nazwy hosta i portu
    W Compose adres bazy to db:5432, gdzie db to service name, a nie localhost. Typowy błąd:

    DATABASE_URL=postgres://user:pass@localhost:5432/mydb

    zamiast:

    DATABASE_URL=postgres://user:pass@db:5432/mydb

    Jeśli backend jest pod backend, a baza pod db, nazwy te są równocześnie DNS-ami w sieci Compose.

  2. Ping z wnętrza kontenera
    Zamiast zgadywać, można sprawdzić połączenie bezpośrednio:

    docker compose exec backend sh -c "apt-get update && apt-get install -y netcat && nc -vz db 5432"

    Jeśli nc melduje Connection refused albo timeout, problem leży w bazie lub sieci, a nie w kodzie backendu.

  3. Stan usługi bazy i kolejność startu

    docker compose ps

    pokaże, czy db w ogóle żyje. Jeśli kontener wychodzi od razu po starcie, logi zwykle wszystko wyjaśniają:

    docker compose logs db

    depends_on w Compose pilnuje kolejności startu, ale nie gwarantuje, że baza jest już gotowa do przyjęcia połączeń. Przy bardziej kapryśnych bazach przydaje się prosty „wait-for-db.sh” lub narzędzie typu wait-for-it w komendzie backendu.

Port nie jest dostępny na hoście

Tutaj zwykle przeplatają się dwie przyczyny: brak mapowania portu w Compose lub konflikt z innym procesem na hoście.

  1. Weryfikacja mapowania w YAML-u
    Usługa musi mieć sekcję ports, np.:

    services:
      backend:
        ports:
          - "8000:8000"

    Pierwsza liczba to port hosta, druga – port wewnątrz kontenera. Jeśli backend nasłuchuje na 5000, a w YAML-u widnieje "8000:8000", nic nie zadziała.

  2. Konflikt z inną aplikacją na hoście

    Zdarza się klasyczne „Already in use”. W takiej sytuacji:

    lsof -i :8000  # Linux/macOS
    netstat -ano | findstr :8000  # Windows

    Jeśli port jest zajęty, można albo zabić proces, albo zmienić pierwszą część mapowania, np. na "8001:8000".

  3. Różnica: port vs sieć Compose
    Debugując, warto rozróżnić dwie płaszczyzny:

    • z hosta – używasz portu opublikowanego, np. localhost:8000,
    • z innej usługi – używasz nazwy usługi i portu wewnętrznego, np. http://backend:8000.

    Jeśli frontend (w innym kontenerze) próbuje łączyć się z http://localhost:8000, a siedzi w tej samej sieci Compose, to strzela do „swojego” localhosta, a nie do hosta. Różnica bywa subtelna, ale efekt jest jeden: ECONNREFUSED.

Dane bazy znikają po restarcie

Najczęściej przyczyną jest brak nazwanych wolumenów albo nieużywany katalog hosta.

  1. Sprawdzenie definicji wolumenu
    W bazowym przykładzie Postgresa:

    services:
      db:
        volumes:
          - db-data:/var/lib/postgresql/data
    
    volumes:
      db-data:

    Jeśli dolna sekcja volumes: jest pominięta, Docker utworzy anonimowy wolumen, o którym łatwo zapomnieć.

  2. Różnica między down a down -v

    docker compose down usuwa kontenery i sieci, ale zostawia wolumeny. Z kolei:

    docker compose down -v

    kasuje również wolumeny nazwane. Po takim poleceniu baza wróci do „fabrycznych” ustawień.

  3. Bind mount vs wolumen nazwany
    Porównując dwa podejścia:

    • wolumen nazwany – dane są w zarządzanej przez Dockera lokalizacji, mniej ingerencji, ale trudniej je odnaleźć ręcznie,
    • bind mount – katalog w projekcie, łatwy podgląd, ale ryzyko przypadkowego skasowania lub nadpisania z hosta.

    W projektach „do zabawy” wolumen nazwany zwykle wystarcza. W długotrwałym devie lub gdy trzeba przerzucać dane między maszynami często wygodniejszy jest bind mount.

Logi, interaktywne shell’e i szybki podgląd stanu

Compose ma kilka komend, które wyraźnie przyspieszają codzienną diagnozę w porównaniu z ręcznym szukaniem ID kontenerów.

  • Podgląd logów wybranej usługi

    docker compose logs backend
    docker compose logs -f backend

    Pierwsza komenda wypisze historię, druga będzie ją śledzić na żywo. Można też użyć wielu nazw usług naraz.

  • Wejście do kontenera „jak na serwer”

    docker compose exec backend sh
    # lub, jeśli w obrazie jest bash
    docker compose exec backend bash

    Różnica w stosunku do docker run -it ... jest taka, że trafiasz do już uruchomionej instancji w aktualnym stanie stosu, z tymi samymi wolumenami i sieciami.

  • Lista usług i ich status

    docker compose ps

    pokazuje coś w rodzaju docker ps, ale ograniczone do konkretnego projektu. Jeśli używasz wielu stosów naraz (np. kilku mikroserwisów), to wygodniejszy widok niż globalna lista wszystkich kontenerów.

Dwóch programistów współpracuje przy laptopach nad projektem Docker
Źródło: Pexels | Autor: Ketut Subiyanto

Compose w codziennym developmencie – praktyczne wzorce pracy

Pod względem workflow Compose zwykle wypiera kombinację: kilka skryptów bash + notatki z parametrami docker run. Poniżej kilka typowych scenariuszy i różnice między podejściami.

Tryb „cały stos działa w tle”

Gdy backend, baza, może jeszcze cache i worker mają działać bez przerwy przez dzień lub dwa:

docker compose up -d

W porównaniu z trybem „na pierwszym planie”:

  • terminal jest wolny,
  • logi podglądasz tylko gdy są potrzebne (docker compose logs -f),
  • restart hosta zwykle nie psuje środowiska: po podniesieniu systemu wywołujesz docker compose up -d i wracasz do pracy.

Alternatywą bywa docker compose start, jeśli kontenery już istnieją. Różnica:

  • up – buduje obrazy (jeśli trzeba), tworzy sieci, wolumeny i kontenery,
  • start – tylko uruchamia wcześniej utworzone kontenery, nie tyka ich konfiguracji.

W praktyce do codziennego użycia wystarcza zwykle up -d + down. start/stop przydają się, gdy masz kilka wariantów tego samego projektu i ręcznie przełączasz się między nimi.

Tryb „jednorazowego” kontenera narzędziowego

Dla narzędzi typu migracje, seed bazy, jednorazowe skrypty administracyjne wygodniejsze bywa docker compose run niż up:

docker compose run --rm migrate
docker compose run --rm backend python manage.py shell

Różnica pomiędzy:

  • docker compose up migrate – usługa staje się elementem stosu, jest zarządzana jak pozostałe, jej kontener trwa, dopóki nie wywołasz down,
  • docker compose run --rm migrate – tworzony jest jednorazowy kontener, który po zakończeniu pracy jest usuwany.

W przypadku zadań okresowych lub manualnie odpalanych (migracje przy deployu, generowanie raportów) run --rm jest zwykle czytelniejsze i nie zaśmieca listy kontenerów.

Praca z kodem: przebudowywanie obrazu vs hot-reload

Dwa dominujące style pracy z Compose to:

  1. „produkcyjny” – kod jest w obrazie, każda zmiana wymaga przebudowy,
  2. „deweloperski” – kod jest montowany z hosta jako bind mount, backend sam przeładowuje się przy zmianach plików.

Porównanie na przykładzie Node.js:

services:
  backend:
    build: ./backend
    volumes:
      - ./backend:/app
    command: ["npm", "run", "dev"]

Frontend w trybie dev czy backend z nodemon reagują na zmiany w katalogu ./backend. Jeśli ten sam projekt budujesz do produkcji, zwykle używasz osobnego pliku Compose lub profilu:

services:
  backend:
    build: ./backend
    command: ["npm", "start"]
    # brak volumes, kod jest w obrazie
    profiles:
      - prod

Podejście „dwa profile, jedna definicja usługi” często upraszcza życie w porównaniu z utrzymywaniem dwóch zupełnie niezależnych plików Compose. Różnica jest głównie taka, że trudniej o rozjazd wersji obrazu czy konfiguracji sieci.

Compose a testy automatyczne

Dla testów integracyjnych Compose bywa wygodniejszy niż ręcznie odpalana baza lub osobny skrypt. Można potraktować go jak jednorazowe środowisko testowe, które powstaje na początku pipeline’u i znika na końcu.

Dwie typowe strategie:

  • Start pełnego stosu, testy z zewnątrz

    docker compose -f docker-compose.test.yml up -d
    pytest tests/integration
    docker compose -f docker-compose.test.yml down -v

    Testy odpalane są na hoście (lub w runnerze CI), a Compose tylko zapewnia infrastrukturę (bazy, kolejki, API).

  • Osobny kontener testowy w tym samym projekcie

    services:
      tests:
        build: ./backend
        command: ["pytest", "tests/integration"]
        depends_on:
          - db
          - backend
        profiles:
          - test

    W pipeline:

    docker compose --profile test up --exit-code-from tests --abort-on-container-exit

    Po zakończeniu testów cały stos można skasować jedną komendą down -v. W porównaniu z ręcznym pilnowaniem kolejności odpalania usług i czyszczenia baz to dość znaczące uproszczenie.

Docker Compose a inne opcje orkiestracji i automatyzacji

Compose rozwiązuje problem wielokontenorowego środowiska lokalnie i częściowo w prostych deployach. Gdy projekt rośnie, pojawia się wybór: zostać przy Compose, dopisać własne skrypty, czy przeskoczyć na coś w rodzaju Kubernetesa. Różnice nie są tylko technologiczne – dotyczą też kosztu utrzymania i bariery wejścia.

Compose vs pojedyncze kontenery Docker

Przy jednym kontenerze (np. tylko Postgres) Compose nie wnosi wiele – poza zapamiętaniem konfiguracji w YAML-u. Porównanie dwóch podejść dla prostej bazy:

# goły docker
docker run --name my-postgres -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgres:15

# docker compose
services:
  db:
    image: postgres:15
    environment:
      - POSTGRES_PASSWORD=secret
    ports:
      - "5432:5432"

Dla pojedynczej usługi:

  • Docker – szybciej, mniej plików, konfiguracja w historii powłoki,
  • Compose – powtarzalność, łatwa wersjonowalność, jedno polecenie do startu/stopu.

Granica opłacalności często przebiega przy drugim/trzecim kontenerze: gdy dochodzi baza i cache, Compose zaczyna wygrywać klarownością. Zamiast trzech długich komend docker run i notatek z portami powstaje jeden YAML, który można uruchomić z nowym członkiem zespołu w kilkanaście sekund.

Compose vs skrypty bash / Makefile

Inne popularne podejście to zestaw skryptów (np. scripts/start_dev.sh) lub Makefile, który woła Dockera bezpośrednio. Oba podejścia można też łączyć z Compose.

Porównanie funkcji:

Najczęściej zadawane pytania (FAQ)

Czym Docker Compose różni się od zwykłego Dockera (docker run)?

Docker to „silnik” kontenerów, a docker run to pojedyncze polecenie uruchomienia jednego kontenera. Docker Compose dodaje warstwę deklaratywną: cały stos aplikacji (kilka kontenerów, sieci, wolumeny) opisujesz w jednym pliku docker-compose.yml i uruchamiasz jednym poleceniem docker compose up.

Przy jednym kontenerze docker run bywa szybszy, natomiast przy kilku usługach (app, baza, cache, worker) ręczne komendy robią się długie, trudne do odtworzenia i podatne na literówki. Compose porządkuje to w jednym, wersjonowanym pliku, który można przeglądać w code review i łatwo odtworzyć na innym komputerze.

Kiedy używać Docker Compose, a kiedy wystarczy pojedynczy kontener?

Pojedynczy kontener z docker run wystarczy, gdy uruchamiasz prosty, samodzielny serwis: np. testujesz Redis, Nginx, jednorazowy kontener alpine do narzędzi albo monolityczną aplikację serwującą statyczne pliki bez bazy danych.

Compose ma przewagę, gdy tylko pojawia się zależność: baza danych, broker wiadomości (RabbitMQ, Kafka), osobny frontend czy worker do zadań w tle. Nawet układ app + db uzasadnia przejście na Compose, bo wtedy dochodzi konfiguracja sieci, wolumenów, zmiennych środowiskowych i powiązań depends_on, które w jednym pliku są dużo czytelniejsze niż seria poleceń docker run.

Czy Docker Compose nadaje się na produkcję, czy potrzebny jest Kubernetes?

Docker Compose dobrze sprawdza się na jednym hoście: lokalnie u developera, na serwerze testowym, w środowiskach QA/UAT albo w małej produkcji typu demo dla klienta. Umożliwia proste skalowanie usług na jednym serwerze (np. docker compose up --scale app=3), ale nie zarządza klastrem wielu maszyn.

Jeżeli potrzebujesz automatycznego przenoszenia usług między serwerami po awarii, zaawansowanego autoskalowania, service discovery i routingowania ruchu w skali kilku–kilkunastu węzłów, wchodzą w grę platformy orkiestracyjne jak Kubernetes (lub w prostszych przypadkach Docker Swarm). Typowy układ to Compose na lokalnym devie, a Kubernetes na produkcji.

Do czego Docker Compose jest najczęściej używany w praktyce?

Najpopularniejsze scenariusze to:

  • lokalne środowisko developerskie – każdy developer odpala identyczny stos komendą docker compose up, bez instalowania baz czy MQ na swoim systemie,
  • testy integracyjne i e2e w CI – pipeline przed testami podnosi cały stos z Compose, uruchamia testy, a po wszystkim go wyłącza,
  • demo lub „mini-produkcja” na jednym serwerze – szybkie postawienie kompletnego środowiska dla klienta czy zespołu QA.

W tych zastosowaniach Compose jest znacznie prostszy i szybszy w konfiguracji niż pełny Kubernetes, a jednocześnie dużo bardziej uporządkowany niż ręcznie uruchamiane pojedyncze kontenery.

Jak wygląda podstawowy plik docker-compose.yml i co musi zawierać?

W aktualnym Docker Compose (V2 jako plugin CLI) nie trzeba podawać pola version. Minimalny szkielet to sekcja services z definicjami usług oraz opcjonalne sekcje volumes i networks:

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=app

Sekcja services określa, z jakich obrazów lub buildów mają powstać kontenery, jakie porty i wolumeny podpiąć oraz jakie zmienne środowiskowe ustawić. volumes i networks pozwalają nazwać i parametryzować dane oraz sieci, ale jeśli ich nie zdefiniujesz, Compose i tak tworzy domyślną sieć projektu.

Jak w Docker Compose działają sieci i komunikacja między usługami?

Docker domyślnie tworzy sieć typu bridge. Przy ręcznym docker run kontenery komunikują się po adresach IP, a porty trzeba mapować ręcznie (-p 8080:80). Przy kilku usługach szybko robi się z tego mało czytelny zestaw mapowań.

Compose grupuje usługi w jednej sieci i nadaje im nazwy hostów odpowiadające nazwom usług. Backend może połączyć się z bazą po adresie db:5432, a nie po zmieniającym się IP. Mapowanie portów na hosta zapisujesz w jednym miejscu w polu ports, więc łatwo zobaczyć, co jest wystawione na zewnątrz, a co działa wyłącznie wewnątrz sieci Compose.

Jak diagnozować błędy w Docker Compose – od czego zacząć?

Większość problemów w Compose sprowadza się do klasycznych kłopotów z Dockerem: kontener szybko się wyłącza, port jest źle zmapowany, brakuje wolumenu lub jest konflikt nazw. Najszybciej diagnozuje się to, traktując usługi z Compose jak zwykłe kontenery:

  • docker ps – sprawdź, czy kontener danej usługi faktycznie działa,
  • docker logs NAZWA_USLUGI-1 – przejrzyj logi startu i ewentualne stack trace,
  • docker inspect – zobacz szczegóły konfiguracji sieci, portów i wolumenów.

Różnica jest taka, że Compose opakowuje wiele operacji w jedno polecenie docker compose up, ale pod spodem to nadal typowe kontenery Dockera. Znajomość tych kilku „gołych” komend bardzo ułatwia znalezienie źródła błędu.

Najważniejsze punkty

  • Docker Compose rozwiązuje problem rosnącej złożoności przy wielu usługach – zamiast serii długich, podatnych na pomyłki komend docker run masz jeden deklaratywny plik docker-compose.yml i jedno polecenie docker compose up.
  • Plik docker-compose.yml jest wersjonowany razem z kodem, dzięki czemu konfiguracja środowiska staje się powtarzalna, recenzowalna w code review i łatwa do porównania między gałęziami – podobnie jak migracje SQL zastępują ręczne klepanie zapytań.
  • Pojedynczy kontener i goły docker run wystarczą przy prostych scenariuszach (np. sam Redis, Nginx, monolit bez zależności), natomiast już układ app + baza danych uzasadnia użycie Compose, bo dochodzą kwestie sieci, wolumenów, zmiennych środowiskowych i zależności startu usług.
  • Największy zysk z Compose widać w środowiskach dev, test, demo i małej produkcji: jednym poleceniem stawiasz kompletny stos dla dewelopera, pipeline CI czy środowiska QA, bez ręcznej instalacji baz, brokerów wiadomości czy runtime’ów na hoście.
  • Compose uzupełnia, a nie zastępuje Dockera: opisuje, jak z obrazów tworzyć kontenery, jak podłączać wolumeny i sieci, oraz agreguje typowe operacje (build, run, mapowanie portów, tworzenie sieci) w jedno spójne polecenie, ułatwiając zrozumienie i diagnozowanie całego stosu.