Wstęp
Jeśli pracujesz z Dockerem, na pewno już doceniasz, jak gwałtownie 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 manualnie 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, iż 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:latestTeraz wyobraź sobie, iż każdy z tych kontenerów ma jeszcze zmienne środowiskowe, specyficzne konfiguracje sieci, health checki… gwałtownie 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żeli 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 manualnej instalacji i konfiguracji bazy danych, brokera wiadomości i innych zależności, wystarczy, iż 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, iż 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żeli 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żeli masz Docker Desktop, prawdopodobnie masz już wszystko, czego potrzebujesz. Aby to sprawdzić, otwórz terminal i wpisz: docker compose version. jeżeli zobaczysz numer wersji, jesteś gotowy do pracy.
Linux (jako plugin) – jeżeli na Linuksie zainstalowałeś silnik Dockera manualnie (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-pluginPo 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, # iż 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, iż 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żeli 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. najważniejsze 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żeli 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, choćby 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, iż 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, # iż 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 logachKrok 2: Dockerfile dla aplikacji Spring Boot
Aby skonteneryzować naszą aplikację, potrzebujemy prostego Dockerfile. Zakładając, iż budujemy naszą aplikację dzięki 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, iż nie chcemy mieć tego robionego przez Spring Boota (natomiast jeżeli 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, iż 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, iż w terminalu, w głównym katalogu projektu, wykonamy dwie komendy:
Najpierw zbudujemy aplikacje Spring Boot do pliku .jar:
./mvnw clean packageA następnie wszystko uruchomimy przy użyciu Docker Compose:
docker compose upNatomiast jeżeli chcemy uruchomić kontenery w tle, wystarczy, iż dodamy flagę -d:
docker compose up -dI 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żeli 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żeli 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 manualnie 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, iż 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żeli 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, iż 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żeli 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żeli 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żeli 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=supersecretI 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ątrzNastę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 upSkalowanie i równoważenie obciążenia (Load Balancing)
Docker Compose pozwala na proste, manualne skalowanie serwisów. jeżeli 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 -dTo 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 bazyA 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 gwałtownie 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 manualne, 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ę, iż 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ę.









