SSD Nodes Learn 8GB RAM — $66/rok
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-01

Docker Compose: wiele plików i zasady scalania

Sprawdź, jak compose.override.yaml ładuje się samodzielnie, jak działa kolejność plików, dlaczego ports może pozostawić otwarty port oraz jak użyć include dla dev i prod.

Co Compose robi z więcej niż jednym plikiem

Docker Compose może zbudować jeden projekt na podstawie kilku plików. Odczytuje je w kolejności, w której je otrzymuje, i scala je w jeden model. Późniejszy plik ma zatem pierwszeństwo w przypadku każdej kolidującej wartości. Z poziomu wiersza poleceń służą do tego dwa mechanizmy: plik zastępujący, który Compose ładuje automatycznie, oraz przekazywana ręcznie flaga -f. Trzeci mechanizm znajduje się wewnątrz pliku, w elemencie include, i działa inaczej niż oba pozostałe.

Scalanie nie polega na prostym nadpisaniu. Mapowania są scalane według kluczy, sekwencje są dołączane, a niewielki zestaw pól jest zastępowany w całości. To właśnie ta różnica powoduje nieoczekiwane wyniki, a lista ports jest elementem, który sprawia problemy niemal wszystkim.

Wszystkie poniższe informacje dotyczą Compose v2, czyli wtyczki docker compose, a nie starego skryptu docker-compose. Uruchom docker compose version, aby to sprawdzić. Jeśli plik Compose nie został jeszcze utworzony, zacznij od podstaw Docker Compose, a następnie wróć tutaj.

Plik override ładowany przez Compose bez jawnego wskazania

Uruchomienie docker compose up bez flagi -f powoduje, że Compose przeszukuje katalog roboczy, a następnie jego katalogi nadrzędne, w poszukiwaniu compose.yaml lub docker-compose.yaml. Jeśli plik override znajduje się obok pliku bazowego, Compose automatycznie ładuje go jako drugi.

ls compose.yaml compose.override.yaml
docker compose up -d

Jeśli obecne są oba pliki, jest to równoważne ręcznemu podaniu ich nazw.

docker compose -f compose.yaml -f compose.override.yaml up -d

Nazwy rozpoznawane przez Compose to compose.override.yaml, compose.override.yml oraz starsze docker-compose.override.yml i docker-compose.override.yaml. Każda inna nazwa, na przykład compose.dev.yaml, zostanie załadowana tylko po jawnej podaniu jej za pomocą -f.

Po przekazaniu choć jednego -f automatyczne ładowanie zostaje wyłączone. docker compose -f compose.yaml up odczytuje dokładnie ten jeden plik i pomija plik override. Na tej właściwości opiera się opisany dalej schemat dev i prod.

Na serwerze może to działać w obie strony. Plik override pozostawiony w katalogu wdrożenia zostanie załadowany przez każde polecenie docker compose uruchomione bez dodatkowych opcji z tego katalogu, w tym przez polecenie wykonywane przez zadanie cron. W ten sposób stos produkcyjny może zacząć montować za pomocą bind mount katalog z kodem źródłowym, którego nie planowano wdrażać. Po każdym wdrożeniu uruchom docker compose config i sprawdź wynik.

Kolejność z użyciem -f oraz sposób rozwiązywania ścieżek względnych

Compose tworzy konfigurację w kolejności podanych plików. Kolejne pliki zastępują ustawienia z poprzednich plików i dodają nowe ustawienia. Obowiązuje zasada: od lewej do prawej, wygrywa ostatnie ustawienie.

docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d

Każde polecenie w tym projekcie musi używać tej samej listy plików. Uruchomienie up z dwoma plikami i logs z jednym plikiem oznacza pracę z innym scalonym modelem. Jest to szybki sposób na uzyskanie usługi, której Compose nie rozpoznaje. Należy ustawić listę jednokrotnie za pomocą zmiennej środowiskowej COMPOSE_FILE.

export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -d

