Docker Compose: .env, env_file i environment – różnice
Wyjaśnienie różnic między plikami .env, atrybutem env_file oraz sekcją environment w Docker Compose. Sprawdź kolejność pierwszeństwa zmiennych i dowiedz się, gdzie przechowywać sekrety.
Trzy znaczenia terminu plik env
Docker Compose posiada trzy odrębne mechanizmy o myląco podobnych nazwach. Plik .env wypełnia symbole zastępcze ${VARIABLE} wewnątrz samego compose.yaml, zanim Compose w ogóle przetworzy plik. Atrybut env_file: wczytuje plik z parami klucz/wartość do środowiska kontenera. Atrybut environment: ustawia zmienne bezpośrednio w kontenerze, zapisane w pliku compose. Nie są one zamienne, a w przypadku ustawienia tego samego klucza przez dwa z nich, zwycięzcę określa udokumentowana kolejność pierwszeństwa.
Niniejszy przewodnik pokazuje działanie każdego z nich, potwierdza pierwszeństwo za pomocą polecenia, które można uruchomić, a następnie omawia kwestię o większym znaczeniu: zmienne środowiskowe są czytelne dla każdego, kto może uruchomić docker inspect, dlatego hasła nie powinny być w nich przechowywane. Jeśli użytkownik dopiero zaczyna pracę z plikami compose, należy zacząć od Podstawy Docker Compose na VPS, a następnie wrócić tutaj w celu konfiguracji.
Plik .env służy do konfiguracji compose, a nie kontenera
Utwórz katalog i umieść w nim dwa pliki.
mkdir -p ~/envdemo && cd ~/envdemo
printf 'ALPINE_TAG=3.20\n' > .envservices:
demo:
image: alpine:${ALPINE_TAG}
command: printenv ALPINE_TAGTeraz sprawdź, co faktycznie zinterpretowało Compose.
docker compose configDane wyjściowe pokazują image: alpine:3.20. Symbol zastępczy zniknął, ponieważ interpolacja nastąpiła w momencie parsowania. Compose szuka .env w katalogu projektu, czyli w katalogu zawierającym plik compose, i podstawia każdą znalezioną wartość ${NAME}.
Następnie uruchom usługę.
docker compose run --rm demoprintenv ALPINE_TAG kończy działanie ze statusem 1 i nie wyświetla niczego. Zmienna nie istnieje wewnątrz kontenera. Jest to najczęstsze nieporozumienie: .env skonfigurowało plik compose, a nie proces. Plik .env zawierający POSTGRES_PASSWORD=hunter2 nie robi absolutnie nic dla bazy danych, chyba że jakaś część pliku compose się do niego odwołuje.
${NAME:-default} zapewnia wartość domyślną, gdy zmienna jest nieustawiona lub pusta. ${NAME:?message} sprawia, że Compose odmawia uruchomienia i wyświetla komunikat, co jest właściwym wyborem dla wartości, która nie posiada bezpiecznego ustawienia domyślnego.
env_file ładuje zmienne do kontenera
Atrybut env_file: wskazuje jeden lub więcej plików, których zawartość staje się zmiennymi środowiskowymi kontenera.
printf 'GREETING=from_env_file\nAPP_MODE=production\n' > app.envservices:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.envdocker compose run --rm demoTo polecenie wyświetla from_env_file. Format pliku to zwykłe linie KEY=value, po jednej w linii, gdzie # rozpoczyna komentarz. Nie jest to powłoka shell. Cudzysłowy są w większości przypadków zachowywane jako część wartości, a przedrostki export nie są wymagane. Nie należy umieszczać spacji wokół znaku =, ponieważ KEY = value tworzy zmienną o nazwie dosłownie KEY z wiodącą spacją w wartości.
Brak ścieżki env_file powoduje błąd i zatrzymanie działania Compose. Należy oznaczyć plik jako opcjonalny, jeśli jego brak jest dopuszczalny:
env_file:
- path: ./app.env
required: falseustawianie zmiennych środowiskowych w linii
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environmentAkceptowane są dwie składnie: powyższa forma mapowania oraz forma listy z użyciem - GREETING=from_environment. Działają one identycznie. Forma listy posiada dodatkową funkcję: klucz bez przypisanej wartości przekazuje zmienną z powłoki, w której uruchomiono docker compose.
environment:
- GREETINGGREETING=from_my_shell docker compose run --rm demoPolecenie to wypisuje from_my_shell. Uruchomienie go bez ustawienia GREETING w powłoce powoduje, że Compose nie ustawia żadnej wartości, nie generując przy tym ostrzeżenia. Warto pamiętać o tym cichym niepowodzeniu przekazywania, ponieważ usługa startująca z pustą zmienną hasła często uruchamia się poprawnie, pozostając całkowicie niezabezpieczoną.
Która wartość ma pierwszeństwo
Dokumentacja Docker określa kolejność pierwszeństwa, od najwyższego: docker compose run -e w wierszu poleceń, następnie environment lub env_file, których wartość jest interpolowana z powłoki lub pliku env, potem zwykłe environment w pliku compose, następnie env_file, a na końcu dyrektywa ENV wbudowana w obraz.
W skrócie do codziennej pracy: environment: ma pierwszeństwo przed env_file:, a -e w wierszu poleceń wygrywa z obydwoma. Można to sprawdzić w jednym pliku.
services:
demo:
image: alpine:3.20
command: printenv GREETING
env_file:
- ./app.env
environment:
GREETING: from_environmentdocker compose run --rm demo
docker compose run --rm -e GREETING=from_cli demo printenv GREETINGPierwsze polecenie wypisuje from_environment, co oznacza, że environment: nadpisało wartość w app.env. Drugie wypisuje from_cli. Żaden element w pliku compose nie nadpisuje wiersza poleceń.
Jeśli kontener zachowuje się tak, jakby konfiguracja nie została zastosowana, nie należy zgadywać. docker compose config wyświetla w pełni rozwiązaną postać pliku, a docker compose config --environment drukuje zmienne interpolacji, z których korzysta Compose. Większość zgłoszeń typu "mój plik env jest ignorowany" wynika z ustawienia wartości dwukrotnie na różnych poziomach.
Dlaczego zmienne środowiskowe wyciekają
Ustawienie hasła w environment: powoduje jego zapisanie w konfiguracji kontenera na dysku, co czyni je widocznym dla każdego użytkownika w grupie docker.
docker compose run -d --name leaky -e DB_PASSWORD=hunter2 demo sleep 300
docker inspect leaky --format '{{json .Config.Env}}'Dane wyjściowe zawierają "DB_PASSWORD=hunter2" w postaci jawnego tekstu. Trzy kolejne ścieżki ujawniają tę samą wartość. docker compose config wypisuje ją w terminalu, co prowadzi do jej wklejenia na forum wsparcia technicznego. Każdy proces wewnątrz kontenera może odczytać /proc/1/environ, a każdy proces potomny dziedziczy tę zmienną. Ponadto procedury obsługi awarii aplikacji rutynowo zrzucają całe środowisko do dziennika lub raportu o błędach.
Członkostwo w grupie docker jest równoznaczne z posiadaniem uprawnień root na hoście, więc nie jest to granica uprawnień, na której można polegać. Przewodnik dotyczący kont użytkowników o najniższych wymaganych uprawnieniach na VPS wyjaśnia, dlaczego warto ograniczyć dostęp do tej grupy na każdym współdzielonym serwerze.
Compose secrets przechowuje wartość w pliku
Compose obsługuje sekrety oparte na plikach. Wartość jest montowana w kontenerze jako plik, zamiast być wstrzykiwana do zmiennych środowiskowych.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtSekret jest montowany w /run/secrets/db_password wewnątrz kontenera. Nazwa po ukośniku to nazwa sekretu z głównego bloku secrets:.
Sufiks _FILE to konwencja stosowana przez oficjalne obrazy Docker, w tym postgres, mysql oraz mariadb. Skrypty punktu wejścia (entrypoint) sprawdzają istnienie VARNAME_FILE, odczytują plik i wykorzystują jego zawartość. Nie jest to funkcja samego Docker, więc działa tylko wtedy, gdy obraz ją implementuje. Przed założeniem, że SOMETHING_FILE zostanie obsłużone, należy sprawdzić dokumentację obrazu. Aplikacje, które tego nie wspierają, często mogą samodzielnie odczytać plik podczas uruchamiania, albo można przekazać ścieżkę i obsłużyć to we własnym skrypcie entrypoint.
Weryfikacja z wnętrza uruchomionego kontenera:
docker compose exec db cat /run/secrets/db_password
docker compose exec db printenv POSTGRES_PASSWORDPierwsze polecenie wyświetla hasło. Drugie nie wyświetla nic, ponieważ wartość nigdy nie trafiła do środowiska. Na tym polega główny cel: docker inspect dla tego kontenera pokazuje jedynie nieszkodliwą ścieżkę.
Należy chronić plik źródłowy na hoście, ponieważ sekret jest tak prywatny, jak plik, w którym się znajduje:
chmod 600 db_password.txtPragmatyczne podejście na serwerze VPS
Wiele obrazów przeznaczonych do samodzielnego hostowania nie obsługuje zmiennych _FILE, dlatego zmienne środowiskowe pozostają jedynym sposobem przekazywania danych. Na serwerze VPS zarządzanym przez jednego administratora realistycznym celem jest uniemożliwienie odczytu tych wartości przez dowolnego użytkownika systemu oraz wykluczenie ich z repozytorium git.
sudo install -o root -g root -m 600 /dev/null /etc/myapp/app.env
sudo nano /etc/myapp/app.env env_file:
- /etc/myapp/app.envPolecenie install -m 600 tworzy plik z odpowiednimi uprawnieniami od razu w momencie zapisu, co eliminuje ryzyko odczytu przez osoby niepowołane. Właścicielem pliku jest root, więc użytkownik bez uprawnień roota nie może go odczytać, choć każdy, kto może wykonać docker, nadal jest w stanie odczytać wartość z poziomu kontenera. Należy dodać *.env oraz .env do pliku .gitignore, a do repozytorium zatwierdzić jedynie plik app.env.example zawierający nazwy kluczy z pustymi wartościami. Zatwierdzone w repozytorium hasło należy uznać za skompromitowane i wymagające rotacji.
Rotacja wartości wymaga restartu usługi. Zmienne środowiskowe są odczytywane jednorazowo podczas uruchamiania procesu kontenera, więc edycja pliku nie przynosi efektu do momentu wykonania docker compose up -d --force-recreate db. Jest to ten sam schemat, który zastosowano w przewodniku n8n za HTTPS na VPS, gdzie klucz szyfrujący znajduje się poza plikiem compose.
Podział konfiguracji w zależności od środowiska
Domyślnie Compose odczytuje .env z katalogu projektu. Wskaż inną lokalizację za pomocą --env-file.
docker compose --env-file .env.staging configWiele plików jest odczytywanych w określonej kolejności, przy czym pliki wczytane później nadpisują te wcześniejsze. Przechowuj domyślne ustawienia niebędące danymi wrażliwymi w pliku podlegającym kontroli wersji, a sekrety w pliku, który nigdy nie opuszcza serwera. Ta sama zasada dotyczy env_file:, gdzie w przypadku zduplikowanego klucza pierwszeństwo ma ostatni wymieniony plik.
FAQ
Dlaczego mój plik .env jest ignorowany wewnątrz kontenera?
Nie jest ignorowany. Plik .env służy wyłącznie do podstawiania wartości w miejsce symboli zastępczych ${NAME} w pliku compose. Nigdy nie ustawia on zmiennych bezpośrednio wewnątrz kontenera. Aby przekazać wartość do kontenera, należy odwołać się do niej za pomocą environment: { KEY: "${NAME}" } lub użyć env_file: ./that-file.env.
Co ma wyższy priorytet: environment czy env_file?
environment: ma wyższy priorytet. Zgodnie z dokumentacją Docker, atrybut environment jest nadrzędny wobec env_file, a oba są niżej w hierarchii niż docker compose run -e przekazywane w linii poleceń. Jeśli klucz jest zdefiniowany w obu miejscach, wartość w env_file jest pomijana bez żadnego komunikatu.
Jak sprawdzić ostateczną wartość, której użyje Compose?
Należy wykonać docker compose config, aby wyświetlić w pełni przetworzony plik compose z uwzględnieniem wszystkich interpolacji. W przypadku uruchomionego kontenera, polecenie docker inspect <container> --format '{{json .Config.Env}}' pokazuje dokładnie to, co otrzymał jego proces.
Czy sekrety w Compose są szyfrowane?
Nie. Sekret oparty na pliku jest montowany w kontenerze jako zwykły plik w lokalizacji /run/secrets/<name>, a plik źródłowy znajduje się na dysku hosta w postaci niezaszyfrowanej. Korzyścią jest ograniczenie zakresu, a nie szyfrowanie: wartość nie trafia do środowiska kontenera, nie pojawia się w danych wyjściowych docker inspect oraz nie jest obecna w zrzutach pamięci po awarii, które zawierają zmienne środowiskowe.
Czy w pliku env można używać cudzysłowów i spacji?
Należy użyć KEY=value with spaces i pominąć cudzysłowy. Compose traktuje całą resztę linii jako wartość, więc cudzysłowy zazwyczaj stają się dosłownymi znakami wewnątrz tej wartości. Nigdy nie należy dodawać spacji wokół =, ponieważ klucz otrzyma wtedy spację na końcu i żadne dopasowanie nie zadziała.