Docker Compose – zarządzanie wieloma kontenerami

You are currently viewing Docker Compose – zarządzanie wieloma kontenerami

Wstęp

Jeśli pracujesz z Dockerem, na pewno już doceniasz, jak szybko można odpalić kontener z PostgreSQL, Nginx czy własną apką. Wystarczy jedna komenda i wszystko działa – bez instalacji, bez konfiguracji, bez walki z zależnościami. Ale życie programisty rzadko bywa tak proste.

Wyobraź sobie typowy projekt: backend w Spring Boot nasłuchuje na porcie 8080, Angular serwuje frontend na porcie 4200, PostgreSQL kręci się na 5432, a Redis cache’uje dane na porcie 6379. Do tego jeszcze jakiś Nginx jako reverse proxy. Uruchamianie tego wszystkiego ręcznie to prawdziwy koszmar – musisz pamiętać o kolejności (najpierw baza, potem backend, na końcu frontend), skonfigurować sieć między kontenerami, ustawić zmienne środowiskowe, podpiąć wolumeny… I mieć nadzieje, żeby nic się nie wywaliło.

Do tego wszystkiego przychodzi nowy kolega z zespołu i pyta: „Jak odpalić ten projekt lokalnie?”. Bez dedykowanych narzędzi musisz przekazać mu instrukcję z dwudziestoma komendami docker run z różnymi flagami. No nie brzmi jak świetne rozwiązanie. Na szczęście mamy Docker Compose – narzędzie, które pozwala temu zapobiec.

To rozwiązanie, które idealnie sprawdza się w projektach opartych o wiele mikrousług. Zamiast żonglować dziesiątkami komend, pamiętać kolejność uruchamiania poszczególnych elementów i ich konfigurację, po prostu opisujesz całość w jednym pliku docker-compose.yml. Od tego momentu jedno polecenie docker compose up stawia cały system na nogi. Chcesz go zatrzymać? docker compose down. Potrzebujesz przebudować obrazy? docker compose up --build. Łatwe w użyciu, błyskawiczne w działaniu i eliminujące ryzyko, że coś pominiesz.

W tym wpisie pokażę Ci, jak Docker Compose może zmienić Twoje podejście do zarządzania wielokontenerowymi aplikacjami. Przejdziemy od podstaw – czym to w ogóle jest i dlaczego warto się tym interesować – przez praktyczny przykład z Spring Boot i PostgreSQL, aż po zaawansowane techniki i porównanie z Kubernetesem. Na koniec będziesz wiedział, kiedy Docker Compose wystarczy, a kiedy czas pomyśleć o czymś poważniejszym.

Czym jest Docker Compose i kiedy warto go używać?

Docker Compose to narzędzie do definiowania i uruchamiania aplikacji składających się z wielu kontenerów. Całą konfigurację zapisujesz w pliku YAML (najczęściej nazwanym docker-compose.yml), który opisuje serwisy, sieci, wolumeny – wszystko, czego potrzebuje Twoja aplikacja do działania. Jednym poleceniem możesz uruchomić całe środowisko, niezależnie czy to lokalne dev, test czy staging.

Wyobraź sobie typową aplikację webową. Masz backend w Spring Boot, frontend w Angularze, bazę PostgreSQL i Redis do cache’owania. Bez Docker Compose musiałbyś pamiętać o uruchomieniu każdego kontenera osobno, z odpowiednimi parametrami:

docker run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=secret -v postgres_data:/var/lib/postgresql/data postgres:16
docker run -d --name redis -p 6379:6379 redis:alpine
docker run -d --name spring-backend -p 8080:8080 my-spring-app:latest
docker run -d --name angular-frontend -p 4200:4200 my-angular-app:latest

Teraz wyobraź sobie, że każdy z tych kontenerów ma jeszcze zmienne środowiskowe, specyficzne konfiguracje sieci, health checki… Szybko robi się z tego bałagan, prawda?

Z Docker Compose cały ten setup zapisujesz raz w pliku YAML i uruchamiasz jednym poleceniem: docker compose up. Docker Compose czyta konfigurację, pobiera potrzebne obrazy, buduje je jeśli trzeba, tworzy sieci i wolumeny, a na koniec uruchamia wszystko w odpowiedniej kolejności. To świetne uproszczenie, gdzie wszystko jest uporządkowane.

To samo zapisane z użyciem Docker Compose:

version: '3.9'

services:
  postgres:
    image: postgres:16
    container_name: postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:alpine
    container_name: redis
    ports:
      - "6379:6379"
    restart: unless-stopped

  spring-backend:
    image: my-spring-app:latest
    container_name: spring-backend
    ports:
      - "8080:8080"
    depends_on:
      - postgres
      - redis
    restart: unless-stopped

  angular-frontend:
    image: my-angular-app:latest
    container_name: angular-frontend
    ports:
      - "4200:4200"
    restart: unless-stopped