Separatorem w systemie Linux jest :, a zmienia go COMPOSE_PATH_SEPARATOR. Zmienna COMPOSE_FILE może również znajdować się w pliku projektu .env. Wtedy staje się częścią repozytorium zamiast historii powłoki. Ustawienie jawnie w wierszu poleceń ma pierwszeństwo przed zmienną środowiskową.

Teraz reguła, która powoduje problemy z montowaniami bind. Przy użyciu wielu plików z -f wszystkie ścieżki względne we wszystkich tych plikach są rozwiązywane względem katalogu pierwszego pliku, a nie względem pliku, w którym się znajdują. Wpisanie ./data:/var/lib/postgresql/data w pliku deploy/prod/compose.prod.yaml nadal powoduje, że Compose szuka ./data obok pliku bazowego. Docker tworzy następnie pusty katalog w tej niewłaściwej lokalizacji, a kontener uruchamia się bez znajdujących się w nim danych. Wygląda to jak utrata danych, ale nią nie jest. Należy przekazać --project-directory, aby samodzielnie ustawić ścieżkę bazową, albo użyć include, które rozwiązuje każdą ścieżkę względem katalogu odpowiadającego jej pliku.

Nazwa projektu pochodzi z tego samego katalogu bazowego. Zmiana kolejności plików może więc zmienić nazwę projektu. Zmieniona nazwa projektu oznacza nowe nazwy kontenerów i wolumenów. Stary wolumen nadal znajduje się na dysku pod starą nazwą. Należy ustawić nazwę jawnie za pomocą elementu najwyższego poziomu name: w pliku bazowym.

name: myapp

Które pola są scalane, a które zastępowane

Compose scala wartości na podstawie ich typu, a nie nazwy pola.

  • Pola jednowartościowe są zastępowane. image, command, entrypoint i mem_limit przyjmują bezpośrednio późniejszą wartość. Nie można dołączyć jednego argumentu do command, ponieważ nadpisanie zastępuje całą linię.
  • Mapowania są scalane według kluczy. environment, labels, volumes i devices zachowują wszystkie klucze z obu plików, a późniejszy plik ma pierwszeństwo w przypadku kluczy występujących w obu plikach. W przypadku environment i labels kluczem jest nazwa zmiennej lub etykiety. W przypadku volumes i devices kluczem jest ścieżka kontenera.
  • Sekwencje są dołączane. dns, dns_search, expose, tmpfs i external_links są łączone. Połączenie konfiguracji bazowej zawierającej expose: ["3000"] z nadpisaniem zawierającym ["4000", "5000"] daje ["3000", "4000", "5000"].

Cztery sekwencje zawierają klucz tożsamości, dlatego wpisy zgodne według tego klucza są scalane zamiast dołączane. volumes, secrets i configs są dopasowywane według target. ports jest dopasowywane na podstawie kombinacji ip, target, published i protocol.

Przeczytaj regułę ports dwukrotnie, ponieważ właśnie tutaj łatwo o błąd. Dwa wpisy portów są tym samym wpisem tylko wtedy, gdy wszystkie cztery elementy są zgodne. Zmiana dowolnego z nich sprawia, że Compose traktuje drugi wpis jako odrębny, niezwiązany wpis, dlatego zachowuje oba.

Dlaczego port jest nadal publikowany po zastosowaniu nadpisania

Plik bazowy, który publikuje usługę na każdym interfejsie:

services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"

Nadpisanie wiążące usługę wyłącznie z localhost, ponieważ przed usługą zostanie umieszczony odwrotny serwer proxy:

services:
  web:
    ports:
      - "127.0.0.1:8080:80"

Przed przyjęciem, że zmiana zadziałała, należy sprawdzić wynik.

docker compose -f compose.yaml -f compose.prod.yaml config

Oba wpisy znajdują się w danych wyjściowych. Część ip jest różna: 0.0.0.0 w porównaniu z 127.0.0.1. Z punktu widzenia procesu scalania są to dwa różne porty, dlatego publiczne wiązanie, które miało zostać usunięte, nadal znajduje się w modelu. Ma to większe znaczenie w Docker niż w innych środowiskach, ponieważ opublikowany port jest zapisywany w iptables przed regułami zapory. Mechanizm opisano w sekcji dlaczego opublikowane porty Docker omijają ufw.

