Kurs NestJS · Moduł 12: Konteneryzacja i CI/CD
Docker Compose - multi-service networking i wolumeny
W tej lekcji6
Twoja aplikacja to już nie jeden proces. Jest API, jest MongoDB, jest Redis - i żeby uruchomić całość lokalnie, trzeba wywołać trzy razy docker run z długą listą flag, w odpowiedniej kolejności, pamiętając o sieci i o tym, że baza potrzebuje chwili na start. Przy każdym nowym członku zespołu ta instrukcja jest przekazywana ustnie i za każdym razem trochę inaczej.
Legion nie wymaszerowywał na podstawie ustnych ustaleń. Miał rozkaz dzienny: spisaną listę oddziałów, ich kolejność i to, co każdy ze sobą zabiera. Docker Compose jest takim rozkazem - służy do zarządzania wieloma kontenerami jednocześnie.
Nie myl go z sąsiednimi narzędziami: budowaniem obrazów z kodu źródłowego zajmuje się docker build i Dockerfile, testowaniem - Twój pipeline, a monitorowaniem zużycia zasobów - narzędzia, które poznałeś przy obserwowalności.
Rozkaz dzienny
Cała konfiguracja mieści się w jednym pliku docker-compose.yml:
1services:
2 mongodb:
3 image: mongo:7
4 volumes:
5 - mongo-data:/data/db
6 healthcheck:
7 test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
8 interval: 10s
9 retries: 5
10
11 redis:
12 image: redis:7-alpine
13
14 api:
15 build: .
16 ports:
17 - "3000:3000"
18 environment:
19 DATABASE_URL: mongodb://mongodb:27017/legions
20 REDIS_HOST: redis
21 depends_on:
22 mongodb:
23 condition: service_healthy
24 redis:
25 condition: service_started
26
27volumes:
28 mongo-data:Każdy wpis pod services to jeden kontener. image bierze gotowy obraz, build: . każe zbudować go z Dockerfile'a w tym katalogu.
Kolejność startu
Pole depends_on zapewnia kolejność uruchamiania serwisów. Nasze trzy serwisy wstają więc tak: najpierw MongoDB, potem Redis, na końcu API, które czeka na oba.
Ale tu jest pułapka, o którą rozbija się większość pierwszych konfiguracji: samo depends_on czeka tylko na uruchomienie kontenera, nie na jego gotowość. MongoDB potrzebuje kilku sekund na wczytanie danych - kontener już działa, a baza jeszcze nie przyjmuje połączeń. API startuje, próbuje się połączyć i pada.
Dlatego przy bazie stoi healthcheck, a w depends_on warunek condition: service_healthy. Teraz Compose czeka, aż polecenie z healthcheck zacznie kończyć się powodzeniem. Redis wstaje szybko i wystarcza mu service_started - domyślne zachowanie, czyli „kontener ruszył".
Zapamiętaj tę różnicę: uruchomiony to nie to samo co gotowy. Zdecydowana większość błędów „connection refused" przy starcie środowiska bierze się właśnie stąd.
Sieć bez konfiguracji
Zwróć uwagę na adres bazy: mongodb://mongodb:27017/legions. Nie ma tam localhost ani żadnego adresu IP - jest nazwa serwisu.
Compose tworzy dla wszystkich serwisów wspólną sieć i rejestruje w niej każdy pod jego nazwą. Kontener api łączy się z bazą, pisząc po prostu mongodb - reszta dzieje się sama. Nie musisz znać adresów IP ani niczego konfigurować.
Zauważ też, czego nie ma w naszym pliku: MongoDB i Redis nie mają sekcji ports. To celowe. Bez niej usługa jest widoczna tylko wewnątrz sieci Compose - API do niej dosięgnie, ale nikt z zewnątrz już nie. Port wystawiamy wyłącznie tam, gdzie naprawdę trzeba, czyli przy API.
Wolumeny - dane, które przeżyją kontener
Kontener jest z założenia nietrwały: usuwasz go i znika wszystko, co zapisał. Dla bazy danych to katastrofa, więc wolumeny służą do przechowywania danych trwałych poza kontenerem.
Wpis mongo-data:/data/db mówi: to, co MongoDB zapisuje w katalogu /data/db, trzymaj w wolumenie o nazwie mongo-data, żyjącym niezależnie od kontenera. Usunięcie i odtworzenie kontenera nie tknie tych danych.
Wolumeny nie ograniczają zużycia procesora, nie szyfrują komunikacji i nie skalują niczego - przechowują dane, i tyle.
Uruchomienie
Cały rozkaz wykonuje jedno polecenie:
1docker compose up --build -dTrzy człony po up warto rozdzielić. --build przebudowuje obrazy przed startem - bez tego Compose użyje poprzednio zbudowanego, choćbyś zmienił kod. -d (od detached) puszcza wszystko w tle i oddaje Ci terminal.
Zatrzymanie ma dwa warianty i różnica między nimi bywa kosztowna. docker compose down usuwa kontenery i sieć, zostawiając wolumeny. docker compose down -v usuwa także wolumeny - czyli dane bazy. Pierwsze robisz codziennie, drugie wtedy, gdy naprawdę chcesz zacząć od pustej bazy.
Podsumowanie
Rozkaz dzienny spisany, oddziały wyruszają w kolejności:
- Docker Compose służy do zarządzania wieloma kontenerami jednocześnie - nie buduje obrazów z kodu, nie testuje i nie monitoruje zużycia,
- konfiguracja mieszka w
docker-compose.yml, a każdy wpis podservicesto jeden kontener, depends_onzapewnia kolejność uruchamiania serwisów,- uruchomiony to nie gotowy -
depends_onczeka na start kontenera, a nie na gotowość usługi, healthcheckpluscondition: service_healthykaże czekać na faktyczną gotowość;service_startedwystarcza dla szybkich usług,- kontenery łączą się po nazwie serwisu -
mongodb://mongodb:27017, bez adresów IP i bez konfiguracji sieci, - brak sekcji
portsznaczy, że usługa jest widoczna tylko wewnątrz sieci Compose, - wolumeny przechowują dane trwałe poza kontenerem - nie ograniczają CPU, nie szyfrują i nie skalują,
- uruchomienie:
docker compose up --build -d, gdzie--buildprzebudowuje obrazy, a-dpuszcza w tle, downzostawia wolumeny,down -vusuwa je razem z danymi.
W następnej lekcji zajmiemy się sekretami - hasłami i kluczami, które w tym pliku wpisaliśmy jeszcze wprost. A na razie zapamiętaj: Compose to rozkaz dzienny - jeden plik zastępuje ustną instrukcję, którą każdy zapamiętał inaczej.
Kod do tej lekcji: src/docker-compose-demo.ts
1// Docker Compose - Siec Drog Imperium
2console.log("=== DOCKER COMPOSE ===\n");
3
4interface ComposeService {
5 name: string;
6 image: string;
7 port: number;
8 role: string;
9 dependsOn?: string[];
10}
11
12const services: ComposeService[] = [
13 {
14 name: 'api',
15 image: 'Dockerfile (build)',
16 port: 4000,
17 role: 'Glowna Forteca (Praetorium)',
18 dependsOn: ['mongodb', 'redis'],
19 },
20 {
21 name: 'mongodb',
22 image: 'mongo:7.0',
23 port: 27017,
24 role: 'Archiwum Imperium (baza danych)',
25 },
26 {
27 name: 'redis',
28 image: 'redis:7-alpine',
29 port: 6379,
30 role: 'Szybki Goniec (cache)',
31 },
32];
33
34console.log("Serwisy w docker-compose.yml:\n");
35services.forEach(s => {
36 console.log(` ${s.name}:`);
37 console.log(` Image: ${s.image}`);
38 console.log(` Port: ${s.port}`);
39 console.log(` Rola: ${s.role}`);
40 if (s.dependsOn) {
41 console.log(` depends_on: ${s.dependsOn.join(', ')}`);
42 }
43 console.log();
44});
45
46// Kluczowe komendy
47const commands = [
48 { cmd: 'docker compose up -d', desc: 'Uruchom wszystko w tle' },
49 { cmd: 'docker compose up --build -d', desc: 'Zbuduj i uruchom' },
50 { cmd: 'docker compose down', desc: 'Zatrzymaj wszystko' },
51 { cmd: 'docker compose logs -f api', desc: 'Logi serwisu api' },
52 { cmd: 'docker compose ps', desc: 'Status serwisow' },
53];
54
55console.log("Komendy Docker Compose:\n");
56commands.forEach(c => {
57 console.log(` $ ${c.cmd}`);
58 console.log(` ${c.desc}\n`);
59});
60Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Do czego służy Docker Compose?
2. Co zapewnia pole 'depends_on' w docker-compose.yml?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Zdefiniuj trzy serwisy z odpowiednimi obrazami, portami, zmiennymi środowiskowymi i zależnościami
- Układanie w pionie
Uporządkuj serwisy Docker Compose według kolejności uruchamiania (od pierwszego):
- Klikanie w kolejności
Ułóż elementy komendy uruchamiającej Docker Compose w tle z przebudową:
- Edytor kodu
Ustaw zmienne NODE_ENV, PORT, DATABASE, JWT_SECRET, REDIS_HOST, LOG_LEVEL, ALLOWED_ORIGINS dla obu środowisk