volumes:
  postgres_data:

Dzięki temu mamy jeden plik, który może być używany przez wiele osób.

Kiedy warto go używać?

Docker Compose jest niezwykle wszechstronny, ale jego siła najmocniej objawia się w kilku konkretnych scenariuszach:

  • Środowiska deweloperskie – To najczęstsze zastosowanie. Zamiast zmuszać każdego nowego programistę w zespole do ręcznej instalacji i konfiguracji bazy danych, brokera wiadomości i innych zależności, wystarczy, że dostarczysz mu plik docker-compose.yml. Jedna komenda i całe środowisko potrzebne do pracy nad aplikacją jest gotowe w kilka minut. To gigantyczna oszczędność czasu i gwarancja, że każdy pracuje na identycznym, spójnym środowisku, co eliminuje słynny problem „ale u mnie działa!”.
  • Zautomatyzowane testy – W procesach CI/CD (Continuous Integration/Continuous Deployment) często potrzebujemy uruchomić pełne środowisko aplikacji do przeprowadzenia testów end-to-end. Docker Compose idealnie się do tego nadaje. Przed uruchomieniem testów, skrypt CI może wykonać docker compose up, aby postawić cały system, a po zakończeniu testów – docker compose down, aby posprzątać i zwolnić zasoby. Wszystko jest w pełni zautomatyzowane i odizolowane.
  • Wdrożenia na pojedynczym hoście – Chociaż do zaawansowanych wdrożeń produkcyjnych częściej używa się narzędzi takich jak Kubernetes, Docker Compose jest świetnym rozwiązaniem dla mniejszych aplikacji lub środowisk, które działają na jednej maszynie (serwerze). Jeśli Twoja aplikacja nie wymaga jeszcze skomplikowanej orkiestracji, wysokiej dostępności i automatycznego skalowania, Compose może być wystarczający i znacznie prostszy w obsłudze.

Instalacja

Jeśli masz już zainstalowanego Dockera, to instalacja Docker Compose jest bardzo prosta (jeśli nie masz jednak jeszcze zainstalowanego Dockera lokalnie to tutaj opisuje jak to zrobić).

Docker Desktop (Windows, macOS, Linux) – Najłatwiejszym i zalecanym sposobem jest instalacja Docker Desktop. Ten pakiet zawiera nie tylko silnik Dockera i klienta wiersza poleceń (CLI), ale również wbudowany plugin Docker Compose. Jeśli masz Docker Desktop, prawdopodobnie masz już wszystko, czego potrzebujesz. Aby to sprawdzić, otwórz terminal i wpisz: docker compose version. Jeśli zobaczysz numer wersji, jesteś gotowy do pracy.

Linux (jako plugin) – Jeśli na Linuksie zainstalowałeś silnik Dockera ręcznie (bez Docker Desktop), możesz doinstalować Compose jako plugin. Najlepiej zrobić to, korzystając z oficjalnego repozytorium Dockera dla swojej dystrybucji. Przykładowo, na systemach bazujących na Debianie/Ubuntu, po skonfigurowaniu repozytorium, wystarczy wykonać:

sudo apt-get update
sudo apt-get install docker-compose-plugin

Po instalacji, tak jak w przypadku Docker Desktop, możesz zweryfikować poprawność instalacji komendą docker compose version.

Anatomia pliku docker-compose.yml

Plik docker-compose.yml to miejsce, gdzie opisujesz całą swoją aplikację. To zwykły plik tekstowy w formacie YAML, ale zamiast kombinować z kilkunastoma komendami docker run, wszystko masz w jednym miejscu.

Żeby pokazać jak to działa, weźmy prosty przykład: aplikacja z Nginx jako frontend i nasza własna aplikacja backendowa. Nic skomplikowanego, ale wystarczające żeby zrozumieć mechanizm.

# Wersja specyfikacji Docker Compose, której używamy. Zalecane jest używanie nowszych wersji jak 3.8 czy 3.9
version: "3.9"

