Docker Compose - jak działa i kiedy go używać

Uploaded image

Na początku był chaos

Na łamach tego bloga mówiliśmy już o różnicach między kontenerami a wirtualizatorami oraz o tym jak pisać dobrzę dockerfile. Padło tam zdanie, że jeden kontener to jedna usługa, a jak potrzebujesz kilku naraz to używamy docker-compose albo Kubernetes. Dzisiaj nadszed czas zapoznania się z jednym z nich.

Sytuacja z życia wzięta. Twoja aplikacja backendowa potrzebuje bazy danych, do tego jakiś Redis pod cache, a do kompletu jeszcze jakiś kontenerek do przetwarzania kolejek. Odpalasz to wszystko ręcznie: docker run na bazę, towrzysz osobą sieć i zapamiętujesz nazwę, docker run na Redisa, znowu ta sama sieć, docker run na aplikację z dziesięcioma flagami -e i -v, i na końcu kolejki, które czasem zapomnisz w ogóle wystartować. Działa, dopóki nie zrestartujesz komputera. Albo dopóki kolega z zespołu nie spróbuje odpalić tego samego u siebie i nie spędzisz popołudnia, odtwarzając te wszystkie flagi z Twojej pamięci, a nie z dokumentacji, bo komu to potrzebne.

Każde narzędzie w IT powstało żeby rozwiązać jakiś problem. Docker Compose rozwiązuje dokładnie to o czym była mowa. Zamiast serii komend w terminalu, albo nawet pisania skryptu który to wszystko ogarnia, masz jeden plik YAML, który opisuje cały stack, wszystkie usługi, ich zależności, sieci i wolumeny, a na deser masz jedną komendę, która to wszystko odpala.

Co to właściwie jest

Docker Compose to narzędzie do definiowania i uruchamiania aplikacji złożonych z wielu kontenerów. Zamiast pamiętać flagi do docker run, opisujesz całą infrastrukturę w jednym pliku docker-compose.yml, deklaratywnie, czyli mówisz "chcę, żeby infrastryktura tak wyglądała po zakończeniu", a nie imperatywnie, czyli "krok po kroku zrób to i to". Compose sam ogarnia kolejność, sieć między kontenerami i przywracanie stanu.

To nie jest narzędzie do produkcji na dużą skalę, do tego służy Kubernetes, o czym napiszę osobno. Ale do lokalnego developmentu, małych projektów, czy prostych wdrożeń na jednym serwerze, Compose jest dokładnie tym, czego potrzebujesz. Prostszym, szybszym i bez narzutu, jaki niesie ze sobą pełny orkiestrator.

Anatomia pliku docker-compose.yml

Zbudujmy coś konkretnego - prostą aplikację webową z bazą PostgreSQL i Redisem pod cache. Tak wygląda docker-compose.yml dla takiego stacku:

Przejdźmy przez to po kolei, bo każda z tych sekcji odpowiada za co innego.

services

Każdy blok pod services to jeden kontener. app buduje się z lokalnego Dockerfile (stąd build: .), db i cache biorą gotowe obrazy z Docker Hub. Są to gotowe usługi którym podaje się dane do logowania i zaczynają działać. Nazwa usługi (app, db, cache) to jednocześnie nazwa hosta, pod którą inne kontenery mogą się do niej dobić. Dlatego w DATABASE_URL wpisujesz nazwę kontenera z bazą danych, w naszym przypadku db, a nie jakiś adres IP. Compose sam tworzy dla całego stacku wspólną sieć i sam rozwiązuje te nazwy jak lokalny DNS.

ports

Linia "3000:3000" mapuje port kontenera na port Twojego hosta. Pierwsza liczba to host, druga to kontener. Dla przykładu jeśłi chcemy, żeby nasza aplikacja działąła na porcie 443 (HTTPS), a działą na 8080, musimy wzkazać kontenerowi, do jakiego portu ma iść ruch i z jakiego portu wychodzi (443:8080). Baza i cache nie mają tu żadnego mapowania, bo nie muszą być dostępne z zewnątrz. Do środka dobija się tylko app, przez wewnętrzną sieć Compose.

environment

Zmienne środowiskowe, które trafiają do kontenera. W realnym projekcie hasła i sekrety zamiast wpisywać wprost, warto trzymać w osobnym pliku .env i odwoływać się do nich jako ${DB_PASSWORD}, ale to temat na osobny akapit, do którego zaraz wrócę.

depends_on

Mówi Compose, w jakiej kolejności startować kontenery. Kontener app poczeka, aż db i cache w ogóle wystartują. Ważna pułapka: depends_on pilnuje kolejności startu kontenera, a nie tego, czy usługa w środku jest już gotowa na połączenia. Postgres potrafi uruchomić kontener i jeszcze przez chwilę inicjalizować się w środku, zanim faktycznie zacznie przyjmować połączenia. Jeśli Twoja aplikacja łączy się z bazą zaraz po starcie i nie ma retry, warto dodać healthcheck i condition: service_healthy, żeby Compose naprawdę czekał na gotowość, a nie tylko na sam start kontenera.