Istnieją dwa rozwiązania. Jawnym rozwiązaniem jest znacznik !override, który zastępuje cały atrybut i pomija reguły scalania:

services:
  web:
    ports: !override
      - "127.0.0.1:8080:80"

!override wymaga Compose w wersji 2.24.4 lub nowszej. Przenośne rozwiązanie nie wymaga żadnego znacznika: należy całkowicie usunąć ports z pliku bazowego i zadeklarować go wyłącznie w plikach zależnych od środowiska. Brak elementu do scalenia oznacza brak możliwości jego ujawnienia. Taki wzorzec zastosowano w poniższym przykładzie.

Usuwanie wartości ustawionej w pliku bazowym

!reset usuwa atrybut i przywraca jego wartość domyślną albo ustawia ją na null. Wymaga podania wartości, ale ją ignoruje, dlatego należy podać poprawną, pustą wartość.

services:
  web:
    ports: !reset []
    environment:
      DEBUG: !reset null

!reset wymaga Compose w wersji 2.24 lub nowszej. Należy użyć tego rozwiązania, gdy plik bazowy nie jest przeznaczony do edycji, na przykład gdy jest to dołączany fragment dostawcy.

include, dla stosów składanych z części

include dołącza inną aplikację Compose do modelu. Jest elementem najwyższego poziomu, a nie flagą.

include:
  - path: ../commons/compose.yaml

Każda ścieżka w include jest ładowana jako osobny model aplikacji Compose z własnym katalogiem projektu. Ścieżki względne w tym pliku są więc rozwiązywane względem katalogu tego pliku. Jest to zasadnicza różnica względem -f oraz powód, dla którego include jest właściwym narzędziem, gdy fragment znajduje się w innym katalogu lub w innym repozytorium.

Długi zapis przyjmuje opcje podrzędne.

include:
  - path:
      - ../monitoring/compose.yaml
      - ../monitoring/compose.vps.yaml
    project_directory: ../monitoring
    env_file: ../monitoring/.env

path przyjmuje listę, a wskazane pliki są scalane zgodnie ze standardowymi regułami, zanim wynik zostanie dołączony do modelu. project_directory ustawia ścieżkę bazową używaną do rozwiązywania ścieżek względnych w dołączanym pliku. env_file nadaje dołączanemu plikowi własne zmienne używane podczas interpolacji. Zapobiega to niejawnemu odczytywaniu przez współdzielony fragment zmiennej .env z projektu. include wymaga Compose w wersji 2.20.0 lub nowszej.

Powtarzające się nazwy zasobów w pliku głównym i w dołączanym pliku są zgłaszane jako błąd, a nie niejawnie scalane. Jest to działanie zamierzone. Aby zmienić element zadeklarowany w dołączanym pliku, należy umieścić zmianę w compose.override.yaml. Nadpisanie jest stosowane do złożonego modelu, więc może modyfikować dołączone zasoby bez powodowania konfliktu nazw.

W skrócie: include składa oddzielne aplikacje, a -f nakłada konfigurację na jedną aplikację.

Rozdzielenie środowiska deweloperskiego i produkcyjnego na jednym VPS

Poniżej przedstawiono cały schemat w trzech plikach. Plik bazowy deklaruje ustawienia obowiązujące wszędzie i nie publikuje żadnych portów.

name: myapp

services:
  app:
    image: ghcr.io/example/app:1.4.2
    environment:
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
      LOG_LEVEL: info
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_DB: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  db_data:

Warunek depends_on sprawia, że aplikacja oczekuje na odpowiadającą bazę danych, a nie tylko na istniejący kontener. Wyjaśniono to w sekcji healthchecki i warunki depends_on. Zmienna POSTGRES_PASSWORD jest interpolowana z pliku projektu .env, który nigdy nie powinien znajdować się w repozytorium git. Bezpieczniejsze warianty opisano w sekcji pliki env i sekrety Compose.