# Główna sekcja, w której definiujemy wszystkie nasze kontenery (serwisy)
services:
  # Nazwa pierwszego serwisu - w tym przypadku nasz backend
  backend:
    # Określa, jak zbudować obraz dla tego serwisu. Kropka oznacza,
    # że Dockerfile znajduje się w tym samym katalogu co plik docker-compose.yml
    build: .
    # Mapowanie portów. Przekierowuje port 8080 na hoście do portu 8080 w kontenerze.
    ports:
      - "8080:8080"
    # Montowanie wolumenu typu 'bind mount' do synchronizacji kodu.
    # Zmiany w kodzie na hoście będą natychmiast widoczne w kontenerze,
    # co jest idealne dla środowiska deweloperskiego.
    volumes:
      - .:/app
    # Definiuje sieć, do której podłączony będzie ten serwis
    networks:
      - app-network

  # Nazwa drugiego serwisu - serwer Nginx
  frontend:
    # Używa gotowego, oficjalnego obrazu Nginx w wersji 1.21
    image: nginx:1.21
    # Mapuje port 80 na hoście do portu 80 w kontenerze
    ports:
      - "80:80"
    # Montuje plik konfiguracyjny Nginx z hosta do kontenera,
    # aby nadpisać domyślną konfigurację.
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    # Definiuje, że ten serwis zależy od serwisu 'backend'.
    # Docker Compose uruchomi backend, zanim uruchomi frontend.
    depends_on:
      - backend
    # Podłącza serwis do tej samej sieci
    networks:
      - app-network

# Sekcja, w której definiujemy sieci
networks:
  # Nazwa naszej niestandardowej sieci
  app-network:
    # Określa sterownik sieci. 'bridge' to standardowy wybór dla pojedynczego hosta.
    driver: bridge

# Sekcja do definiowania nazwanych wolumenów (w tym przykładzie nieużywana,
# ale warto o niej pamiętać przy przechowywaniu danych np. bazy danych)
volumes:
  app-data:

Analiza struktury pliku

Zacznijmy analizowanie pliku docker-compose.yml od najwyższego poziomu. Możemy wyróżnić kilka podstawowych elementów:

  • version – Określa wersję składni pliku Docker Compose, której używamy. W najnowszych wersjach Docker Compose tego już nie musisz podawać – wybór należy do Ciebie. Osobiście ja zawsze umieszczam, ale bardzo często jest to zależne od praktyk w zespole/firmie.
  • services – To najważniejsza sekcja, w której definiujemy wszystkie kontenery tworzące naszą aplikację. Każdy klucz w tej sekcji (np. backend, frontend) odpowiada jednemu serwisowi. Nazwa serwisu jest jednocześnie jego nazwą hosta wewnątrz sieci Dockera, co umożliwia łatwą komunikację między kontenerami.
  • networks – Docker Compose automatycznie tworzy sieć dla Naszych serwisów. Natomiast jeśli byśmy chcieli zdefiniować niestandardowe sieci wirtualne to możemy zrobić to w tej sekcji (np. jawnie chcemy stworzyć sieć app-network).
    Jeśli chciałbyś dowiedzieć się więcej na ten temat to przygotowałem cały osobny wpis o sieciach w Dockerze.
  • volumes – Tutaj deklarujemy nazwane wolumeny. Są to zarządzane przez Dockera obszary na dysku hosta, przeznaczone do trwałego przechowywania danych (np. dla bazy danych). Zdefiniowanie wolumenu w tym miejscu pozwala na jego wielokrotne użycie w różnych serwisach i ułatwia zarządzanie danymi niezależnie od cyklu życia kontenerów.
    Jeśli chciał byś się więcej dowiedzieć na ten temat to przygotowałem cały osobny wpis o wolumenach w Dockerze.

Analiza definicji serwisu

Przyjrzymy się teraz najważniejszej części pliku services, gdzie definiujemy nasze kontenery (np. backend, frontend):

  • image – Określa obraz, z którego ma być stworzony kontener. Może to być obraz z Docker Huba (np. postgres:16-alpine) lub z prywatnego repozytorium.
  • build – Zamiast używać gotowego obrazu, możemy go zbudować na podstawie Dockerfile. Wartość tej instrukcji to ścieżka do katalogu zawierającego Dockerfile. W praktyce używane głównie w developmencie, gdy na bieżąco wprowadzamy zmiany w kodzie aplikacji.
  • ports – Mapuje porty między maszyną hosta a kontenerem w formacie "HOST:KONTENER". Umożliwia dostęp do usług działających w kontenerach z zewnątrz (np. z naszej przeglądarki).
  • environment – Pozwala na ustawienie zmiennych środowiskowych wewnątrz kontenera. To najczęściej używany sposób na przekazywanie konfiguracji, np. haseł do bazy danych, adresów URL innych serwisów czy kluczy API.
  • volumes – Służy do montowania wolumenów lub ścieżek z hosta (bind mounts) do kontenera. Kluczowe dla trwałego przechowywania danych (np. danych bazy danych) lub do udostępniania plików konfiguracyjnych.
  • networks – Określa, do których sieci ma być podłączony kontener. Domyślnie Docker Compose tworzy jedną sieć dla wszystkich serwisów w pliku, ale tworzenie własnych, nazwanych sieci jest dobrą praktyką.
  • depends_on – Definiuje zależności między serwisami. Jeśli serwis A zależy od B, Compose uruchomi kontener B przed kontenerem A. Ważne: depends_on czeka tylko na uruchomienie kontenera, a nie na to, aż aplikacja wewnątrz niego będzie w pełni gotowa do przyjmowania połączeń (np. aż baza danych zainicjalizuje się). Do tego służą bardziej zaawansowane mechanizmy, jak healthcheck.
  • container_name – Domyślnie Compose nadaje kontenerom nazwy w formacie nazwa_projektu-nazwa_serwisu-numer. Ta opcja pozwala nadać kontenerowi stałą, konkretną nazwę.

