Файлы .env в Node.js: --env-file и process.env¶
Источник: theNodeBook — .env Files Configuration
Файлы окружения в Node.js загружают пару «ключ–значение» в process.env при старте. Механика охватывает --env-file, синтаксис DotEnv, приоритет родительского процесса, NODE_OPTIONS и программную загрузку. Флаг --env-file указывает Node прочитать файл до запуска entrypoint. Node разбирает поддерживаемый синтаксис и сливает значения в окружение.
Файлы окружения в Node.js¶
Загрузка окружения — это конфигурация на старте. Код приложения читает итоговое представление process.env после того, как Node обработал переменные родителя, env‑файлы и разрешённые опции самого Node. Программные помощники могут разобрать или загрузить env‑файлы позже, но поздняя мутация меняет только окружение текущего процесса.
Значение из родительского процесса может иметь приоритет над тем же ключом в .env. Этот приоритет действует до того, как код приложения читает process.env, поэтому ошибка конфигурации может начаться в команде запуска, даже когда файл на диске выглядит правильно.
1 | |
Если в .env указано PORT=3000, процесс всё равно стартует с process.env.PORT === "9000". Побеждает окружение родителя. Node читает файл, видит ключ, но сохраняет унаследованное значение, потому что в процессе оно уже было.
Это правило — первое, что стоит усвоить. Поддержка .env в Node — слой ввода с более низким приоритетом. Авторитет зависит от того, когда значение попало в процесс и какой путь загрузки его записал.
ОС запускает процесс с вектором аргументов и блоком окружения. Node читает оба на нативном этапе старта. Если в команде есть --env-file, Node читает этот файл до выполнения пользовательского JavaScript. Затем создаётся JavaScript‑окружение, открывается process.env, выполняются preload и оценивается entrypoint. К моменту старта server.js слияние уже произошло.
1 2 | |
Этот код читает результат. Про process.env уже говорилось в главе про объект process. Новое здесь — встроенный путь Node, который кладёт дополнительные ключи в окружение до оценки графа модулей приложения.
Конфигурация runtime и конфигурация приложения здесь близки. Значение вроде PORT обычно относится к приложению. Значение вроде NODE_OPTIONS может изменить старт самого Node, если оно присутствует достаточно рано. Одна и та же поверхность хранения — разные точки потребления.
Граница старта¶
В Node v24 есть два CLI‑флага для загрузки env‑файлов.
1 | |
--env-file загружает обязательный файл. Нет файла — процесс завершается с ошибкой. Entrypoint не трогается: Node падает на этапе обработки опций старта.
1 | |
--env-file-if-exists использует тот же парсер и те же правила присвоения, но продолжает работу, если файла нет. Удобно для локальных переопределений разработчика: на одной машине есть .env.local, в CI его может не быть.
Оба флага должны стоять до entrypoint.
1 | |
В этой команде --env-file=.env передаётся в server.js как аргумент приложения. Node уже пересёк границу аргументов — файл не загружается.
Несколько файлов накладываются в порядке командной строки.
1 | |
Node разбирает .env, затем .env.local. Значения из более позднего файла перекрывают значения из более раннего, если оба пришли из env‑файлов. Унаследованное окружение по‑прежнему сильнее обоих.
1 2 3 | |
1 2 | |
Без унаследованного LOG_LEVEL процесс увидит "debug". Добавьте LOG_LEVEL=warn в shell или process manager — процесс увидит "warn".
Это приоритет переменных окружения — правило упорядочивания, которое выбирает итоговое значение, когда несколько источников на старте упоминают один ключ. Для CLI‑пути env‑файлов полезна такая модель:
1 2 3 | |
Формулировка важна. Более поздние env‑файлы побеждают более ранние только внутри слоя env‑файлов. Окружение родителя лежит выше этого слоя.
Слоистая конфигурация — практика загрузки нескольких источников в осознанном порядке. Типичный локальный паттерн: сначала базовые значения по умолчанию, затем машинные переопределения, в конце — shell или process manager.
1 | |
Порядок в команде виден явно. Базовый файл говорит, что приложению обычно нужно. Файл development сужает конфигурацию для одного режима. Окружение запуска всё ещё может переопределить ключ для одного прогона.
Обратный порядок — реальный баг.
1 | |
Без унаследованного значения .env может перезаписать .env.local. Команда успешна. Сервис стартует. Видимая поломка — просто неверная БД, уровень логов или порт. Порядок файлов — часть контракта старта.
Смотрите итоговое значение в процессе, который реально работает.
1 2 3 4 | |
Этот вывод показывает итоговое значение и прямые аргументы выполнения Node. Унаследованный NODE_OPTIONS он не показывает — смотрите этот ключ отдельно, если поведение старта расходится с видимой командой.
1 | |
Держите такие принты временными. В окружении часто есть учётные данные из слоёв, о которых env‑файл не знал.
Грамматика DotEnv, которую разбирает Node¶
Файл .env — текстовый файл с присвоениями переменных окружения. Обычное имя — .env, но Node принимает любой путь, переданный флагу или API. .env.local, .env.test и config/service.env для Node — просто файлы.
Синтаксис dotenv — грамматика присвоений, которую Node использует внутри таких файлов. Node документирует свою грамматику, потому что в экосистеме была договорённость до появления парсера в core. Большинство файлов скучны. Скучное — хорошо.
1 2 3 | |
У каждого объявления есть имя, знак равенства и значение. Node хранит значение как строку. 3000 становится "3000". true — "true". Текст, похожий на JSON, остаётся текстом.
1 2 3 | |
Приведение типов — задача приложения. Node не выводит boolean, number, array или object из текста env‑файла.
Документированная переносимая грамматика имён переменных узкая:
1 | |
Имя начинается с буквы или подчёркивания. Дальше — буквы, цифры и подчёркивания. Верхний регистр с подчёркиваниями — наименее сюрпризный стиль: shell, service manager, CI и деплой‑инструменты с ним работают предсказуемо.
1 2 3 | |
Текущие парсеры Node в ряде случаев permissive: некоторые имена вне документированного шаблона всё же принимаются, потому что нативный парсер в основном отделяет текст вокруг первого = и обрезает пробелы. Считайте такие написания вне документированного контракта. Если проекту нужны строгие имена — валидируйте разобранные ключи или итоговый объект конфигурации.
Пробелы вокруг неэкранированных ключей и значений обрезаются.
1 2 | |
Node сохранит PORT как "3000", TOKEN как "abc123". Пробелы у = исчезают.
В кавычках пробелы внутри сохраняются.
1 | |
GREETING содержит два ведущих и два завершающих пробела. Node снимает ограничители кавычек и сохраняет внутренние байты после разбора escape‑поведения.
Символ # в неэкранированных значениях начинает комментарий.
1 2 | |
LOG_LEVEL станет "debug". PASSWORD_HASH сохранит #, потому что символ внутри кавычек. Синтаксис комментария простой: вне кавычек # и всё до конца строки игнорируется.
Одинарные, двойные кавычки и обратные кавычки могут оборачивать значение.
1 2 3 | |
Экранированное значение окружения — значение с явными границами. Кавычки полезны, когда в значении есть пробелы, #, = или ведущие/завершающие пробелы.
В двойных кавычках у нативного парсера Node есть дополнительное поведение: \n становится реальным символом новой строки.
1 2 | |
PRIVATE_KEY содержит переводы строк. LITERAL содержит обратный слэш и n. Одинарные кавычки и обратные кавычки сохраняют эту пару как обычный текст.
Многострочные значения — экранированные значения, продолжающиеся на нескольких физических строках.
1 2 3 | |
Node сохраняет одну строку с символами новой строки. Используйте осторожно. Крупные секреты в переменных окружения могут утечь через отладочные принты, диагностические отчёты, дампы падений и инструменты инспекции процесса. Управление секретами — отдельная тема книги; локальное правило простое: не превращайте env‑файлы в хранилище секретов только потому, что существует разбор многострочных значений.
Префикс export принимается и игнорируется.
1 | |
Node сохранит PORT. Префикс export в окружении нужен, чтобы тот же файл присвоений иногда можно было подключить через shell. Совместимость ограничена: shell‑expansion, подстановка команд и shell‑специфичное экранирование остаются вне грамматики DotEnv Node.
Дубликаты ключей в одном разобранном вводе — побеждает последнее значение.
1 2 | |
Результат разбора: PORT: "4000". Парсер перезаписывает предыдущее значение по мере чтения.
Для кривых строк держите консервативную модель. Документация задаёт формат. Текущие релизы Node часто восстанавливаются после проблем формы содержимого, пропуская строки или принимая непереносимые ключи. Отсутствие обязательных файлов и неверные опции старта роняют процесс; странная строка внутри env‑файла обычно требует валидации на стороне приложения, если вы хотите отклонять её явно.
Окончания строк — ещё одна «скучная» деталь кроссплатформенных репозиториев. Парсер Node обрабатывает распространённые окончания, включая файлы с Windows. Если после разбора в значении остались неожиданные управляющие символы — проверьте окружение родителя, сгенерированный ввод или парсер не из Node, прежде чем винить путь --env-file.
Подстановка переменных остаётся вне встроенной грамматики Node.
1 2 | |
Node сохранит LOG_DIR как "$ROOT/logs". Предыдущий ключ не читается и не подставляется. Поведение держит разбор локальным для каждого присвоения. Проекты с expansion нуждаются в явном userland‑парсере или шаге expansion на уровне приложения. Держите этот шаг видимым: правила expansion влияют на безопасность и кавычки.
Подстановка shell‑команд тоже вне грамматики.
1 | |
Node сохранит текст. Команда не выполняется. Разбор env‑файла должен разбирать текст конфигурации, а не исполнять код.
Путь старта под капотом¶
CLI‑путь выполняется достаточно рано, чтобы изменить и окружение, видимое приложению, и часть поведения старта Node.
Старт начинается в нативном коде. Node получает argv и унаследованное окружение от ОС. Парсер опций и NODE_OPTIONS разобраны в CLI‑флаги и конфигурация runtime; держите эту границу в голове. --env-file — CLI‑флаг Node, поэтому Node потребляет его до entrypoint и до аргументов приложения.
У пути env‑файла три задачи.
Разрешить путь относительно текущего рабочего каталога, если не указан абсолютный. Прочитать файл. Разобрать DotEnv‑текст в пары ключ–значение и слить их в состояние окружения, которое Node откроет как process.env.
При CLI‑загрузке NODE_OPTIONS из env‑файла получает смысл на старте.
1 2 | |
1 | |
Флаг предупреждений может повлиять на тот же процесс, потому что Node видит его при старте. Легко упустить, если думать об env‑файлах только как о конфигурации приложения. Файл может подпитывать и конфигурацию старта самого Node.
Путь всё ещё упорядочен. Прямые опции командной строки и унаследованное окружение имеют свой приоритет (см. CLI‑флаги). NODE_OPTIONS из env‑файла входит как часть старта, но при конфликте слабее значений, заданных напрямую в окружении и командной строке.
Путь NODE_OPTIONS из env‑файла — только на старте. Node читает его при сборке состояния опций runtime. Парсер токенизирует строку, проверяет тот же allowlist флагов окружения, что и для унаследованного NODE_OPTIONS, и применяет принятые флаги на уровне dotenv. Уровень важен, когда та же настройка появляется в более сильном месте.
1 2 | |
Если в .env указано NODE_OPTIONS=--enable-source-maps, для этого ключа окружения побеждает унаследованная строка. Node сохранит "--trace-warnings" и проигнорирует значение из env‑файла. Прямые CLI‑флаги всё ещё могут переопределить singleton‑опции или добавиться к повторяемым, но работают против эффективной строки NODE_OPTIONS, которую Node оставил.
Allowlist виден из JavaScript.
1 2 3 | |
Проверка полезна инструментам, валидирующим env‑файлы до запуска. Если проект разрешает NODE_OPTIONS в env‑файлах — валидируйте точные флаги. Опечатка должна падать в CI, а не в обёртке сервиса при деплое.
Preload тоже видят значения из env‑файла.
1 | |
boot.mjs выполняется после загрузки env‑файла. Если .env задаёт APP_MODE=local, preload может прочитать это из process.env.APP_MODE.
Такой порядок делает env‑файлы пригодными для локальных переключателей инструментирования, настройки тестов и небольших bootstrap‑переключателей. Env‑файл становится частью поверхности старта. Значение, загруженное до preload, может повлиять на код, который выполняется до entrypoint.
Отсутствующие файлы делятся на два случая.
1 | |
Команда падает, если .env.required нет. Приложение не стартует.
1 | |
Команда продолжается, если .env.local нет. Node может сообщить об отсутствии опционального файла, но не превращает отсутствие в ошибку старта.
Ошибки чтения — ошибки старта для обязательных файлов. Права, неверные пути и проблемы ФС всплывают до того, как пользовательский код установит свою обработку ошибок. Полезно: обязательная конфигурация старта должна падать рано.
CLI‑путь env‑файла синхронен с точки зрения приложения. Пользовательский JavaScript не видит «полузагруженную» конфигурацию. Node либо закончил чтение и слияние набора файлов, либо старт упал до entrypoint. Это другая форма сбоя, чем модуль приложения, который вызывает fs.readFile() и грузит конфиг позже.
Нативный путь старта также означает, что event loop — не то место, где искать тайминг env‑файла. Ни таймер, ни promise job, ни callback потока, ни preload приложения не выполняются «посередине» CLI‑обработки env‑файла. Node ещё строит состояние, которое увидит JavaScript. После этого выполняются preload и entrypoint.
Слияние — строка к строке. Разобранные ключи и значения становятся записями окружения. Метаданных типа нет. Метаданных источника тоже нет. Попав в process.env, JavaScript‑объект не скажет, пришло ли значение из shell, service manager, .env, .env.local или тестового harness. Если источник важен для отладки — логируйте команду старта и выбранный объект конфигурации на безопасном уровне, а не всё окружение.
Потеря источника объясняет много config‑багов. Итоговый ключ выглядит обычно.
1 | |
Значение могло прийти из export в shell шесть часов назад. Из переменной CI. Из второго env‑файла. Задача Node — собрать представление окружения. Задача приложения — валидировать итоговые данные и при необходимости сделать выбранную конфигурацию наблюдаемой без утечки секретов.
NODE_OPTIONS получает дополнительный проход, потому что его потребляет сам Node. Загрузка через env‑файл может участвовать в этом проходе только при загрузке через CLI‑флаг. Поэтому один и тот же текст даёт разные последствия в зависимости от API загрузки.
1 | |
Загруженный через --env-file, может повлиять на trace предупреждений в текущем процессе. Загруженный через process.loadEnvFile() — строка в process.env после того, как поведение предупреждений уже выбрано. Один ключ. Один парсер. Разная точка bootstrap.
Шаг слияния защищён существующими ключами. Если в окружении уже есть DATABASE_URL, значение из env‑файла остаётся ниже. Защита по ключу, не по смыслу. Node не знает, какой ключ безопаснее, новее или лучше. Видны только строки.
Острый край: унаследованная пустая строка всё равно считается существующим значением.
1 | |
Если в .env есть настоящий DATABASE_URL, процесс всё равно увидит "". Пустая строка — значение. Граница валидации должна отклонять её, если пустое недопустимо.
Пакеты userland dotenv стоят в другой точке временной шкалы.
1 2 | |
Этот preload — JavaScript. Он может заполнить конфигурацию приложения до server.js, если выполнится раньше графа приложения. Он не меняет размер heap V8, старт inspector, уже выполненные preload‑модули или разбор CLI‑опций, который Node уже завершил.
1 | |
Форма всё ещё на тайминге JavaScript preload: рано для приложения, поздно для нативных решений старта Node.
1 | |
Встроенный флаг переносит разбор env‑файла в старт Node. Это главное runtime‑отличие. Экосистема npm по‑прежнему даёт расширения, совместимость со старыми версиями и пакеты expansion. Node core владеет общим парсером и путём загрузки на старте; возможности конкретного пакета остаются у пакета.
Миграция с preload пакета должна быть механической, если проект использовал только базовую загрузку DotEnv.
1 | |
Затем уберите JavaScript preload из entrypoint.
1 | |
После смены прогоните тесты конфигурации. Обратите внимание на variable expansion, override mode, многострочные значения, дубликаты ключей и кавычки. Проекты, зависящие от возможностей пакета, должны оставить пакет или заменить их явным кодом приложения.
Переходный переключатель может ненадолго оставить старые launcher’ы.
1 2 3 | |
Переключатель покупает совместимость, пока service files, npm‑скрипты и CI переходят на --env-file. Уберите его, когда launcher’ы переехали. Стартовые переключатели без владельца становятся невидимыми слоями конфигурации.
Чище миграция, когда это видно в команде.
1 2 3 4 5 | |
Скрипт в package.json показывает ревьюеру, откуда входит конфиг. Entrypoint может сосредоточиться на валидации и старте приложения.
Баги приоритета выглядят скучно¶
Большинство багов env‑файлов выглядят как обычные неверные значения.
1 | |
1 | |
В логе 9000. Разработчик открывает .env, видит PORT=3000, и тратит время на не тот файл. Процесс читает другой слой.
Самый быстрый шаг отладки — вывести итоговое значение окружения и команду запуска вместе. Локально env | grep PORT до запуска может показать состояние shell. Для работающего Linux‑процесса /proc/<pid>/environ показывает унаследованное окружение, если позволяют права. Не кладите это в обычные логи приложения: вывод окружения часто содержит секреты.
Баги слоистости приходят и из порядка файлов.
1 | |
Команда грузит локальные переопределения первыми, базовые значения вторыми. Для ключей, которых нет в окружении родителя, .env может перезаписать .env.local. Для привычного «база, потом override» порядок нужно развернуть.
Configuration drift — разрыв между конфигурацией, которую вы думаете, что имеет процесс, и той, что у него реально есть. Env‑файлы создают drift, потому что локальные файлы, export в shell, значения service manager, переменные CI и деплоя пишут в одну итоговую поверхность.
Опасная часть — тихий успех.
1 | |
1 | |
Процесс стартует. Ключ есть. Значение пустое. Код, проверяющий только наличие ключа, принимает плохую конфигурацию.
Валидация должна проверять смысл, а не только существование.
1 2 3 4 | |
Фрагмент уместен рядом со стартом. Он отклоняет отсутствие и пустые строки до открытия сокетов или фоновой работы.
Ещё один источник drift — NODE_OPTIONS. Значение через --env-file влияет на старт. Значение, загруженное позже через process.loadEnvFile(), — обычный текст окружения.
1 2 3 4 | |
Если в .env есть NODE_OPTIONS=--trace-warnings, вызов напечатает строку. Он не включит trace warnings задним числом. Node уже разобрал опции старта, создал runtime и выбрал поведение предупреждений.
Drift приходит и от опечаток в ключах.
1 | |
Node может разобрать эту строку. Приложение, скорее всего, читает DATABASE_URL. Проверка наличия неверного ключа ничего не даёт. Валидация неизвестных ключей ловит этот класс ошибок до fallback на default или старта с пустым значением.
1 2 3 | |
Проверка относится к разобранным данным или известному объекту env‑файла. Прогон по всему окружению родителя шумный: shell и платформы задают много посторонних переменных.
На Windows стоит помнить край платформы. Имена переменных окружения в главном потоке case‑insensitive, а Node открывает их через process.env с платформенным поведением. Держите ключи проекта стабильными по регистру. PORT, port и Port — три разные строки при ревью файла, даже если платформа позже их схлопнет.
Читайте реальную команду как трассу.
1 2 | |
Начните с окружения родителя. NODE_ENV уже задан до чтения файлов. Если любой файл задаёт NODE_ENV, значение родителя всё равно побеждает. Затем Node читает .env. Затем .env.local: значения более позднего файла перекрывают более ранний для ключей, которые родитель не закрыл. Затем стартует src/main.js.
Добавьте аргумент приложения.
1 | |
Первый --env-file — Node. Вторая строка — приложение, потому что она после entrypoint. Если у приложения свой парсер аргументов, он может увидеть --env-file=.other и сделать с ним что‑то своё. Node — нет.
Добавьте preload.
1 | |
Сначала env‑файл. Затем preload. Затем entrypoint. Если boot.mjs читает process.env.FEATURE_X, он видит загруженное значение. Если boot.mjs сохранил значение в экспортируемый объект, последующая мутация process.env.FEATURE_X этот объект не обновит.
Добавьте унаследованный NODE_OPTIONS.
1 2 | |
Унаследованный NODE_OPTIONS входит до слоя опций командной строки. Если .env тоже даёт NODE_OPTIONS, унаследованное значение сильнее для этого ключа окружения. Точное взаимодействие повторяемых и singleton‑флагов следует правилам парсера опций из главы про CLI. Config‑баг в приложении мог вызвать preload, которого нет в видимой команде node ....
Привычка трассировки масштабируется. Когда сервис стартует с неверной конфигурацией, запишите слои по порядку:
1 | |
Отметьте, какой слой владеет ключом. Угадывать только по .env — плохой цикл отладки. Файл — один слой.
Программная загрузка через process.loadEnvFile()¶
process.loadEnvFile(path) — императивный путь загрузки. Читает DotEnv‑файл и записывает ключи в process.env.
1 2 3 | |
Путь по умолчанию — ./.env, если аргумент опущен. Путь может быть string, URL или Buffer. Функция возвращает undefined; эффект — мутация.
Используйте в контролируемой точке bootstrap.
1 2 3 4 5 | |
Выражение import() важно в ESM. Статические import выполняются до тела импортирующего модуля. Если server.js читает конфигурацию на верхнем уровне, статический import оценит его до loadEnvFile(). Runtime import ставит загрузку первой.
В CommonJS граница похожа, синтаксис другой.
1 2 3 4 | |
Здесь require() идёт после загрузки env‑файла. Модули, которые тянет server.cjs, увидят загруженные значения.
Поздняя загрузка создаёт устаревшие предположения.
1 2 3 4 | |
server.js уже оценён до вызова загрузки. Любое чтение process.env на верхнем уровне в этом графе видело старое окружение. Снимок конфигурации мог уже существовать.
Снимок конфигурации — зафиксированная копия конфигурации в один момент времени: переменная, объект, export модуля или клиент, построенный из env‑значений.
1 2 3 | |
Модуль читает один раз. Поздние изменения process.env.PORT не меняют config.port. Значение всё равно нужно валидировать, прежде чем код приложения считает его TCP‑портом.
process.loadEnvFile() сохраняет существующие ключи окружения — включая ключи родителя и более ранние программные загрузки.
1 2 | |
Эти вызовы не ведут себя как повторные CLI --env-file. После того как первый вызов записал LOG_LEVEL, второй видит существующий process.env.LOG_LEVEL и оставляет его. Для программного слоистого поведения, где поздние файлы должны побеждать, используйте util.parseEnv() и явное слияние объектов.
Выбор пути важнее при программной загрузке, чем при CLI: вызов может жить в пакете, скрипте или test helper.
1 | |
Форма привязывает файл к расположению модуля. Простая относительная строка привязывает к process.cwd(). Обе могут быть верны. Неверная ломает запуск из IDE, npm‑скрипта или test runner с другим рабочим каталогом.
CommonJS preload может вызвать loadEnvFile() до entrypoint приложения.
1 | |
1 2 | |
Достаточно рано для модулей приложения, загруженных после preload. Это всё ещё тайминг JavaScript: NODE_OPTIONS внутри загруженного файла для текущего процесса остаётся обычным текстом.
ESM preload использует --import.
1 | |
1 2 | |
Снова bootstrap приложения: может заполнить process.env до оценки server.mjs, но не откатывает флаги старта.
Preload должен оставаться маленьким. Preload, который читает env, валидирует конфиг, открывает БД, патчит глобалы и стартует метрики, создаёт порядок старта, который трудно инспектировать. Держите загрузку файлов рядом с созданием конфига. Передавайте итоговый объект конфигурации в приложение.
Обработка ошибок тоже отличается от CLI. Исключение из loadEnvFile() — JavaScript exception. Его можно поймать, обернуть или решить, какие файлы обязательны.
1 2 3 4 5 | |
Паттерн даёт контроль на уровне кода, но сдвигает сбой позже. Если приложение уже импортировало модули, читающие конфигурацию, поймать отсутствующий файл там может быть слишком поздно.
CLI‑загрузка — для process‑wide конфигурации старта. Программная — для скриптов, тестов и аккуратно упорядоченных boot‑модулей, где таймингом владеет код.
Разбор без мутации¶
util.parseEnv(content) отделяет разбор от process‑wide мутации.
1 2 3 4 | |
Возвращается обычный объект со строками. process.env не меняется.
Разница полезна в тестах.
1 2 3 4 5 | |
Глобальное состояние процесса не изменилось. Тест может разобрать несколько случаев без очистки process.env после каждого.
Полезно и для валидации.
1 2 3 4 5 | |
На этом этапе у вас данные, а не глобальное состояние. Можно инспектировать ключи, отклонять неизвестные имена, сливать объекты, приводить типы и только потом решать, что получит приложение.
Явное слияние понятнее, чем полагаться на мутацию.
1 2 3 4 | |
Объект повторяет форму CLI‑модели слоёв: база, локаль, унаследованное окружение последним. Порядок виден в одном выражении.
Можно выбрать другую политику и сделать её видимой.
1 | |
Файлы получают последнее слово, в том числе над унаследованными значениями. Для некоторых инструментов это валидная политика приложения, но она отличается от приоритета CLI env‑файлов Node. Держите политику в одном месте. Назовите её. Протестируйте.
Разбор без мутации позволяет валидировать файлы до запуска Node.
1 2 | |
Команда может идти в CI: имена, обязательные плейсхолдеры, зарезервированные runtime‑ключи и форма значений — без зависимости от текущего shell разработчика.
Проверки зарезервированных ключей дешёвы.
1 2 3 | |
Одни команды разрешают runtime‑ключи в env‑файлах. Другие запрещают: поведение runtime должно быть видно в команде запуска. Работает любое правило, если оно записано. Плохое — то, которого никто не записал.
Держите объект вне process.env, пока библиотека не требует переменных окружения. Чище граница — типизированный объект конфигурации.
1 2 3 4 5 6 7 8 | |
Затем при построении объекта:
1 2 3 4 | |
Типизированный объект конфигурации — данные приложения, созданные из сырых env‑строк. «Типизированный» здесь значит: приложение превратило строки в формы, которые реально использует: number, boolean, enum string, URL, duration, размер в байтах и т.д.
Библиотеки schema validation могут автоматизировать паттерн; этой главе достаточно границы. Сырые env‑значения входят на старте. Приложение валидирует один раз. Остальной код получает объект со смыслом приложения.
parseEnv() делает поведение парсера тестируемым.
1 2 3 4 5 6 7 | |
Такой тест ловит случайные предположения о парсере. Маленький, быстрый, независимый от shell.
Обработка дубликатов тоже тестируема.
1 2 3 4 5 | |
Это только поведение парсера. После слияния в process.env сохранение существующих ключей может изменить исход. Держите тесты парсера отдельно от тестов политики слияния.
Граница валидации¶
Граница валидации конфигурации — место, где сырые строки перестают приниматься как доверенная конфигурация приложения.
Поставьте её рано.
1 2 3 4 | |
Объект всё ещё сырой: отсутствующие значения, пустые строки, опечатки, значения из любого слоя, который выиграл приоритет.
Приводите типы осознанно.
1 2 | |
Код может бросить исключение. Пусть падает на старте или перехватите и замените более ясной ошибкой конфигурации. Главное — тайминг: падать до приёма трафика или фоновых задач.
Затем передайте результат.
1 2 3 4 | |
Object.freeze опционален. Сильнее привычка читать окружение один раз и передавать config как данные. Доступ к process.env из каждого модуля создаёт скрытые зависимости и привязывает тесты к глобальной мутации.
Небольшой reader обычно достаточен.
1 2 3 4 5 | |
Функция считает отсутствие и пустую строку невалидными. Принимает объект env: в production — process.env, в тестах — plain object.
С boolean нужна осторожность.
1 | |
Строка "true" — true. "false", "0", "no", "" и отсутствие — false. Если приложение принимает несколько написаний — закодируйте правило в одной parser‑функции и протестируйте.
1 2 3 4 5 | |
Парсер отклоняет "1" и "yes". Другое приложение может их принять. Важно одно правило, а не разбросанные сравнения.
Числам нужны проверки пустых значений и границ.
1 | |
Node этого не сделает. Разбор env‑файла дал строку. Number("") даёт 0, поэтому парсер должен отклонять пустой текст до приведения, если пустое не означает что‑то в вашем приложении. Смысл — у приложения.
URL тоже разбирайте один раз.
1 2 3 4 | |
Остальное приложение может получить URL или валидированную строку. Не повторяйте проверки протокола в каждом модуле, создающем клиент.
Неизвестные ключи полезно отклонять.
1 2 | |
Проверка ловит опечатки вроде DATABSE_URL. Текущий Node может разобрать ключ нормально. Слой конфигурации отклонит его до случайного default.
Держите снимки конфигурации намеренными. Однократное чтение на старте делает приложение предсказуемым. Повторные чтения process.env во время обработки запроса привязывают поведение к изменяемому глобальному состоянию. Позднее присвоение может затронуть одни запросы и не тронуть клиентов, собранных раньше.
1 | |
Присвоение мутирует JavaScript‑объект окружения текущего процесса. Оно не уведомляет модули, уже захватившие уровень логов, не пересоздаёт клиентов и не перезапускает валидацию. Горячая перезагрузка конфигурации — отдельная операционная задача. Загрузка на старте заканчивается на границе валидации.
Форма bootstrap, которая выдерживает нагрузку¶
Чистый стартовый код имеет узкую форму: загрузить, разобрать, валидировать, построить config, затем запустить приложение. Держите шаги рядом.
1 2 3 4 5 6 | |
loadConfig() получает сырые env‑данные. createServer() — данные приложения. Модуль сервера не обязан знать, пришли ли значения из --env-file, shell, тестового объекта или платформы деплоя.
Модуль конфигурации может оставаться маленьким.
1 2 3 4 5 | |
В функции нет file I/O. Она только превращает строки в данные приложения. Это тестируется без process.env.
1 2 3 4 | |
Тесты передают ровно нужные ключи и проверяют плохие значения без мутации глобального состояния для всего процесса.
Загрузка файлов может жить в отдельной обёртке, когда нужна программная загрузка.
1 2 3 4 5 | |
Обёртка владеет мутацией. Reader конфигурации — валидацией. Остальное приложение получает замороженный результат.
При CLI‑загрузке обёртка сжимается.
1 | |
src/main.js может сразу вызвать loadConfig(process.env): Node уже заполнил окружение до оценки entrypoint.
Библиотеки не должны грузить env‑файлы, если их работа не конфигурация. Пакет БД, читающий process.env.DATABASE_URL при import, выбрал глобальный источник до валидации приложением. Передача config в factory оставляет владение у приложения.
1 2 3 | |
Вызов скучный и явный. Клиент БД получает валидированное значение. Источник конфигурации остаётся снаружи клиента.
То же для логгеров, HTTP‑клиентов, feature flags и воркеров. Прочитайте сырое окружение один раз. Преобразуйте. Передавайте данные. Модуль, читающий process.env при import, может быть приемлем для маленьких скриптов; сервисный код проще рассуждать, когда конфигурация идёт через аргументы функций.
Сгенерированный конфиг следует той же границе.
1 2 3 | |
Последовательность валидирует сгенерированный env‑текст до process‑wide окружения. Инструменты могут падать на неверных ключах, пустых обязательных значениях или зарезервированных runtime‑ключах без мутации process.env.
Дочернему процессу нужно явное окружение.
1 2 3 | |
Объект становится окружением родителя ребёнка. Если ребёнок тоже использует --env-file, внутри него действует то же правило: унаследованный WORKER_MODE сильнее env‑файла для этого ключа. Код родителя должен собирать объект env осознанно: для ребёнка это самый сильный слой.
Операционные краевые случаи¶
Env‑файлы удобны, потому что это файлы. Это же и край.
Закоммиченный .env может раскрыть учётные данные. Скопированный локальный файл может разойтись с production. Файл с слишком широкими правами может быть читаем другим локальным пользователем. Диагностический отчёт или debug‑лог может включить значения окружения. Управление секретами и ротация учётных данных — отдельная тема; локальная привычка начинается здесь: коммитьте примеры, не секреты.
1 2 3 | |
Пример документирует обязательные ключи без живых значений. Реальный файл остаётся локальным или приходит с платформы деплоя.
Сделайте пример полезным.
1 2 3 | |
Безопасные default можно заполнить. Чувствительные или специфичные для деплоя значения — пустыми или фейковыми. Ревьюер должен видеть, какие ключи есть, без реальных credential.
Права на файлы всё ещё важны для локальных env‑файлов.
1 | |
Команда Unix‑специфична; биты прав разобраны в главе про права и метаданные. Для этой главы суть уже: локальный env‑файл с секретами должен иметь меньшую поверхность чтения, чем исходники.
Переменные окружения от деплоя обычно сильнее env‑файлов, потому что попадают в окружение родителя: service manager, CI, контейнеры, оркестрация. Механика деплоя — в других главах; результат приоритета здесь: если платформа задала PORT, env‑файл, вероятно, проигрывает.
Имена файлов несут смысл: Node принимает любое имя.
1 2 3 4 | |
Имена говорят сопровождающему, какой это слой. .env2, .env.new, .env.prod.bak заставляют смотреть содержимое и историю команд. Loader примет их. Дисциплину накладывает проект.
Тестовый env‑файл может содержать значения, недопустимые в production.
1 2 3 | |
PORT=0 просит ОС выбрать свободный порт при bind сервера. Парсер всё равно вернёт строку "0". Валидатор должен разрешить это для тестового пути, если приложение намеренно использует ephemeral ports.
Дайте каждому источнику роль.
Закоммиченный пример описывает форму: ключи, безопасные default, пустые плейсхолдеры. Локальный env — машина разработчика. Окружение деплоя — работающий сервис. Командная строка — политика runtime Node. Secret manager — чувствительные значения. Смешивание ролей превращает маленькую config‑систему в угадывание.
1 2 3 4 | |
Файл в исходниках. Документирует контракт. Без живых значений.
1 2 3 | |
Файл на машине разработчика. В .gitignore. Значения могут быть низкорисковыми для dev, но привычка «только локально» строит правильное поведение.
Runtime‑флаги Node заслуживают отдельного пути ревью.
1 | |
В команде два вида данных. Флаг отчёта меняет поведение Node. .env даёт строки. Разделение помогает ревьюеру видеть, что меняет runtime, а что — приложение.
Скрытая версия тоже работает, но нуждается в правиле проекта.
1 2 | |
Некоторые платформы хотят один смонтированный env‑файл со всеми значениями старта. Некоторые команды держат runtime‑флаги в service definition, а значения приложения — в env‑файлах. Выберите одно. Затем валидируйте под него.
Диагностический вывод заслуживает подозрения.
1 | |
Принт может включить токены, URL БД, приватные ключи и credential сервисов — и значения из окружения родителя, о которых env‑файл не знал. Отлаживайте минимальный нужный ключ, затем уберите принт.
У diagnostic reports Node есть управление исключением данных окружения — тема observability позже; риск начинается с той же поверхности: process.env легко инспектировать, логировать и утечь.
Используйте env‑файлы для локальной разработки, настройки тестов и небольших входов на старте. Для production, когда платформа владеет значениями, — платформу. Преобразуйте строки в конфигурацию приложения один раз. После границы валидации остальной код должен зависеть от данных конфигурации, а не от того, что случайно лежит в process.env в момент оценки модуля.
Связанное чтение¶
- Предыдущая: CLI‑флаги и конфигурация runtime Node.js
- Далее: Web Platform APIs в Node.js