volumes

Bez tego wszystko, co baza zapisze, zniknęłoby razem z kontenerem przy jego usunięciu. db_data:/var/lib/postgresql/data mówi: dane z tej ścieżki w kontenerze trzymaj w nazwanym wolumenie na hoście, niezależnie od tego, ile razy kontener padnie albo zostanie odbudowany. Deklaracja volumes: na samym dole pliku tworzy te wolumeny, jeśli jeszcze nie istnieją.

Podstawowe komendy, których naprawdę używasz

  • docker compose up -d - odpala cały stack w tle
  • docker compose ps - pokazuje status kontenerów w stacku
  • docker compose logs -f app - logi konkretnej usługi na żywo
  • docker compose down - zatrzymuje i usuwa kontenery (wolumeny zostają)
  • docker compose down -v - to samo, ale kasuje też wolumeny - ostrożnie z tym
  • docker compose restart app - restart jednej usługi bez ruszania reszty
  • docker compose exec app sh - wchodzisz do środka działającego kontenera

docker compose up -d to komenda, po której zapamiętasz cały Compose, nawet jeśli zapomnisz wszystkich pozostałych. Flaga -d odpala wszystko w tle, zamiast blokować Ci terminal logami ze wszystkich kontenerów naraz.

.env zamiast haseł wpisanych na sztywno

Zamiast trzymać hasła i sekrety bezpośrednio w docker-compose.yml, Compose domyślnie czyta plik .env z tego samego katalogu i podstawia jego wartości tam, gdzie widzi ${ZMIENNA}:

Ten plik oczywiście ląduje w .gitignore, dokładnie tak samo, jak .env w normalnym projekcie backendowym. Do repo trafia za to .env.example z tymi samymi kluczami, ale bez realnych wartości, żeby ktoś, kto klonuje projekt, wiedział, co w ogóle musi uzupełnić.

Częsty błąd na start - jeden stack, wiele środowisk

Jedna pułapka, w którą łatwo wpaść na początku: ten sam docker-compose.yml używany bez zastanowienia i lokalnie, i na serwerze produkcyjnym. Lokalnie chcesz hot-reload, zamontowany kod źródłowy jako wolumen i otwarte porty do debugowania. Na produkcji nie chcesz żadnego z tych rzeczy. Chcesz zbudowany obraz, żadnego montowania kodu z hosta i tylko te porty, które faktycznie muszą być otwarte.

Rozwiązanie to rozbicie configu na kilka plików: bazowy docker-compose.yml z tym, co wspólne, i nakładki docker-compose.override.yml (ładowany automatycznie lokalnie) albo docker-compose.prod.yml, odpalany jawnie:

Dzięki temu nie kopiujesz całego pliku za każdym razem, gdy coś się różni między środowiskami - nadpisujesz tylko te fragmenty, które faktycznie muszą być inne.

Podsumowanie

Docker Compose nie zastąpi Kubernetesa tam, gdzie potrzebujesz autoskalowania na kilkudziesięciu węzłach albo zero-downtime deploymentów na produkcji o dużym ruchu. Ale do lokalnego developmentu, małych projektów i prostych serwerów to narzędzie, które oszczędza godziny wklepywania tych samych flag do docker run i eliminuje klasyczne "u mnie działa".

Zanim zamkniesz tę kartę, kilka rzeczy, które warto zapamiętać na dłużej:

  • Jeden plik docker-compose.yml opisuje cały stack deklaratywnie - usługi, sieci, wolumeny i zależności między nimi
  • depends_on pilnuje kolejności startu, a nie gotowości usługi - do tego służy healthcheck
  • Wolumeny to jedyny sposób, żeby dane bazy przetrwały restart czy odbudowę kontenera
  • Sekrety i hasła trzymaj w .env, nigdy wpisane na sztywno w YAML-u

Jeden plik YAML, jedna komenda, ten sam efekt za każdym razem. U Ciebie, u kolegi, u rodziny i na serwerze. Jeśli ogarnąłeś już Dockerfile z poprzedniego wpisu, Compose to naturalny kolejny krok, bo w końcu bez dobrych obrazów nawet najlepszy plik Compose niewiele pomoże.

A jeśli w pewnym momencie Compose przestanie Ci wystarczać, bo potrzebujesz kilku serwerów, automatycznego skalowania albo zero-downtime deploymentów, to naturalny następny krok to Kubernetes. Ale to już temat na osobny wpis.

#docker #devops #backend #sysadmin #linux #containerization

Podobne posty