Z mojego doświadczenia ten zestaw instrukcji pozwala na zdefiniowanie praktycznie każdej, nawet bardzo złożonej, aplikacji wielokontenerowej.

Przykład: Aplikacja Spring Boot z bazą PostgreSQL

Teoria jest ważna, ale nic tak nie uczy, jak praktyka. Zobaczmy, jak użyć Docker Compose do uruchomienia typowej aplikacji backendowej – serwisu napisanego w Spring Boot, który komunikuje się z bazą danych PostgreSQL.

Nasz przykład zakłada poniższą strukturę katalogów:

spring-boot-app/
├── docker-compose.yml
├── Dockerfile
├── init-db/
│ ├── 01-create-tables.sql
│ └── 02-insert-data.sql
├── src/
└── … pozostałe pliki aplikacji Spring Boot

Krok 1: Aplikacja Spring Boot

Zakładamy, że mamy aplikację Spring Boot z zależnościami do Spring Data JPA, Spring Web i sterownika PostgreSQL (do utworzenia aplikacji Spring Boot można skorzystać z Spring initializr). Naszym celem będzie stworzenie prostego API, które pobiera listę produktów z bazy danych.

Na początek potrzebujemy encji, która będzie mapowana na tabelę w bazie danych. Stwórzmy prostą klasę Product oraz prosty interfejs repozytorium, który umożliwi nam wykonywanie operacji CRUD na encji Product..

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    private BigDecimal price;

    // Konstruktory, gettery i settery
}

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    // Metoda do wyszukiwania produktu po nazwie
    Optional<Product> findByName(String name);
}

Teraz tworzymy kontroler, który wystawi endpoint /api/products do pobierania wszystkich produktów.

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductRepository productRepository;

    public ProductController(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @GetMapping
    public ProductDto getProducts(@RequestParam String name) {
         Product product = productRepository.findByName(name)
                .orElseThrow(() -> new EntityNotFoundException("Entity not found for name: " + name));
         return new ProductDto(
            product.getId(),
            product.getName(),
            product.getDescription(),
            product.getPrice()
        );
    }

    public record ProductDto(
       long id, 
       String name, 
       String description, 
       BigDecimal price
     ) {}
}

A na koniec jeszcze przygotujmy sobie plik application.yml:

spring:
  # Konfiguracja źródła danych
  datasource:
    # Używamy nazwy serwisu 'db' jako hosta bazy danych. Docker Compose zapewni,
    # że ta nazwa zostanie przetłumaczona na odpowiedni adres IP wewnątrz sieci kontenerów.
    url: jdbc:postgresql://db:5432/mydatabase
    # Użytkownik i hasło, które ustawimy w zmiennych środowiskowych dla kontenera PostgreSQL.
    username: user
    password: password
  
  # Konfiguracja JPA i Hibernate
  jpa:
    show-sql: true # Pokazuje generowane zapytania SQL w logach

Krok 2: Dockerfile dla aplikacji Spring Boot

Aby skonteneryzować naszą aplikację, potrzebujemy prostego Dockerfile. Zakładając, że budujemy naszą aplikację za pomocą Mavena do pliku .jar, Dockerfile może wyglądać tak:

# Używamy oficjalnego obrazu OpenJDK 21 jako bazy
FROM openjdk:21-slim

# Ustawiamy katalog roboczy w kontenerze
WORKDIR /app

# Kopiujemy spakowaną aplikację (plik .jar) do katalogu roboczego
# ARG wskazuje na plik JAR, który zostanie zbudowany przez Mavena
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar

# Komenda, która zostanie wykonana podczas uruchamiania kontenera
ENTRYPOINT ["java", "-jar", "app.jar"]

Krok 3: Inicjalizacja bazy danych