Następny jest plik compose.override.yaml, ładowany automatycznie przez Compose. Jest to plik deweloperski.

services:
  app:
    build: .
    command: npm run dev
    environment:
      LOG_LEVEL: debug
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src

  db:
    ports:
      - "127.0.0.1:5432:5432"

Na laptopie samo polecenie docker compose up scala te dwa pliki. command zastępuje wartość domyślną obrazu, ponieważ jest wartością pojedynczą. LOG_LEVEL zastępuje info, ponieważ environment scala wartości według klucza. Dowiązanie bind mount oraz dwa publikowane porty są dodawane. Port bazy danych jest dowiązany wyłącznie do localhost, aby laptop podłączony do współdzielonej sieci nie udostępniał PostgreSQL innym urządzeniom.

Na końcu znajduje się compose.prod.yaml. Jego nazwa nie jest nazwą wyszukiwaną przez Compose, dlatego plik ten nigdy nie zostanie załadowany przypadkowo.

services:
  app:
    ports:
      - "127.0.0.1:8000:3000"
    deploy:
      resources:
        limits:
          memory: 512M

Na VPS należy jawnie wskazać oba pliki. To właśnie jawne wskazanie wyklucza plik override.

docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml ps

Polecenie ps powinno wykazać, że obie usługi działają, a db powinno wyświetlać (healthy). Ponieważ przekazano -f, plik compose.override.yaml nie został odczytany. W rezultacie polecenie deweloperskie, dowiązanie bind mount ze źródłem oraz publiczny port 3000 nie mogą przedostać się do środowiska produkcyjnego, nawet jeśli plik znajduje się w tym samym katalogu. Port 8000 jest dostępny wyłącznie przez localhost i jest gotowy do użycia przez proxy. Po dodaniu drugiej usługi zobacz sekcję uruchamianie kilku aplikacji za Traefik.

Ustaw COMPOSE_FILE=compose.yaml:compose.prod.yaml w .env serwera, a pozostałe polecenia ponownie będą zwykłymi poleceniami docker compose logs -f app.

Odczytaj scalony model przed wdrożeniem

docker compose config wyświetla w pełni scalony i zinterpolowany model. Nie jest to podgląd. Jest to dokładny model wejściowy, na podstawie którego Compose wykona operacje. Jeśli dane wyjściowe różnią się od oczekiwań, należy uznać je za prawidłowe.

docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml config --no-interpolate
docker compose -f compose.yaml -f compose.prod.yaml config --services

--no-interpolate pozostawia ${VAR} bez rozwinięcia. Należy użyć tego polecenia przed wklejeniem danych wyjściowych w dowolnym miejscu, ponieważ zwykłe config wyświetla wszystkie rozwiązane wartości kluczy tajnych w postaci jawnego tekstu. --services wyświetla tylko nazwy usług. Jest to szybki sposób na potwierdzenie, że include uwzględnił oczekiwane elementy.

Tryby awarii i obserwowane objawy

no configuration file provided: not found. Compose nie znalazł niczego do odczytania. Bieżący katalog znajduje się poza katalogiem projektu albo COMPOSE_FILE wskazuje ścieżkę, która nie istnieje. Compose przeszukuje katalogi nadrzędne w poszukiwaniu domyślnego pliku bazowego, ale nie wyszukuje w żadnym miejscu pliku podanego bezpośrednio.

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. Interpolacja jest wykonywana względem pliku projektu .env oraz środowiska powłoki, a katalog projektu jest tutaj katalogiem pierwszego pliku -f. Wdrożenie z katalogu innego niż katalog zawierający .env powoduje wyświetlenie tego ostrzeżenia, a następnie uruchomienie bazy danych, która odrzuca każde połączenie.

