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 elementy określane mianem pliku env
Docker Compose posiada trzy odrębne mechanizmy o myląco podobnych nazwach. Plik .env uzupełnia symbole zastępcze ${VARIABLE} wewnątrz samego compose.yaml, po odczytaniu pliku YAML przez Compose, a przed walidacją lub uruchomieniem czegokolwiek. Atrybut env_file: ładuje plik z parami klucz/wartość do środowiska kontenera. Atrybut environment: ustawia zmienne bezpośrednio w kontenerze, zapisane w pliku compose. Mechanizmy te nie są zamienne, a w przypadku ustawienia tego samego klucza przez dwa z nich, zwycięzca jest określony przez udokumentowaną kolejność pierwszeństwa.
Niniejszy przewodnik pokazuje działanie każdego z nich, dowodzi pierwszeństwa za pomocą polecenia, które można wykonać, 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 powrócić tutaj w celu konfiguracji.
Plik .env służy do konfiguracji pliku compose, a nie kontenera
Należy utworzyć katalog i umieścić 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 należy sprawdzić, jak Compose zinterpretował plik.
docker compose configDane wyjściowe pokazują image: alpine:3.20. Symbol zastępczy zniknął, ponieważ interpolacja nastąpiła w momencie wczytywania pliku przez Compose. Compose szuka pliku .env w katalogu projektu: katalogu wskazanym przez --project-directory (jeśli został ustawiony), w przeciwnym razie w katalogu pierwszego pliku przekazanego za pomocą -f, a w ostateczności w bieżącym katalogu roboczym. Narzędzie podstawia symbole zastępcze ${NAME} znalezione w wartościach YAML. Klucze nigdy nie podlegają interpolacji, a podwójny znak $$ generuje dosłowny znak dolara zamiast podstawienia.
Następnie należy uruchomić usługę.
docker compose run --rm demoprintenv ALPINE_TAG kończy działanie z kodem wyjścia 1 i nie wyświetla żadnych komunikatów. 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 wywiera żadnego wpływu na bazę danych, dopóki jakikolwiek element pliku compose nie odwoła się do tej zmiennej.
${NAME:-default} zapewnia wartość domyślną, gdy zmienna jest nieustawiona lub pusta. ${NAME:?message} powoduje, że Compose odmawia uruchomienia i wyświetla zdefiniowany komunikat, co jest właściwym rozwiązaniem w przypadku wartości, dla których nie istnieje bezpieczna wartość domyślna.
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 wierszu, gdzie # rozpoczyna komentarz. Nie jest to powłoka systemowa, chociaż Compose przetwarza plik zgodnie z kilkoma regułami przypominającymi działanie powłoki. Cudzysłowy otaczające wartość są usuwane, więc KEY="value" oraz KEY=value dają ten sam wynik. Wartości ujęte w podwójny cudzysłów oraz wartości bez cudzysłowów mają interpolowane symbole zastępcze ${...}, natomiast wartości w pojedynczym cudzysłowie są traktowane dosłownie. Prefiks export jest akceptowany, ale nie jest wymagany. Spacje wokół znaku = są usuwane. Należy uważać na komentarze wewnątrz linii: w wartości bez cudzysłowów spacja poprzedzająca # rozpoczyna komentarz, dlatego każdą wartość zawierającą # należy ująć w cudzysłów.
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: falsezmienne środowiskowe ustawiane 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. Obie posiadają dodatkową funkcję: klucz bez wartości przekazuje zmienną z powłoki, w której uruchomiono docker compose. W formie listy jest to sam - GREETING, a w formie mapowania GREETING: bez wartości po dwukropku.
environment:
- GREETINGGREETING=from_my_shell docker compose run --rm demoTo polecenie wypisuje from_my_shell. Uruchomienie go bez ustawienia GREETING w powłoce powoduje, że Compose nie ustawia niczego, nie generując przy tym ostrzeżenia. Warto pamiętać o cichym niepowodzeniu przekazywania zmiennych, 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 jawnej. Trzy kolejne ścieżki ujawniają tę samą wartość. docker compose config wypisuje ją w terminalu, co prowadzi do jej wklejenia na forum wsparcia technicznego. Procesy potomne dziedziczą zmienną domyślnie, chyba że proces nadrzędny celowo uruchomi je ze zmienionym środowiskiem, a proces działający z tym samym użytkownikiem co PID 1 lub posiadający CAP_SYS_PTRACE może odczytać ją z /proc/1/environ. W kontenerze, w którym wszystko działa jako root, dotyczy to każdego procesu. Aplikacja działająca jako użytkownik bez uprawnień, podczas gdy PID 1 działa jako root, otrzyma błąd uprawnień przy próbie dostępu do tego pliku, mimo że nadal przechowuje zmienną we własnym środowisku. 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ń roota na hoście, więc nie jest to granica uprawnień, na której można polegać. Przewodnik dotyczący kont użytkowników z ograniczonymi uprawnieniami na VPS wyjaśnia, dlaczego warto ograniczyć tę grupę 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 plik .env jest ignorowany wewnątrz kontenera?
Nie jest ignorowany. Plik .env podstawia wartości w miejsce symboli zastępczych ${NAME} w pliku compose i może również ustawiać zmienne samego Compose, takie jak COMPOSE_PROJECT_NAME lub COMPOSE_FILE. Nie ustawia on zmiennych wewnątrz kontenera. Aby przekazać wartość do kontenera, należy się do niej odwołać: environment: { KEY: "${NAME}" } lub użyć env_file: ./that-file.env.
Co ma wyższy priorytet: environment czy env_file?
environment: wygrywa. Zgodnie z dokumentacją Docker, atrybut environment ma pierwszeństwo przed atrybutem env_file, a oba są niżej w hierarchii niż docker compose run -e w wierszu poleceń. Jeśli klucz jest ustawiony w obu miejscach, wartość w env_file jest pomijana bez komunikatu.
Jak sprawdzić ostateczną wartość, której użyje Compose?
Należy uruchomić docker compose config, aby wyświetlić w pełni przetworzony plik compose ze wszystkimi zastosowanymi interpolacjami. W przypadku kontenera, który już działa, 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 /run/secrets/<name>, a plik źródłowy znajduje się na dysku hosta w postaci niezaszyfrowanej. Korzyścią jest zakres, a nie szyfrowanie: wartość pozostaje poza środowiskiem kontenera, poza danymi wyjściowymi docker inspect oraz poza zrzutami pamięci po awarii, które zawierają zmienne środowiskowe.
Czy w pliku env można używać cudzysłowów i spacji?
Tak. Compose usuwa otaczające cudzysłowy, więc KEY="value with spaces" i KEY=value with spaces dają ten sam wynik value with spaces. Należy używać pojedynczych cudzysłowów, gdy wartość zawiera $, który nie powinien być interpolowany, oraz ująć w cudzysłów każdą wartość zawierającą #, ponieważ bez cudzysłowu znak ten rozpoczyna komentarz. Spacje wokół = są usuwane, więc KEY = value nadal ustawia KEY.