Problem, który rozwiązuje Compose

Prawdziwa aplikacja rzadko jest jednym kontenerem. Web API potrzebuje bazy danych, baza potrzebuje wolumenu, żeby jej dane przetrwały restart, a potem dochodzi jeszcze cache Redis i worker w tle. To cztery polecenia docker run, każde z własnymi flagami dla portów, wolumenów, zmiennych środowiskowych i wspólnej sieci. W odpowiedniej kolejności. Za każdym razem, gdy siadasz do pracy.

Docker Compose zastępuje te polecenia jednym plikiem i jednym poleceniem. Opisujesz kontenery i to, jak się łączą, w pliku o nazwie compose.yaml, a docker compose up uruchamia je wszystkie. Jeśli obrazów i docker run jeszcze nie znasz, przeczytaj najpierw czym jest Docker i jak działają kontenery.

Co opisuje plik compose

Plik compose ma garść kluczy najwyższego poziomu. Ten, którego używasz zawsze, to services — każda usługa to kontener, który Compose uruchomi.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:18
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  db-data:

Dwa kontenery. api jest budowany z pliku Dockerfile w bieżącym katalogu i publikuje port 8000. db uruchamia oficjalny obraz postgres:18 i trzyma swoje dane w nazwanym wolumenie, więc dane zostają, kiedy odtwarzasz kontener.

Dwa szczegóły wykonują tu prawdziwą robotę:

  • api dociera do db po nazwie hosta db. Compose umieszcza każdą usługę we wspólnej sieci i rejestruje nazwę każdej usługi jako nazwę DNS. Żadnych adresów IP, żadnego starego --link. DATABASE_URL wskazuje na db:5432 i nazwa się rozwiązuje.
  • depends_on z condition: service_healthy wstrzymuje api, aż Postgres naprawdę odpowie. Zwykłe depends_on czeka tylko na start kontenera, a nie na to, aż proces bazy w środku zacznie przyjmować połączenia — ta różnica to najczęstsza przyczyna „connection refused” przy pierwszym uruchomieniu.

Trzy główne klucze: services, volumes, networks

Trzy klucze najwyższego poziomu odpowiadają pojęciom Dockera, które już znasz:

KluczCo definiujeRęczny odpowiednik
serviceskontenery do uruchomieniadocker run
volumesnazwane wolumeny dla danych, które muszą przetrwaćdocker volume create
networkssieci między usługamidocker network create

networks deklarujesz rzadko. Compose tworzy jedną sieć na projekt i podłącza do niej każdą usługę. Dlatego przykład z Postgresem nie wymagał żadnej konfiguracji sieci. Deklaruj sieci jawnie tylko wtedy, gdy chcesz odciąć jedną grupę usług od drugiej:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  api:
    build: .
    networks: [frontend, backend]
  db:
    image: postgres:18
    networks: [backend]        # not reachable from frontend
  proxy:
    image: caddy:2
    networks: [frontend]

networks:
  frontend:
  backend:

Polecenia, których naprawdę będziesz używać

Uruchamiaj je z katalogu z plikiem compose.yaml.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Uruchom wszystko, logi w terminalu
docker compose up

# Uruchom w tle
docker compose up -d

# Przebuduj obrazy, które mają sekcję build, i uruchom
docker compose up --build

# Zatrzymaj i usuń kontenery oraz sieci (nazwane wolumeny zostają)
docker compose down

# To samo, plus usunięcie nazwanych wolumenów
docker compose down -v

# Co działa w tym projekcie
docker compose ps

# Śledź logi jednej usługi
docker compose logs -f api

# Jednorazowe polecenie w nowym kontenerze
docker compose run --rm api python manage.py migrate

# Powłoka w już działającym kontenerze
docker compose exec api bash

up i down to para, którą wpisujesz najczęściej. up można bez ryzyka uruchamiać wielokrotnie: po edycji pliku odtwarza tylko te usługi, których konfiguracja się zmieniła, a resztę zostawia w spokoju.

Co docker compose up wypisuje przy pierwszym uruchomieniu

Uruchom powyższy przykład z Postgresem, a dostaniesz mniej więcej to:

1
2
3
4
5
[+] Running 4/4
 ✔ Network app_default    Created
 ✔ Volume "app_db-data"   Created
 ✔ Container app-db-1      Healthy
 ✔ Container app-api-1     Started

Do nazw Compose dokłada z przodu nazwę projektu (domyślnie nazwę katalogu), a na końcu numer, bo potrafi uruchomić więcej niż jedną replikę tej samej usługi. app-db-1 jest Healthy, zanim wystartuje app-api-1 — to condition: service_healthy wykonuje swoją pracę.

Jak trzymać hasła poza plikiem compose

Compose czyta plik o nazwie .env w katalogu projektu i podstawia odwołania ${VAR} w pliku compose:

1
2
3
4
5
services:
  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
1
2
# .env  — add this file to .gitignore
DB_PASSWORD=secret

