Три файла в проекте: .env, .env.local, .env.production, и переменная API_URL в каждом свое значение. Локально приложение почему-то стучится в продакшн API вместо локального мока, хотя разработчик точно помнит, что менял значение именно в .env.local. Разгадка в том, что порядок приоритета переменных окружения у Node.js через dotenv, у Docker Compose и у Next.js это три разных набора правил, и они не совпадают между собой.
Node.js с классическим dotenv
Библиотека dotenv в базовой конфигурации читает только один файл, обычно .env, и не перезаписывает уже существующие переменные окружения системы.
require("dotenv").config();
console.log(process.env.API_URL);
Ключевой момент здесь: если переменная API_URL уже была установлена в окружении до запуска Node процесса, например через команду export API_URL=... прямо в терминале, значение из .env файла не перезапишет ее. Системная переменная окружения имеет приоритет над файлом по умолчанию, и это довольно частая причина путаницы, когда разработчик правит файл, а поведение приложения не меняется.
Docker Compose: три источника одновременно
Docker Compose устроен сложнее, у него минимум три независимых источника переменных, и они не сливаются в один файл, а применяются в разных местах с разным приоритетом.
| Источник | Где применяется | Приоритет |
|---|---|---|
| Файл .env в папке compose файла | Подстановка внутри самого yaml через ${VAR} |
Базовый |
| Поле environment в сервисе | Переменные внутри контейнера | Выше базового |
| Поле env_file в сервисе | Переменные внутри контейнера | Ниже, чем environment |
| Переменная окружения shell при запуске | Зависит от контекста использования | Обычно самый высокий |
services:
web:
env_file: .env.production
environment:
- API_URL=http://localhost:3000
Здесь API_URL внутри контейнера будет равна http://localhost:3000 из поля environment, даже если в .env.production для той же переменной прописано совсем другое значение, потому что явное поле environment в описании сервиса имеет приоритет над env_file.
Next.js: своя иерархия из четырех файлов
Next.js добавляет еще один слой сложности, различая файлы по назначению окружения и приоритету одновременно.
| Файл | Приоритет | Особенность |
|---|---|---|
| .env.development.local или .env.production.local | Высший | Не грузится в противоположном окружении |
| .env.local | Высокий | Не грузится, если NODE_ENV равен test |
| .env.development или .env.production | Средний | Выбирается автоматически по значению NODE_ENV |
| .env | Базовый, самый низкий | Грузится всегда, во всех окружениях без исключения |
Если одна и та же переменная объявлена и в .env.local, и в .env.development, победит значение из .env.local, потому что этот файл выше в иерархии приоритета Next.js. Важный нюанс: .env.local намеренно не подхватывается при NODE_ENV=test, чтобы автоматические тесты не зависели от локальных секретов конкретного разработчика на его машине.
Как реально отладить, откуда взялось значение
Самый быстрый способ проверить итоговое значение переменной, не гадая по иерархии приоритетов из документации, это залогировать его прямо в момент старта приложения, до всей остальной бизнес-логики.
console.log("Реальное значение API_URL:", process.env.API_URL);
Для Docker Compose есть более элегантный способ без правки кода приложения:
docker compose config
Эта команда показывает полностью развернутую конфигурацию compose файла со всеми уже примененными переменными и правилами приоритета, ровно то, что реально увидит контейнер при запуске, без необходимости гадать по документации, какое значение в итоге победило в конкретном случае.
Итоговый чеклист
Для Node.js с dotenv помните: системная переменная окружения побеждает значение из файла .env, а не наоборот, это частая причина, почему правка файла как будто не действует.
Для Docker Compose явное поле environment в сервисе побеждает env_file, даже если интуитивно кажется, что должно быть наоборот из-за разного расположения в файле.
Для Next.js файл .env.local выше по приоритету, чем .env.development или .env.production, и намеренно не грузится в тестовом окружении.
При странном поведении со значением переменной сначала залогируйте реальное значение в момент старта приложения, а не пытайтесь угадать победителя правил приоритета на глаз по документации.
Если нужно быстро проверить содержимое нескольких .env файлов на предмет расхождений между окружениями, инструмент .env Diff покажет разницу построчно по каждому ключу.