Edycja pliku zastępującego nie jest widoczna w docker compose config. Przekazano -f, które wyłącza automatyczne wczytywanie pliku zastępującego, albo Compose znalazł compose.yaml w katalogu nadrzędnym, a plik zastępujący znajduje się w innym katalogu. Uruchomienie docker compose config bez innych argumentów pokazuje model faktycznie tworzony przez Compose.

Punkt montowania bind jest pusty, a Docker utworzył katalog, o który nie proszono. Ścieżka względna została rozwiązana względem katalogu pierwszego pliku. Należy poprawić ścieżkę, przekazać --project-directory albo przenieść fragment za include.

Kontenery wracają z nowymi nazwami, a wolumin wygląda na pusty. Zmieniono nazwę projektu, ponieważ jest ona określana na podstawie katalogu pierwszego pliku. Należy dodać najwyższego poziomu name: do pliku bazowego, aby nazwy przestały się zmieniać. Stary wolumin nadal istnieje pod starym prefiksem. Polecenie docker volume ls go wyświetli.

Port usunięty w pliku zastępującym nadal jest otwarty. Scalanie ports dopisało element zamiast go zastąpić. Należy potwierdzić stan za pomocą docker compose config, a następnie użyć !override albo przenieść ports poza plik bazowy.

FAQ

Czy Compose automatycznie ładuje plik compose.override.yaml?

Tak, jeśli uruchomiono docker compose bez flagi -f. Compose przeszukuje katalog roboczy i jego katalogi nadrzędne w poszukiwaniu compose.yaml lub docker-compose.yaml. Jeśli obok tego pliku znajduje się plik override, zostaje on załadowany jako drugi. Rozpoznawane nazwy to compose.override.yaml, compose.override.yml, docker-compose.override.yml i docker-compose.override.yaml. Przekazanie dowolnej wartości -f wyłącza to zachowanie, dlatego docker compose -f compose.yaml up odczytuje tylko jeden plik.

W jakiej kolejności scalanych jest wiele plików -f?

Od lewej do prawej. Compose buduje konfigurację w kolejności podania plików. Każdy plik zastępuje wartości z poprzednich plików i dodaje do nich własne ustawienia. W przypadku konfliktu wygrywa ostatni plik w wierszu polecenia. Tej samej listy należy używać dla każdego polecenia w danym projekcie. Służy do tego COMPOSE_FILE=compose.yaml:compose.prod.yaml.

Dlaczego port jest nadal publikowany po jego zastąpieniu?

Ponieważ wpisy ports są identyfikowane na podstawie całego zestawu ip, target, published i protocol. Zastąpienie 127.0.0.1:8080:80 w konfiguracji bazowej 8080:80 różni się częścią ip. Dlatego Compose traktuje je jako drugi port i zachowuje oba wpisy. Po uruchomieniu docker compose config widoczne będą oba wpisy. Należy użyć ports: !override w Compose v2.24.4 lub nowszym albo usunąć ports z pliku bazowego, aby nie było wpisu, z którym można go scalić.

Jaka jest różnica między include a -f?

-f nakłada kilka plików na jedną aplikację. Ścieżki względne we wszystkich plikach są rozwiązywane względem katalogu pierwszego pliku. include dołącza osobną aplikację Compose. Każdy dołączony plik zachowuje własny katalog projektu, dlatego jego ścieżki względne są rozwiązywane względem tego katalogu. Należy używać -f dla warstw środowiskowych własnego stosu, a include dla fragmentu utrzymywanego w innym miejscu. include wymaga Compose v2.20.0 lub nowszego.

Jak usunąć wartość ustawioną w pliku bazowym?

Należy użyć znacznika !reset w Compose v2.24 lub nowszym. W pliku zastępującym należy zapisać ports: !reset [] lub MY_VAR: !reset null. Atrybut zostanie przywrócony do wartości domyślnej albo ustawiony na null. Wartość przekazana do znacznika jest wymagana, ale ignorowana. Aby zastąpić atrybut zamiast go wyczyścić, należy użyć !override. Ta funkcja wymaga v2.24.4 lub nowszej.