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:
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:password@db:5432/aplicationtest
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=aplicationtest
volumes:
- db_data:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
- cache_data:/data
volumes:
db_data:
cache_data:
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 tledocker compose ps- pokazuje status kontenerów w stackudocker compose logs -f app- logi konkretnej usługi na żywodocker compose down- zatrzymuje i usuwa kontenery (wolumeny zostają)docker compose down -v- to samo, ale kasuje też wolumeny - ostrożnie z tymdocker compose restart app- restart jednej usługi bez ruszania resztydocker 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}:
POSTGRES_PASSWORD=Secret_Password&^!
DATABASE_URL=postgres://user:${POSTGRES_PASSWORD}@db:5432/aplicationtest
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:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
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.ymlopisuje cały stack deklaratywnie - usługi, sieci, wolumeny i zależności między nimi depends_onpilnuje kolejności startu, a nie gotowości usługi - do tego służyhealthcheck- 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.