Trzymaj .env poza kontrolą wersji, a zamiast niego zacommituj .env.example z pustymi lub przykładowymi wartościami. Do czegoś naprawdę wrażliwego w środowisku produkcyjnym użyj Docker secrets zamiast zmiennych środowiskowych.

Opcjonalne usługi z profiles

Nie każda usługa musi startować za każdym razem. Wpis profiles trzyma usługę wyłączoną, dopóki o nią nie poprosisz:

1
2
3
4
5
6
7
8
9
services:
  api:
    build: .
  db:
    image: postgres:18
  seed:
    build: .
    command: python manage.py seed_demo_data
    profiles: [tools]

docker compose up uruchamia api i db, a seed pomija zupełnie. docker compose --profile tools run --rm seed uruchamia seeder, gdy o niego poprosisz. Używaj profiles do wszystkiego jednorazowego: seedery, migracje, powłoka do debugowania, generator obciążenia, którego chcesz tylko podczas testu.

Compose v2 kontra stary docker-compose

Jeśli poradnik każe ci uruchomić docker-compose z myślnikiem, pochodzi sprzed 2023 roku. To była wersja v1, napisana w Pythonie, dziś już bez wsparcia. Obecne narzędzie to docker compose jako podpolecenie CLI Dockera; jest częścią Docker Desktop oraz pakietu wtyczki Compose dla Docker Engine.

Starsze pliki często mają na początku taką linię:

1
version: "3.8"   # delete it

W Compose v2 klucz version jest ignorowany. Compose sprawdza plik według aktualnej Compose Specification i ostrzega, kiedy ten klucz jest obecny. Usuń go, a ostrzeżenie zniknie.

Domyślna nazwa pliku też się zmieniła: compose.yaml jest aktualna, docker-compose.yml nadal działa, a Compose szuka obu.

Przeładowanie na żywo podczas developmentu

docker compose watch (Compose 2.22 i nowsze) aktualizuje kontenery, gdy edytujesz kod. Dodaj do usługi blok develop:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  api:
    build: .
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: ./requirements.txt

sync kopiuje zmienione pliki wprost do działającego kontenera, więc zmiana w kodzie jest widoczna bez przebudowy. rebuild wyzwala pełną przebudowę obrazu, gdy zmienia się plik wpływający na build, na przykład lockfile zależności. Uruchom to przez docker compose watch albo dodaj --watch do up.

Kiedy Compose to złe narzędzie

Compose uruchamia kontenery na jednej maszynie. To jest granica, a większość tego, czego ludziom brakuje w Compose, leży po jej drugiej stronie.

Nadaje się do lokalnego developmentu, automatycznych testów w CI i małych wdrożeń na jednym hoście. Nie potrafi rozkładać kontenerów na wiele serwerów, zastąpić jednego, gdy węzeł padnie, robić stopniowych wdrożeń sterowanych przez healthcheck ani autoskalować.

To zadania orkiestratora: Kubernetes albo Docker Swarm, jeśli chcesz czegoś lżejszego. Te dwa nie konkurują ze sobą. Plik compose to często wstępny szkic, który później staje się zestawem manifestów Kubernetes, a wiele zespołów dalej używa Compose lokalnie długo po tym, jak produkcja przeniosła się na klaster.

Z Podmanem jest podobnie: podman compose i podman-compose czytają ten sam format pliku, z zastrzeżeniami opisanymi w Docker vs Podman.

Częste błędy

  • Sekrety zacommitowane w compose.yaml. Plik trafia do Gita. Użyj .env (w gitignore) albo Docker secrets.
  • Liczenie na to, że depends_on poczeka na gotowość usługi. Samo w sobie czeka na start kontenera, nie na to, aż usługa zacznie przyjmować połączenia. Połącz go z healthcheck i condition: service_healthy.
  • Ustawianie container_name. Uniemożliwia to uruchomienie więcej niż jednej kopii projektu i psuje docker compose up --scale. Pozwól Compose nazwać kontenery.
  • Bind mount na katalogu z zainstalowanymi zależnościami. Zamontowanie folderu projektu w kontenerze Node lub Pythona może zasłonić node_modules albo virtualenv utworzony podczas builda. Montuj podkatalogi kodu źródłowego albo podłącz anonimowy wolumen do ścieżki zależności.

Jak przenieść istniejącą aplikację na Compose

Weź aplikację, którą dziś uruchamiasz skryptem pełnym linii docker run, i przenieś ją do compose.yaml usługa po usłudze. Uruchom, przeczytaj logi, napraw to, co się psuje, dodaj kolejną usługę. Daj healthcheck wszystkiemu, od czego zależą inne usługi — to krok, który wszyscy pomijają, a który zatrzymuje „connection refused” przy zimnym starcie.

Poznasz, że się udało, gdy docker compose down -v && docker compose up odbuduje całe środowisko od zera, a aplikacja za każdym razem wróci tak samo.

Powiązane artykuły