Kurs NestJS · Moduł 12: Konteneryzacja i CI/CD

Docker Compose - multi-service networking i wolumeny

4 min czytania
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 -d

Trzy 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 pod services to jeden kontener,
  • depends_on zapewnia kolejność uruchamiania serwisów,
  • uruchomiony to nie gotowy - depends_on czeka na start kontenera, a nie na gotowość usługi,
  • healthcheck plus condition: service_healthy każe czekać na faktyczną gotowość; service_started wystarcza dla szybkich usług,
  • kontenery łączą się po nazwie serwisu - mongodb://mongodb:27017, bez adresów IP i bez konfiguracji sieci,
  • brak sekcji ports znaczy, ż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 --build przebudowuje obrazy, a -d puszcza w tle,
  • down zostawia wolumeny, down -v usuwa 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});
60

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Do czego służy Docker Compose?

  2. 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

Przydatne artykuły