Do pełnego działania aplikacji przydałoby się mieć przygotowaną jakąś tabelę z danymi. Załóżmy, że nie chcemy mieć tego robionego przez Spring Boota (natomiast jeśli byś wolał jednak mieć wszystkie w Spring Boot to można skorzystać z biblioteki Liquibase lub MyBatis), tylko chcielibyśmy dostarczyć to z zewnątrz. Przygotujmy 2 pliki init-db/01-create-tables.sql oraz init-db/02-insert-data.sql:

# Przykład pliku init-db/01-create-tables.sql:
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    description TEXT
);

# Przykład pliku init-db/02-insert-data.sql
INSERT INTO products (name, price, description) VALUES 
('Laptop', 2999.99, 'Powerful laptop for developers'),
('Mouse', 29.99, 'Wireless optical mouse'),
('Keyboard', 149.99, 'Mechanical gaming keyboard');

Krok 4: Plik docker-compose.yml

Teraz czas na główną część. Przygotowujemy plik docker-compose.yml, który będzie spinał naszą aplikację wraz z bazą PostgreSQL:

version: "3.9"

services:
  # Serwis naszej aplikacji Spring Boot
  app:
    # Buduje obraz na podstawie Dockerfile w bieżącym katalogu
    build: .
    # Nadaje kontenerowi przyjazną nazwę
    container_name: spring-boot-app
    # Mapuje port 8080 na hoście do 8080 w kontenerze,
    # abyśmy mogli wysyłać żądania do naszej aplikacji
    ports:
      - "8080:8080"
    # Definiuje, że nasza aplikacja zależy od bazy danych.
    # 'db' zostanie uruchomione przed 'app'.
    depends_on:
      - db
    # Podłącza do naszej sieci
    networks:
      - my-network

  # Serwis bazy danych PostgreSQL
  db:
    # Używa oficjalnego obrazu PostgreSQL w wersji 16
    image: postgres:16
    # Nadaje kontenerowi nazwę
    container_name: postgres-db
    # Zmienne środowiskowe wymagane przez obraz PostgreSQL do inicjalizacji bazy danych.
    # Muszą być zgodne z tym, co mamy w application.yml!
    environment:
      - POSTGRES_DB=mydatabase
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    # Montuje nazwany wolumen, aby dane bazy danych były trwałe
    # i przetrwały restart kontenera.
    volumes:
      - postgres-data:/var/lib/postgresql/data
      # Inicjalizacja bazy danych - pliki SQL wykonają się przy pierwszym uruchomieniu
      - ./init-db:/docker-entrypoint-initdb.d
    # Podłącza do tej samej sieci
    networks:
      - my-network
    # A co z portami?
    # W developmencie możemy wystawiać port bazy danych:
    # ports:
    #   - "5432:5432"
    # Ułatwia to debugowanie, sprawdzanie danych
    # czy podłączenie się przez DBeaver/pgAdmin. W praktyce zawsze się przydaje.
    # W testach CI/CD też się przyda - gdy coś nie działa.
    # Tylko w produkcji warto ukrywać porty baz danych przed światem zewnętrznym.
   
# Definicja sieci
networks:
  my-network:
    driver: bridge

# Definicja wolumenu
volumes:
  # Nazwany wolumen, którym Docker będzie zarządzał
  postgres-data:

Krok 5: Uruchomienie całości

Teraz wystarczy, że w terminalu, w głównym katalogu projektu, wykonamy dwie komendy:

Najpierw zbudujemy aplikacje Spring Boot do pliku .jar:

./mvnw clean package

A następnie wszystko uruchomimy przy użyciu Docker Compose:

docker compose up

Natomiast jeśli chcemy uruchomić kontenery w tle, wystarczy, że dodamy flagę -d:

docker compose up -d

I to wszystko! Docker Compose zbuduje obraz Twojej aplikacji, uruchomi kontener z bazą danych (w tym zaczytają się przygotowane wcześniej pliki SQL), a następnie kontener z aplikacją, która automatycznie połączy się z bazą. Całe środowisko jest gotowe do pracy. Teraz jeśli uderzymy na endpoint curl http://localhost:8080/api/products?name=Laptop powinniśmy dostać szczegóły produktu.

Bonus: Integracja ze Spring Boot

W przykładzie pokazywałem jak można skonfigurować Docker Compose ze Spring Bootem (oczywiście wiedze też można wykorzystać w budowaniu innych serwisów) bez dodatkowych zależności. Natomiast jeśli pracujesz z Spring Boot w wersji 3.1 lub nowszej, masz do dyspozycji znacznie prostsze rozwiązanie. Spring Boot oferuje wbudowane wsparcie dla Docker Compose, które jeszcze bardziej upraszcza pracę na lokalnym środowisku deweloperskim.

