Docker Compose: разница между .env, env_file и environment
Разберитесь в отличиях .env, env_file и environment в Docker Compose. Узнайте приоритеты переменных, почему нельзя хранить пароли в окружении и как правильно использовать secrets.
Три сущности, которые называют env-файлом
Docker Compose использует три различных механизма с похожими названиями. Файл .env заполняет плейсхолдеры ${VARIABLE} внутри самого compose.yaml после того, как Compose считывает YAML, но до проверки или запуска чего-либо. Атрибут env_file: загружает файл с парами ключ/значение в окружение контейнера. Атрибут environment: задает переменные непосредственно для контейнера, прописанные в файле compose. Они не являются взаимозаменяемыми, и если два из них задают один и тот же ключ, приоритет определяется задокументированным порядком.
В этом руководстве показана работа каждого из них, доказан приоритет с помощью команды, которую вы можете запустить, а затем рассмотрена более важная часть: переменные окружения доступны любому, кто может запустить docker inspect, поэтому пароли в них хранить нельзя. Если вы новичок в работе с файлами compose в целом, начните с Основы Docker Compose на VPS и вернитесь сюда для настройки конфигурации.
Файл .env предназначен для compose-файла, а не для контейнера
Создайте каталог и поместите в него два файла.
mkdir -p ~/envdemo && cd ~/envdemo
printf 'ALPINE_TAG=3.20\n' > .envservices:
demo:
image: alpine:${ALPINE_TAG}
command: printenv ALPINE_TAGТеперь узнайте, что именно интерпретировал Compose.
docker compose configВывод показывает image: alpine:3.20. Заполнитель исчез, так как интерполяция произошла во время загрузки файла программой Compose. Compose ищет .env в рабочем каталоге проекта: каталоге, заданном через --project-directory (если он установлен), в противном случае — в каталоге первого файла, переданного через -f, или в текущем рабочем каталоге. Он подставляет найденные в значениях YAML заполнители ${NAME}. Ключи никогда не подвергаются интерполяции, а удвоенный $$ дает литеральный знак доллара вместо подстановки.
Затем запустите сервис.
docker compose run --rm demoprintenv ALPINE_TAG завершается со статусом 1 и ничего не выводит. Переменная не существует внутри контейнера. Это самое распространенное заблуждение: .env настраивает compose-файл, а не процесс. Файл .env, содержащий POSTGRES_PASSWORD=hunter2, никак не влияет на вашу базу данных, если только какая-либо часть compose-файла не ссылается на него.
${NAME:-default} обеспечивает резервное значение, если переменная не задана или пуста. ${NAME:?message} заставляет Compose отказаться от запуска и вывести ваше сообщение, что является верным решением для параметра, у которого нет безопасного значения по умолчанию.
env_file загружает переменные в контейнер
Атрибут env_file: указывает один или несколько файлов, содержимое которых становится переменными окружения контейнера.
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 demoЭто выводит from_env_file. Формат файла — обычные строки KEY=value, по одной на строку, где # начинает комментарий. Это не shell, хотя Compose обрабатывает файл по нескольким правилам, похожим на shell. Окружающие кавычки удаляются, поэтому KEY="value" и KEY=value дают одинаковый результат. В значениях, заключенных в двойные кавычки или без кавычек, подставляются переменные ${...}, тогда как значения в одинарных кавычках используются буквально. Префикс export допустим, но не обязателен. Пробелы вокруг знака = удаляются. Важный нюанс — встроенные комментарии: в значении без кавычек пробел перед # начинает комментарий, поэтому заключайте в кавычки любое значение, содержащее #.
Отсутствие пути env_file является ошибкой, и Compose останавливает работу. Пометьте файл как необязательный, если его отсутствие является допустимым:
env_file:
- path: ./app.env
required: falseУстановка переменных окружения в строке
services:
demo:
image: alpine:3.20
command: printenv GREETING
environment:
GREETING: from_environmentПоддерживаются два синтаксиса: форма отображения (mapping), показанная выше, и списочная форма с использованием - GREETING=from_environment. Они работают идентично. Оба варианта имеют одну дополнительную особенность: ключ без значения передает переменную из оболочки, в которой вы запустили docker compose. В списочной форме это просто - GREETING, а в форме отображения — GREETING: без указания значения после двоеточия.
environment:
- GREETINGGREETING=from_my_shell docker compose run --rm demoЭта команда выводит from_my_shell. Если запустить её, не установив GREETING в оболочке, Compose не установит ничего и не выдаст предупреждение. Стоит учитывать возможность таких «тихих» сбоев при передаче переменных, так как сервис, запущенный с пустой переменной пароля, часто успешно стартует, оставаясь при этом полностью открытым.
Приоритет конфигурации
Docker определяет порядок приоритета, начиная с самого высокого: docker compose run -e в командной строке, затем environment или env_file, значение которых берется из вашей оболочки или файла переменных окружения, затем обычное environment в файле compose, затем env_file, и в конце директива ENV, встроенная в образ.
Кратко для повседневной работы: environment: имеет приоритет над env_file:, а -e в командной строке перекрывает их оба. Проверим это в одном файле.
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 GREETINGПервая команда выводит from_environment, так как environment: переопределило значение в app.env. Вторая команда выводит from_cli. Никакие параметры в файле compose не переопределяют значения из командной строки.
Если контейнер ведет себя так, будто ваша конфигурация не была применена, не пытайтесь угадать причину. docker compose config выводит полностью разрешенный файл, а docker compose config --environment показывает переменные интерполяции, которые использует Compose. Большинство жалоб на то, что «файл переменных окружения игнорируется», связаны с тем, что значение задано дважды на разных уровнях.
Почему переменные окружения раскрывают данные
Установите пароль в environment:, и он сохранится в конфигурации контейнера на диске, где его сможет увидеть любой пользователь из группы docker.
docker compose run -d --name leaky -e DB_PASSWORD=hunter2 demo sleep 300
docker inspect leaky --format '{{json .Config.Env}}'Вывод содержит "DB_PASSWORD=hunter2" в открытом виде. Еще три пути раскрывают это же значение. docker compose config выводит его в терминал, откуда оно часто попадает в сообщения на форумах поддержки. Дочерние процессы по умолчанию наследуют переменные окружения, если только родительский процесс намеренно не запускает их с измененным окружением. Процесс, запущенный от того же пользователя, что и PID 1, или обладающий правами CAP_SYS_PTRACE, может прочитать эти данные из /proc/1/environ. В контейнере, где всё работает от имени root, это касается любого процесса. Если приложение работает от непривилегированного пользователя, а PID 1 — от root, при попытке доступа к этому файлу возникнет ошибка прав, хотя переменная всё равно останется в окружении самого приложения. Кроме того, обработчики аварийного завершения приложений часто записывают всё окружение в лог или отчет об ошибке.
Членство в группе docker фактически равносильно правам root на хосте, поэтому на эту границу привилегий полагаться нельзя. В руководстве по использованию учетных записей с минимальными привилегиями на VPS объясняется, почему доступ к этой группе следует ограничивать на любом общем сервере.
Хранение секретов Compose в файле
Compose поддерживает секреты на основе файлов. Значение монтируется в контейнер как файл, а не передается через переменные окружения.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./db_password.txtСекрет монтируется по пути /run/secrets/db_password внутри контейнера. Имя после косой черты соответствует имени секрета из блока верхнего уровня secrets:.
Суффикс _FILE — это соглашение, используемое официальными образами Docker, включая postgres, mysql и mariadb. Скрипты точки входа (entrypoint) в этих образах проверяют наличие VARNAME_FILE, считывают файл и используют его содержимое. Это не является функцией Docker, поэтому данный механизм работает только в тех образах, где он реализован. Перед использованием проверьте документацию образа, чтобы убедиться, что SOMETHING_FILE будет обработан. Приложения, которые не поддерживают этот механизм, часто могут самостоятельно прочитать файл при запуске, либо вы можете передать путь к файлу и обработать его в собственном скрипте точки входа.
Проверьте содержимое изнутри запущенного контейнера:
docker compose exec db cat /run/secrets/db_password
docker compose exec db printenv POSTGRES_PASSWORDПервая команда выводит пароль. Вторая команда ничего не выводит, так как значение не было передано в переменные окружения. В этом и заключается основная цель: docker inspect для этого контейнера отображает только безопасный путь.
Защитите исходный файл на хосте, так как уровень конфиденциальности секрета ограничен уровнем доступа к файлу, в котором он хранится:
chmod 600 db_password.txtПрагматичный подход к настройке VPS
Многие образы для самостоятельного хостинга не поддерживают переменные _FILE, поэтому единственным способом передачи конфигурации остаются переменные окружения. На VPS с одним администратором реалистичная цель состоит в том, чтобы значения не хранились в файлах с правами на чтение для всех пользователей в каталоге проекта и не попадали в 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.envКоманда install -m 600 создает файл с уже установленными правами доступа, поэтому не возникает промежутка времени, когда он доступен для чтения всем. Владельцем файла является root, поэтому непривилегированный пользователь на сервере не может его прочитать, хотя любой, кто может выполнить docker, все равно способен извлечь значение из контейнера. Добавьте *.env и .env в .gitignore, а в репозиторий закоммитьте app.env.example, содержащий только имена ключей с пустыми значениями. Закоммиченный пароль считается скомпрометированным и подлежит смене.
Смена значения требует перезапуска службы. Переменные окружения считываются один раз при запуске процесса контейнера, поэтому редактирование файла не дает эффекта до выполнения docker compose up -d --force-recreate db. Этот шаблон используется в руководстве n8n за HTTPS на VPS, где ключ шифрования хранится вне файла compose.
Разделение конфигурации по окружениям
По умолчанию Compose считывает .env из директории проекта. Укажите другой путь с помощью --env-file.
docker compose --env-file .env.staging configНесколько файлов считываются последовательно, при этом последующие файлы переопределяют предыдущие. Храните настройки по умолчанию, не содержащие секретных данных, в файле, который добавляется в репозиторий, а секреты — в файле, который никогда не покидает сервер. То же самое относится к env_file:, где при наличии дублирующихся ключей приоритет имеет последний указанный файл.
FAQ
Почему мой файл .env игнорируется внутри контейнера?
Он не игнорируется. Файл .env подставляет значения вместо плейсхолдеров ${NAME} в файле compose, а также может задавать собственные переменные Compose, такие как COMPOSE_PROJECT_NAME или COMPOSE_FILE. Он никогда не устанавливает переменные внутри контейнера. Чтобы передать значение в контейнер, сошлитесь на него: environment: { KEY: "${NAME}" }, или используйте env_file: ./that-file.env.
Что имеет приоритет: environment или env_file?
environment: имеет приоритет. Согласно документации Docker, атрибут environment стоит выше атрибута env_file, и оба они уступают docker compose run -e в командной строке. Если ключ задан в обоих местах, значение в env_file будет молча проигнорировано.
Как увидеть итоговое значение, которое будет использовать Compose?
Выполните docker compose config, чтобы вывести полностью разрешенный файл compose со всеми примененными интерполяциями. Для уже запущенного контейнера команда docker inspect <container> --format '{{json .Config.Env}}' покажет именно то, что получил его процесс.
Зашифрованы ли секреты в Compose?
Нет. Файловый секрет монтируется в контейнер как обычный файл по пути /run/secrets/<name>, а исходный файл хранится на диске хоста в незашифрованном виде. Преимущество заключается в области видимости, а не в шифровании: значение не попадает в переменные окружения контейнера, в вывод docker inspect и в дампы памяти при сбоях, которые выводят окружение.
Можно ли использовать кавычки и пробелы в файле env?
Да. Compose удаляет окружающие кавычки, поэтому KEY="value with spaces" и KEY=value with spaces дают одинаковый результат value with spaces. Используйте одинарные кавычки, если значение содержит $, который не должен быть интерполирован, и берите в кавычки любое значение, содержащее #, так как без кавычек этот символ начинает комментарий. Пробелы вокруг = удаляются, поэтому KEY = value по-прежнему устанавливает KEY.