Docker Compose wiele plików: jak poprawnie łączyć pliki
Dowiedz się jak Docker Compose scala pliki konfiguracyjne. Wyjaśniamy kolejność łączenia, automatyczne ładowanie compose.override.yaml oraz pułapki przy mapowaniu portów.
Jak Compose obsługuje wiele plików
Docker Compose potrafi zbudować jeden projekt z kilku plików. Odczytuje je w kolejności otrzymania i scala w jeden model, przy czym późniejszy plik nadpisuje wartości w przypadku konfliktu. Dwa mechanizmy realizują to z poziomu wiersza poleceń: plik override, który Compose ładuje automatycznie, oraz flaga -f przekazywana ręcznie. Trzeci mechanizm znajduje się wewnątrz samego pliku, jest to element include, który działa inaczej niż oba poprzednie.
Scalanie nie jest prostym nadpisywaniem. Mapowania są łączone klucz po kluczu, sekwencje są dołączane, a niewielki zestaw pól jest zastępowany w całości. Ta różnica jest źródłem niespodzianek, a lista ports jest tym, co sprawia trudności niemal każdemu.
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 nie przygotowano jeszcze pliku Compose, należy zacząć od przewodnika po podstawach Docker Compose i wrócić tutaj.
Plik override ładowany przez Compose automatycznie
Uruchom docker compose up bez flagi -f, a Compose przeszuka bieżący katalog oraz katalogi nadrzędne w poszukiwaniu compose.yaml lub docker-compose.yaml. Jeśli plik override znajduje się obok pliku bazowego, Compose załaduje go jako drugi, automatycznie.
ls compose.yaml compose.override.yaml
docker compose up -dObecność obu plików jest równoznaczna z ręcznym wskazaniem ich w poleceniu.
docker compose -f compose.yaml -f compose.override.yaml up -dNazwy 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, jest ładowana tylko wtedy, gdy zostanie wskazana za pomocą -f.
W momencie przekazania choćby jednej flagi -f, automatyczne ładowanie zostaje wstrzymane. docker compose -f compose.yaml up odczytuje dokładnie ten jeden plik i ignoruje plik override; na tej właściwości opiera się wzorzec środowisk deweloperskich i produkcyjnych opisany w dalszej części tego przewodnika.
Działa to w obie strony na serwerze. Plik nadpisujący pozostawiony w katalogu wdrożenia jest wczytywany przez każde podstawowe polecenie docker compose uruchomione z tego katalogu, w tym przez polecenie wykonywane przez zadanie cron. W ten sposób stos produkcyjny zaczyna montować za pomocą bind mountu katalog źródłowy, którego nikt nie zamierzał wdrażać. Po każdym wdrożeniu uruchom docker compose config i odczytaj wynik. Gdy wdrożenie jest wykonywane bez nadzoru, kontrola pomaga tylko wtedy, gdy ktoś zostanie poinformowany o błędzie. Służy do tego kanał push, taki jak samodzielnie hostowany serwer ntfy, do którego zadanie cron lub jednostka systemd OnFailure może wysłać powiadomienie.
Kolejność plików z -f oraz rozwiązywanie ścieżek względnych
Compose buduje konfigurację w kolejności podanych plików, przy czym kolejne pliki nadpisują lub uzupełniają poprzednie. W przypadku konfliktów, ostatni plik ma pierwszeństwo.
docker compose -f compose.yaml -f compose.prod.yaml config
docker compose -f compose.yaml -f compose.prod.yaml up -dKażde polecenie w ramach projektu wymaga identycznej listy plików. Uruchomienie up z dwoma plikami, a następnie logs z jednym, powoduje odwołanie się do innego scalonego modelu, co jest szybkim sposobem na uzyskanie błędu o nieistniejącej usłudze. Ryzyko rośnie w przypadku stosów, których aktualizacje są uruchamiane jako jednorazowe polecenia, na przykład podczas migracji bazy danych w samodzielnie hostowanym systemie wsparcia Chatwoot, gdzie docker compose run wywołane z błędną listą plików po cichu odnosi się do innego modelu niż ten, z którego korzystają działające usługi. Zamiast tego należy ustawić listę raz, używając zmiennej środowiskowej COMPOSE_FILE.
export COMPOSE_FILE=compose.yaml:compose.prod.yaml
docker compose config
docker compose up -dSeparator to : w systemie Linux, a COMPOSE_PATH_SEPARATOR pozwala na jego zmianę. Zmienną COMPOSE_FILE można również umieścić w pliku .env projektu, co sprawia, że staje się ona częścią repozytorium, a nie tylko historii powłoki. Wartości ustawione jawnie w wierszu poleceń mają pierwszeństwo przed zmienną środowiskową.
Oto zasada, która wpływa na montowanie wolumenów (bind mounts). W przypadku użycia wielu plików z -f, wszystkie ścieżki względne we wszystkich tych plikach są rozwiązywane względem katalogu pierwszego pliku, a nie pliku, w którym się znajdują. Zapisanie ./data:/var/lib/postgresql/data wewnątrz deploy/prod/compose.prod.yaml spowoduje, że Compose nadal będzie szukać ./data w lokalizacji pliku bazowego. Docker utworzy wówczas pusty katalog w błędnej ścieżce, a kontener uruchomi się bez danych, co wygląda jak ich utrata, choć w rzeczywistości tak nie jest. Należy przekazać --project-directory, aby samodzielnie ustawić ścieżkę bazową, lub użyć include, co powoduje rozwiązywanie ścieżek w każdym pliku względem jego własnego katalogu.
Nazwa projektu pochodzi z tego samego katalogu bazowego, więc zmiana pierwszego pliku na liście może spowodować zmianę nazwy projektu. Zmiana nazwy projektu oznacza nowe nazwy kontenerów i wolumenów, podczas gdy stary wolumen pozostaje na dysku pod starą nazwą. Należy przypiąć nazwę za pomocą parametru najwyższego poziomu name: w pliku bazowym.
name: myappKtóre pola są scalane, a które zastępowane
Compose scala wartości na podstawie typu pola, a nie jego nazwy.
- Pola jednowartościowe są zastępowane.
image,command,entrypointorazmem_limitprzyjmują późniejszą wartość w całości. Nie można dopisać argumentu docommand, ponieważ nadpisanie powoduje zastąpienie całej linii. - Mapowania są scalane klucz po kluczu.
environment,labels,volumesorazdeviceszachowują wszystkie klucze z obu plików, a w przypadku wystąpienia tego samego klucza w obu plikach, pierwszeństwo ma plik późniejszy. Dlaenvironmentorazlabelskluczem jest nazwa zmiennej lub etykiety. Dlavolumesorazdeviceskluczem jest ścieżka w kontenerze. - Sekwencje są dołączane.
dns,dns_search,expose,tmpfsorazexternal_linkssą łączone. Plik bazowy zawierającyexpose: ["3000"]scalony z nadpisaniem zawierającym["4000", "5000"]tworzy["3000", "4000", "5000"].
Cztery sekwencje posiadają klucz identyfikujący, dzięki czemu wpisy pasujące do tego klucza są scalane zamiast dołączane. volumes, secrets oraz configs są dopasowywane na podstawie target. ports jest dopasowywane na podstawie kombinacji ip, target, published oraz protocol.
Przeczytaj tę zasadę ports dwukrotnie, ponieważ jest to pułapka. Dwa wpisy portów są tym samym wpisem tylko wtedy, gdy wszystkie cztery elementy są zgodne. Zmiana któregokolwiek z nich powoduje, że Compose traktuje je jako dwa niezależne porty i zachowuje oba.
Dlaczego port pozostaje opublikowany mimo nadpisania
Plik bazowy, który publikuje usługę na wszystkich interfejsach:
services:
web:
image: nginx:1.27
ports:
- "8080:80"Nadpisanie przygotowane w celu powiązania usługi wyłącznie z localhost, ponieważ przed nią znajduje się reverse proxy:
services:
web:
ports:
- "127.0.0.1:8080:80"Sprawdź wynik przed założeniem, że konfiguracja zadziałała.
docker compose -f compose.yaml -f compose.prod.yaml configOba wpisy znajdują się w danych wyjściowych. Część ip różni się, 0.0.0.0 kontra 127.0.0.1, więc z punktu widzenia procesu łączenia są to dwa różne porty, a publiczne powiązanie, które próbowano usunąć, nadal istnieje w modelu. Ma to większe znaczenie w Docker niż gdziekolwiek indziej, ponieważ opublikowany port jest zapisywany w iptables przed regułami firewalla. Mechanizm ten opisano w dlaczego opublikowane porty Docker omijają ufw.
Istnieją dwie poprawki. Jawna metoda wykorzystuje znacznik !override, który zastępuje cały atrybut i pomija reguły łączenia:
services:
web:
ports: !override
- "127.0.0.1:8080:80"!override wymaga Compose w wersji 2.24.4 lub nowszej. Przenośna poprawka nie wymaga żadnego znacznika: należy całkowicie usunąć ports z pliku bazowego i deklarować go wyłącznie w plikach specyficznych dla danego środowiska. Brak elementów do połączenia oznacza brak wycieku konfiguracji. Jest to wzorzec zastosowany w poniższym przykładzie.
Usuwanie wartości z pliku bazowego
!reset usuwa atrybut, przywracając go do wartości domyślnej lub ustawiając na null. Polecenie wymaga podania wartości, która jest ignorowana, dlatego należy wpisać dowolny poprawny, pusty ciąg.
services:
web:
ports: !reset []
environment:
DEBUG: !reset null!reset wymaga Compose w wersji 2.24 lub nowszej. Należy z niego korzystać, gdy plik bazowy nie podlega edycji, na przykład w przypadku fragmentu dostarczonego przez zewnętrznego dostawcę. Opublikowany stos upstream jest właśnie takim przypadkiem: plik Compose dla samodzielnie hostowanego obszaru roboczego AFFiNE deklaruje cztery kontenery, których nie jesteś autorem, a !reset pozwala wyczyścić jeden atrybut w jednym z nich bez konieczności tworzenia forka pliku i przejmowania obowiązku śledzenia jego zmian.
include, dla stosów złożonych z części
include wczytuje inną aplikację Compose do Twojego modelu. Jest to element najwyższego poziomu, a nie flaga.
include:
- path: ../commons/compose.yamlKażda ścieżka w include jest ładowana jako osobny model aplikacji Compose z własnym katalogiem projektu, więc ścieżki relatywne wewnątrz tego pliku są rozwiązywane względem jego własnego katalogu. To stanowi główną różnicę względem -f i jest powodem, dla którego include jest właściwym narzędziem, gdy fragment znajduje się w innym folderze lub innym repozytorium. Jest to typowy układ dla zewnętrznego stosu, którego nie jesteś autorem: wielousługowy plik Compose dla samodzielnej instalacji Authentik SSO może znajdować się w swoim własnym katalogu z zachowaniem poprawności ścieżek relatywnych, podczas gdy Twój plik pozostaje skupiony na Twoich własnych usługach.
Dłuższa forma przyjmuje podopcje.
include:
- path:
- ../monitoring/compose.yaml
- ../monitoring/compose.vps.yaml
project_directory: ../monitoring
env_file: ../monitoring/.envpath przyjmuje listę, a pliki te są scalane zgodnie ze standardowymi regułami, zanim wynik dołączy do Twojego modelu. project_directory ustawia ścieżkę bazową używaną do rozwiązywania ścieżek relatywnych w dołączonym pliku. env_file nadaje dołączonemu plikowi własne zmienne do interpolacji, co zapobiega sytuacji, w której współdzielony fragment cicho odczytuje plik .env Twojego projektu. include wymaga Compose w wersji 2.20.0 lub nowszej. Te same opcje sprawdzają się w przypadku dodatku z pojedynczym kontenerem do stosu, który już uruchamiasz, na przykład Halcyon, który zmienia wygląd biblioteki Jellyfin na styl wypożyczalni kaset z lat 90.: jego plik zachowuje własny tag obrazu i własne env_file, więc aktualizacja nigdy nie wymaga ingerencji w plik, w którym znajduje się Twój stos multimedialny.
Duplikaty nazw zasobów między Twoim plikiem a plikiem dołączonym są zgłaszane jako błąd, zamiast być cicho scalane, co jest działaniem celowym. Aby zmienić coś, co deklaruje dołączony plik, umieść zmianę w compose.override.yaml: nadpisanie jest stosowane do złożonego modelu, więc może modyfikować dołączone zasoby bez powodowania konfliktów. Ten nawyk najbardziej opłaca się w przypadku stosu, którego plik źródłowy jest nadpisywany przy każdej wersji, tak jak w przypadku wielokontenerowych serwerów zdjęć porównywanych w PhotoPrism kontra Immich, gdzie powiązanie z localhost lub dodatkowy wolumen powinny znaleźć się w Twoim nadpisaniu, a nie w pliku, który zostanie zastąpiony przy następnej aktualizacji.
W skrócie: include łączy oddzielne aplikacje, -f nakłada konfigurację na jedną aplikację.
Podział na środowisko deweloperskie i produkcyjne na jednym VPS
Poniżej przedstawiono kompletny wzorzec w trzech plikach. Plik bazowy deklaruje konfigurację wspólną dla obu środowisk 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 bazę danych, która faktycznie odpowiada, a nie tylko na kontener, który istnieje; szczegóły opisano w healthchecks i warunki depends_on. Wartość POSTGRES_PASSWORD jest interpolowana z pliku .env projektu, który nigdy nie powinien trafiać do repozytorium git. Zobacz pliki env i sekrety Compose, aby poznać bezpieczniejsze warianty.
Następnie compose.override.yaml, który Compose wczytuje automatycznie. Jest to plik przeznaczony dla programisty.
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 polecenie docker compose up łączy oba te pliki. Wartość command zastępuje domyślny obraz, ponieważ jest to wartość pojedyncza. Wartość LOG_LEVEL zastępuje info, ponieważ environment scala konfigurację według klucza. Montowanie typu bind mount oraz dwa opublikowane porty są czystymi dodatkami, a port bazy danych jest powiązany z localhost, dzięki czemu laptop w sieci współdzielonej nie udostępnia PostgreSQL całemu otoczeniu.
Na koniec compose.prod.yaml. Jego nazwa nie jest domyślnie wyszukiwana przez Compose, więc nie zostanie wczytany przypadkowo.
services:
app:
ports:
- "127.0.0.1:8000:3000"
deploy:
resources:
limits:
memory: 512MNa serwerze VPS należy wskazać oba pliki, a samo ich wywołanie wyklucza użycie pliku 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 psPolecenie ps powinno wyświetlić obie usługi jako uruchomione, a db powinno wskazywać (healthy). Ponieważ przekazano -f, plik compose.override.yaml nie został odczytany, więc polecenie deweloperskie, montowanie źródeł przez bind mount oraz publiczny port 3000 nie mają wpływu na produkcję, mimo że plik znajduje się w tym samym katalogu. Port 8000 jest dostępny tylko na localhost, gotowy do obsługi przez proxy: zobacz uruchamianie kilku aplikacji za Traefik, gdy dodasz drugą usługę.
Ustaw COMPOSE_FILE=compose.yaml:compose.prod.yaml w pliku .env na serwerze, a pozostałe polecenia wrócą do standardowej formy docker compose logs -f app.
Stos pojedynczej usługi zyskuje na takim układzie, ponieważ samodzielnie hostowany tracker treningów openGym musi odpowiadać przez TLS za proxy, zanim zarejestrujesz pierwszy klucz dostępu (passkey), a plik bazowy bez sekcji ports zapobiega sytuacji, w której przypadkowe publiczne powiązanie portu wyprzedzi proxy w obsłudze ruchu.
Przejrzyj scalony model przed wdrożeniem
docker compose config wyświetla w pełni scalony i w pełni zinterpolowany model. Nie jest to podgląd. Jest to dokładna postać danych wejściowych, na których operuje Compose, więc jeśli wynik nie zgadza się z oczekiwaniami, to wynik jest poprawny.
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} w formie nierozwiniętej. Użyj tego parametru przed wklejeniem wyniku w jakiekolwiek miejsce, ponieważ standardowe config wypisuje każdy rozwiązany sekret otwartym tekstem. --services wyświetla tylko nazwy usług, co stanowi szybki sposób na potwierdzenie, że include załadowało oczekiwane elementy.
Tryby awarii i ich objawy
no configuration file provided: not found. Narzędzie Compose nie znalazło pliku do odczytu. Użytkownik znajduje się poza katalogiem projektu lub COMPOSE_FILE wskazuje na nieistniejącą ścieżkę. Compose przeszukuje katalogi nadrzędne w poszukiwaniu domyślnego pliku bazowego, jednak nie przeszukuje żadnych lokalizacji w poszukiwaniu pliku wskazanego ręcznie.
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. Interpolacja jest rozwiązywana względem pliku .env projektu oraz środowiska powłoki, a katalogiem projektu jest katalog pierwszego pliku -f. Wdrożenie z katalogu innego niż ten, w którym znajduje się .env, powoduje wyświetlenie ostrzeżenia, a następnie odrzucanie wszystkich połączeń przez bazę danych.
Edycja nadpisania nie jest widoczna w docker compose config. Przekazano flagę -f, która wyłącza automatyczne ładowanie nadpisań, lub Compose znalazło compose.yaml w katalogu nadrzędnym, a plik nadpisania nie znajduje się w tym samym miejscu. Uruchomienie docker compose config bez dodatkowych argumentów pozwala sprawdzić, jaki model jest faktycznie budowany przez Compose.
Montowanie typu bind jest puste, a Docker utworzył niepożądany katalog. Ścieżka względna została rozwiązana względem katalogu pierwszego pliku. Należy poprawić ścieżkę, przekazać --project-directory lub przenieść fragment za include.
Kontenery wracają z nowymi nazwami, a wolumen wydaje się pusty. Nazwa projektu uległa zmianie, ponieważ jest ona powiązana z katalogiem pierwszego pliku. Dodanie parametru name: najwyższego poziomu do pliku bazowego ustabilizuje nazewnictwo. Stary wolumen nadal istnieje pod poprzednim prefiksem i można go wyświetlić za pomocą docker volume ls.
Port usunięty w nadpisaniu pozostaje otwarty. Scalanie ports spowodowało dołączenie zamiast zastąpienia. Należy potwierdzić konfigurację za pomocą docker compose config, a następnie użyć !override lub przenieść ports poza plik bazowy.
FAQ
Czy Compose automatycznie wczytuje plik compose.override.yaml?
Tak, w przypadku uruchomienia docker compose bez flagi -f. Compose przeszukuje bieżący katalog oraz katalogi nadrzędne w poszukiwaniu compose.yaml lub docker-compose.yaml. Jeśli w tym samym miejscu znajduje się plik nadpisujący, jest on wczytywany jako drugi. Rozpoznawane nazwy to compose.override.yaml, compose.override.yml, docker-compose.override.yml oraz docker-compose.override.yaml. Użycie dowolnej flagi -f wyłącza ten mechanizm, więc docker compose -f compose.yaml up odczytuje tylko jeden plik.
W jakiej kolejności scalane są pliki przekazane przez -f?
Od lewej do prawej. Compose buduje konfigurację w kolejności podania plików, a każdy kolejny plik nadpisuje lub uzupełnia poprzednie. W przypadku konfliktu pierwszeństwo ma ostatni plik w linii. Dla każdego polecenia w ramach danego projektu należy używać tej samej listy plików, do czego służy COMPOSE_FILE=compose.yaml:compose.prod.yaml.
Dlaczego port jest nadal publikowany po nadpisaniu konfiguracji?
Ponieważ wpisy ports są identyfikowane przez pełny zestaw ip, target, published oraz protocol. Nadpisanie 127.0.0.1:8080:80 w odniesieniu do bazy 8080:80 różni się w części ip, więc Compose traktuje to jako drugi port i zachowuje oba. Uruchom docker compose config, aby wyświetlić oba wpisy. Użyj ports: !override w wersji Compose v2.24.4 lub nowszej albo usuń ports z pliku bazowego, aby nie było czego scalać.
Jaka jest różnica między include a -f?
-f nakłada kilka plików na jedną aplikację, a każda ścieżka relatywna w każdym pliku jest rozwiązywana względem katalogu pierwszego pliku. include dołącza oddzielną aplikację Compose, a każda dołączona ścieżka zachowuje własny katalog projektu, więc ścieżki relatywne są rozwiązywane względem niej samej. Używaj -f dla warstw środowiskowych własnego stosu, a include dla fragmentów utrzymywanych zewnętrznie. include wymaga Compose w wersji v2.20.0 lub nowszej.
Jak usunąć wartość ustawioną w pliku bazowym?
Użyj znacznika !reset w Compose w wersji v2.24 lub nowszej. Wpisz ports: !reset [] lub MY_VAR: !reset null w pliku nadpisującym, a atrybut powróci do wartości domyślnej lub zostanie ustawiony na null. Wartość przypisana do znacznika jest wymagana, ale ignorowana. Jeśli zamiast usuwania atrybutu chcesz go zastąpić, użyj !override, co wymaga wersji v2.24.4 lub nowszej.