Zamiast ręcznie uruchamiać kontenery komendą docker compose up przed startem aplikacji, możesz dodać do projektu jedną zależność. Dzięki niej Spring Boot podczas uruchamiania sam automatycznie znajdzie plik docker-compose.yml i uruchomi zdefiniowane w nim kontenery. Gdy zatrzymasz aplikację, Spring Boot zatrzyma również kontenery.

Jak to włączyć?

Wystarczy, że dodasz do swojego pliku pom.xml (dla Mavena) następującą zależność:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-docker-compose</artifactId>
    <scope>runtime</scope>
</dependency>

Lub jeśli pracujesz z Gradle to będzie to wyglądać tak:

dependencies {
    developmentOnly("org.springframework.boot:spring-boot-docker-compose")
}

I to wszystko! Upewnij się tylko, że plik docker-compose.yml znajduje się w głównym katalogu Twojego projektu. Teraz przy każdym uruchomieniu aplikacji Spring Bootowej, Twoja baza danych i inne serwisy wstaną razem z nią.

Więcej na temat integracji z Docker Compose możesz przeczytać w oficjalnej dokumentacji Springa!

Niezbędnik Docker Compose – najważniejsze komendy

W tym punkcie spisałem kilkanaście najważniejszych komend związanych z Docker Compose.

Uruchamianie i zatrzymywanie

  • docker compose up – Tworzy i uruchamia wszystkie serwisy zdefiniowane w pliku docker-compose.yml. Jeśli obrazy nie istnieją lokalnie, zostaną pobrane lub zbudowane.
    • docker compose up -d – Uruchamia wszystkie serwisy w tle (detached mode)
    • docker compose up --build – Uruchamia z wymuszonym przebudowaniem obrazów
    • docker compose up database – Uruchamia tylko konkretny serwis
  • docker compose down – Zatrzymuje i usuwa kontenery oraz sieci zdefiniowane w pliku Compose.
    • docker compose down -v – Usuwa również wolumeny (UWAGA: stracisz dane!)
    • docker compose down --rmi all – Usuwa również obrazy
  • docker compose restart app – Restartuje konkretny serwis

Monitorowanie i debugowanie

  • docker compose ps – Wyświetla status kontenerów zarządzanych przez Compose w bieżącym projekcie docker compose logs – Wyświetla logi ze wszystkich lub wybranych serwisów
    • docker compose logs -f – Śledzi logi na żywo (podobnie jak tail -f)
    • docker compose logs app – Pokazuje logi tylko dla konkretnego serwisu
    • docker compose logs -f app – Logi konkretnego serwisu na żywo
  • docker compose exec <nazwa_serwisu> <polecenie> – Wykonuje polecenie wewnątrz już działającego kontenera. Idealne do debugowania.
    • docker compose exec app bash – Wejście do powłoki kontenera
    • docker compose exec db psql -U user -d mydatabase – Wejście do bazy danych
    • docker compose exec app env – Sprawdzenie zmiennych środowiskowych

Zarządzanie obrazami i kontenerami

  • docker compose build – Buduje (lub przebudowuje) obrazy dla wszystkich serwisów
    • docker compose build app – Buduje obraz tylko dla konkretnego serwisu
  • docker compose pull – Pobiera najnowsze wersje obrazów z repozytorium
    • docker compose pull app – Pobiera obraz dla konkretnego serwisu
  • docker compose run <nazwa_serwisu> <polecenie> – Uruchamia jednorazowy kontener dla danego serwisu i wykonuje w nim określone polecenie. Bardzo przydatne do zadań takich jak migracje bazy danych.
    • docker compose run app python manage.py migrate – Uruchomienie migracji
    • docker compose run app bash – Uruchomienie nowego kontenera z bash

Zaawansowane opcje

  • docker compose config – Sprawdza i wyświetla konfigurację (czy YAML jest poprawny)
  • docker compose up --scale app=3 – Uruchamia 3 instancje serwisu app (ale pamiętaj o konfliktach portów!)
  • docker network ls – Wyświetla sieci utworzone przez Compose

Dobre praktyki

Aby w pełni wykorzystać potencjał Docker Compose i utrzymać porządek w projektach, warto stosować kilka sprawdzonych praktyk.

Zarządzanie konfiguracją: .env i pliki override

Jednym z pierwszych wyzwań, na jakie trafiamy w Docker Compose, jest zarządzanie konfiguracją – zwłaszcza takimi danymi jak hasła do bazy, klucze do API czy inne sekrety.

Na etapie developmentu czy testów nie ma co przesadzać – jeśli korzystasz z lokalnych danych testowych i nie łączysz się z żadnymi zewnętrznymi usługami, to wpisanie prostego hasła w stylu password: password123 jest w porządku. Nie ma sensu komplikować sobie życia, gdy chodzi tylko o lokalne odpalenie środowiska.

Inaczej sprawa wygląda w przypadku produkcji. Hardkodowanie haseł w docker-compose.yml to proszenie się o kłopoty – zwłaszcza jeśli plik trafia do repozytorium Git. Jeden commit z prawdziwymi danymi uwierzytelniającymi i masz problem, którego nie zapomina się do końca kariery 🙂.

W tej sytuacji lepszym podejściem jest użycie pliku .env, który umieszczamy w tym samym katalogu. Docker Compose automatycznie wczyta z niego zmienne i udostępni je do użycia w pliku docker-compose.yml. Co najważniejsze, plik .env zawsze dodajemy do .gitignore, dzięki czemu wrażliwe dane nigdy nie trafiają do repozytorium.

Nasz plik .env mógłby wyglądać tak:

POSTGRES_USER=user
POSTGRES_PASSWORD=supersecret

I wtedy w naszym pliku docker-compose.yml możemy zaczytać takie zmienne:

services:
  db:
    image: postgres:13
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}

W Docker Compose istnieją jeszcze pliki override. Docker Compose domyślnie wczytuje dwa pliki: docker-compose.yml oraz opcjonalny docker-compose.override.yml. Ten drugi pozwala na nadpisanie lub rozszerzenie konfiguracji z pliku bazowego. Jest to idealne rozwiązanie do rozdzielenia konfiguracji wspólnej (produkcyjnej) od deweloperskiej. W docker-compose.yml umieszczamy stabilną konfigurację, a w docker-compose.override.yml zmiany specyficzne dla naszego lokalnego środowiska, np. mapowanie portów czy montowanie kodu źródłowego.

# docker-compose.override.yml (automatycznie ładowany w developmencie)
version: '3.9'

services:
  app:
    volumes:
      - ./src:/app/src  # Hot reload w developmencie
    environment:
      - DEBUG=true

  database:
    ports:
      - "5432:5432"  # Eksponuj port w developmencie

# docker-compose.prod.yml (dla produkcji)
version: '3.9'

services:
  app:
    restart: unless-stopped
    environment:
      - DEBUG=false

  database:
    restart: unless-stopped
    # Nie eksponuj portu bazy na zewnątrz

Następnie przy uruchamianiu aplikacji możemy wybrać jaki plik chcemy zaczytać:

# Development (domyślnie)
docker compose up

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up

Skalowanie i równoważenie obciążenia (Load Balancing)

Docker Compose pozwala na proste, manualne skalowanie serwisów. Jeśli chcesz uruchomić więcej instancji swojej aplikacji, aby obsłużyć większy ruch, możesz użyć komendy:

docker compose up --scale app=3 -d

To polecenie uruchomi trzy kontenery serwisu app. Aby to zadziałało, musisz mieć przed nimi jakiś rodzaj load balancera (np. Nginx lub Traefik), który będzie rozdzielał ruch między te instancje. Docker Compose sam z siebie nie zapewnia automatycznego load balancingu. Skalowanie w Compose jest manualne i nadaje się do prostych scenariuszy.

Tak, więc zróbmy prosty przykład:

version: '3.9'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - app

  app:
    build: .
    # Usuń ports - nginx będzie proxy
    environment:
      - DATABASE_URL=postgresql://user:password@database:5432/myapp

  database:
    image: postgres:16
    # ... konfiguracja bazy

A także nasz plik nginx.conf, który posłuży za Load Balancer:

events {
    worker_connections 1024;
}

http {
    upstream app_backend {
        server app:8080;
    }

    server {
        listen 80;
        
        location / {
            proxy_pass http://app_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

Dzięki temu możemy skalować nasze kontenery z użyciem Docker Compose.

Docker Compose vs. Kubernetes: Kiedy iść o krok dalej?

Zarówno Docker Compose, jak i Kubernetes zajmują się orkiestracją kontenerów, ale każdy z nich gra w trochę innej lidze i celuje w inne scenariusze.

Docker Compose to prostsze, lżejsze rozwiązanie, które świetnie sprawdza się w lokalnym devie, testach integracyjnych czy niewielkich wdrożeniach. Kilka usług, jeden plik docker-compose.yml, jedno polecenie – i masz gotowe środowisko.

Kubernetes to już ciężki kaliber – platforma stworzona do zarządzania dużymi klastrami. Oferuje automatyczne skalowanie, samonaprawianie się aplikacji, wdrożenia bez przestojów oraz zaawansowane strategie deployu (blue/green, canary).

Główne różnice? Dość spore:

  • Skalowanie – Compose: klikasz --scale app=3 i masz 3 kontenery. Kubernetes: ustawiasz metryki i sam skaluje w górę/dół jak mu się podoba.
  • Awarie – Compose: jak serwer padnie, idziesz po kawę i czekasz aż admin naprawi. Kubernetes: automatycznie przenosi kontenery na inny serwer.
  • Złożoność – Compose: nauczysz się w godzinę. Kubernetes: miesiące nauki, certyfikaty, a i tak będziesz googlować.
  • Load balancing – Compose: musisz sam skonfigurować Nginx. Kubernetes: ma to wbudowane.
  • Środowisko – Compose: jeden serwer, jeden plik YAML. Kubernetes: klastry wielu maszyn z całą infrastrukturą.

Kiedy warto iść w stronę Kubernetesa?

  • Twoja aplikacja musi działać non-stop – Kubernetes automatycznie restartuje padające kontenery i przenosi je na inne serwery
  • Potrzebujesz skalować na poważnie – mowa o tysiącach użytkowników
  • Masz kilka serwerów – Docker Compose działa na jednej maszynie, Kubernetes zarządza całymi klastrami

Najłatwiej jest zacząć od Docker Compose, żeby szybko wystartować z projektem i mieć wygodne środowisko do codziennej pracy. Gdy aplikacja zacznie rosnąć, a w grę wejdą wymagania dotyczące dużej skali, wysokiej dostępności czy bardziej skomplikowanych wdrożeń – wtedy warto rozważyć przesiadkę na Kubernetes. Dobrym podejściem jest też korzystanie z Compose w lokalnym developmentcie, a tę samą aplikację wdrażać później na klaster Kubernetes w środowisku produkcyjnym.

Podsumowanie

Docker Compose to narzędzie, które powinien znać każdy, kto z Dockerem robi coś więcej niż stawia pojedynczy kontener. Zamienia ręczne, mozolne uruchamianie całego stosu w prosty, powtarzalny proces, który można odpalić jednym poleceniem. Dzięki niemu w kilka sekund przygotujesz kompletne środowisko developerskie, odpalasz testy czy wdrożysz mniej skomplikowane aplikacje – bez chaosu i zbędnych klików. 

Mam nadzieję, że ten wpis dał Ci solidne podstawy i praktyczne przykłady, które pozwolą pewnie korzystać z Docker Compose w Twoich projektach. Pamiętaj – warto bawić się konfiguracją, sprawdzać różne scenariusze i zaglądać do oficjalnej dokumentacji, bo można tam znaleźć naprawdę sporo przydatnych rozwiązań. 

Jeśli masz pytania, uwagi, albo chcesz pochwalić się swoimi doświadczeniami z Docker Compose – pisz w komentarzach! Chętnie pomogę i podyskutuję.

Subskrybuj
Powiadom o
guest
2 komentarzy
Najstarsze
Najnowsze Najwięcej głosów
Opinie w linii
Zobacz wszystkie komentarze
Michał

Fajny, czytelny artykuł. Ale:

Są podane 3 typowe przypadki użycia: development, testy, wdrożenie na 1 hoście. Ja mam do czynienia z pierwszymi dwoma.

1. „Hardkodowanie wartości konfiguracyjnych (np. haseł, kluczy API) bezpośrednio w pliku docker-compose.yml jest złym pomysłem, zwłaszcza jeśli plik ten trafia do repozytorium Git”. Dla wdrożenia tak, dla developmentu i testów nie, jeśli development i test nie korzysta z zewnętrznych serwisów.
2. Podany jest przykład developmentu ze Spring Bootem. Warto tu wspomnieć, że Spring Boot ma własne wsparcie do docker-compose. Bardziej efektywne niż podany tu sposób uniwersalny
3. 'networks’ jest tak wyjaśnione że niczego nie tłumaczy a wprowadza zamieszanie. Lepiej to pominąć albo wyjaśnić dokładnie jak działają sieci w dockerze.
4. „’version’ (…) jego umieszczenie jest dobrą praktyką”. Dobrą praktyką nie zaśmiecanie tego, co człowiek czyta, przez zbędne rzeczy (cognitive load). Jeżeli w developmencie, teście albo prostym wdrożeniu nie jesteśmy w stanie zapanować nad aktualizacją oprogramowania to mamy problem, ale nie docker compose.
5. „Nie wystawiamy portu bazy danych na zewnątrz (…) Jedynie dobrą opcją jest wystawić jeśli mamy środowiska developerskie”. Wdrożenie: prawda, do momentu coś nie działa jak trzeba i chcemy sprawdzić zawartość bazy danych, czyli zwykle 1 dzień. Test: prawda, do momentu gdy testy nie działają jak trzeba i chcemy sprawdzić zawartość bazy danych – jeśli możemy odtworzyć test lokalnie to zwykle nigdy, jeśli działa tylko na CI zwykle 1 miesiąc.