Skip to main content

Як працювати з змінними середовища в Node.js?

Змінні середовища (environment variables) - це пари ключ-значення, які передаються процесу Node.js при старті. Вони дозволяють налаштовувати додатки без жорсткого прописування секретів, портів чи URL баз даних прямо в коді.

Теорія

TL;DR

  • process.env.KEY завжди повертає рядок. 'false' - це truthy-рядок. '0' теж truthy. Без винятків.
  • dotenv завантажуй першим рядком, до будь-яких інших require.
  • .env до git не пушити. Додай у .gitignore до першого коміту.
  • Типи перетворюй вручну: parseInt(...) для чисел, === 'true' для булевих.
  • Node 20.6+ вміє читати .env без пакетів: node --env-file=.env server.js.

Швидкий приклад

js
// .env файл: // PORT=4000 // DEBUG=true require('dotenv').config(); // обов'язково першим рядком const port = parseInt(process.env.PORT || '3000', 10); const debug = process.env.DEBUG === 'true'; // НЕ просто if (process.env.DEBUG) console.log(port); // 4000 console.log(debug); // true (булевий, не рядок)

Тут відбувається два кроки. dotenv зчитує .env і копіює кожну пару в process.env. Далі код читає їх як рядки і перетворює в потрібний тип вручну.

Як Node завантажує змінні середовища

Node.js зчитує змінні від ОС при створенні процесу через C++ біндинги до getenv(). Вони потрапляють в process.env (звичайний JS-об'єкт) ще до того, як завантажується твій головний модуль. Це означає одне: process.env - це знімок (snapshot). Якщо змінити process.env.KEY під час роботи програми, батьківська оболонка нічого не побачить. Якщо інший процес встановить змінну після старту Node, Node теж нічого не дізнається.

Всі значення - рядки. ОС не знає про числа чи булеві типи. Саме тому if (process.env.DEBUG) спрацює навіть коли значення - рядок 'false'.

Встановлення змінних

Три способи залежно від контексту.

Термінал (Linux/macOS):

bash
PORT=4000 node server.js # inline, тільки для цієї команди export PORT=4000 && node server.js # для поточної сесії

Windows:

bash
# cmd set PORT=4000 && node server.js # PowerShell $env:PORT="4000"; node server.js

npm scripts (кросплатформенно через пакет cross-env):

json
{ "scripts": { "start": "cross-env NODE_ENV=production node server.js" } }

Файли .env з dotenv

Для локальної розробки поклади змінні у файл .env в кореневій директорії проекту і завантажуй їх через пакет dotenv.

bash
npm install dotenv
ini
# .env - ніколи не комітити цей файл PORT=3000 NODE_ENV=development DATABASE_URL=postgresql://user:password@localhost:5432/mydb JWT_SECRET=super-secret-key-32-chars-minimum
js
// Вхідний файл: app.js, index.js або server.js require('dotenv').config(); // першим рядком, без винятків const express = require('express'); const app = express(); app.listen(process.env.PORT || 3000);

Найпоширеніша помилка, яку я бачу в pull request-ах: require('dotenv').config() стоїть після require('./db'), який вже спробував прочитати DATABASE_URL. Значення було undefined. Виправлення завжди одне - перемістити dotenv на самий початок.

Кілька env-файлів дозволяють розділяти конфігурацію:

.env # спільні значення за замовчуванням .env.local # локальні налаштування, gitignored .env.development # специфічні для розробки .env.production # специфічні для продакшну
js
dotenv.config({ path: `.env.${process.env.NODE_ENV}` });

Вбудована підтримка .env у Node.js 20.6+

Починаючи з Node 20.6, пакет dotenv не потрібен для базового використання:

bash
node --env-file=.env server.js

Node зчитує файл напряму. Синтаксис такий самий, як і в dotenv. Для більшості проектів цього достатньо. Для кількох файлів або розширення змінних (variable expansion) dotenv дає більше можливостей.

Валідація змінних при старті

Читати process.env.KEY і сподіватися, що значення є, - це отримувати крашi під час роботи, а не при старті. Перевіряй один раз і рано:

js
const { z } = require('zod'); const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']), PORT: z.string().transform(Number).default('3000'), DATABASE_URL: z.string().url(), JWT_SECRET: z.string().min(32), }); const env = envSchema.parse(process.env); // Якщо DATABASE_URL відсутня - виключення при старті // А не через 3 години після деплою в продакшн

Joi працює так само. Суть: виявляти відсутні змінні в момент запуску програми, а не коли юзер потрапить на роут, що потребує бази даних.

Типові помилки

Немає fallback для process.env.PORT:

js
// Неправильно app.listen(process.env.PORT); // undefined в локалці → TypeError // Правильно app.listen(parseInt(process.env.PORT || '3000', 10));

Рядок 'false' як булевий:

js
// Неправильно - 'false' є truthy-рядком if (process.env.DEBUG) { enableLogging(); } // Правильно if (process.env.DEBUG === 'true') { enableLogging(); }

Коміт .env до git:

bash
# .gitignore - додай до першого коміту .env .env.local .env.*.local

У 2023 році витік, пов'язаний з Vercel, стався через .env у публічному репозиторії. Виправлення займає п'ять секунд: додати файл у .gitignore до git init.

Завантаження dotenv після інших модулів:

js
// Неправильно - db.js читає DATABASE_URL як undefined const db = require('./db'); require('dotenv').config(); // запізно // Правильно require('dotenv').config(); const db = require('./db');

Мутація змінних для worker threads:

js
// Зміни process.env в main не потрапляють до воркерів process.env.TEST = 'value'; const { Worker } = require('worker_threads'); new Worker('./worker.js'); // воркер отримує знімок зі старту // Передавай через workerData new Worker('./worker.js', { workerData: { test: 'value' } });

Де зустрічається

  • Express: app.listen(process.env.PORT || 3000) - стандарт на Heroku і Render
  • Next.js: префікс NEXT_PUBLIC_ відкриває змінні клієнту, решта залишається на сервері
  • NestJS: ConfigModule.forRoot() обгортає process.env типізованим сервісом
  • Prisma: читає DATABASE_URL напряму для міграцій і запитів
  • Docker: ENV PORT=3000 в Dockerfile або --env-file .env у docker run
  • GitHub Actions: блок env: у workflow-файлах або repository secrets

Питання для закріплення

Q: Чому всі змінні середовища - рядки?
A: ОС передає їх як рядки. Node копіює їх як є. Немає рантайму, який автоматично перетворює "3000" в 3000.

Q: Що станеться, якщо викликати require('dotenv').config() двічі?
A: За замовчуванням dotenv не перезаписує вже встановлені змінні. Другий виклик нічого не змінює для наявних значень. Змінити це можна через { override: true }.

Q: Як поводяться змінні в cluster mode?
A: cluster.fork() копіює знімок середовища на момент виклику. Якщо оновити process.env після fork-у, воркер цього не побачить. Передавай конфіг через опцію env у cluster.fork({ PORT: '4001' }).

Q: Чи оновлення process.env в main thread потрапляє до worker threads?
A: Ні. Worker threads отримують копію середовища при створенні. Зміни в process.env головного потоку не досягають запущених воркерів. Для цього використовуй workerData або канал повідомлень.

Q: Чи є обмеження на розмір змінних середовища?
A: ОС встановлює ліміт - близько 1MB сумарно на Linux. Великі конфігурації краще зберігати у файлі або менеджері секретів (AWS Secrets Manager, HashiCorp Vault), а не в змінних середовища.

Приклади

Базовий: читання змінної з fallback

js
// Запуск: PORT=4000 node server.js // Або просто: node server.js (fallback до 3000) const port = parseInt(process.env.PORT || '3000', 10); console.log(`Сервер на порті ${port}`); // Вивід: Сервер на порті 4000 // Вивід (PORT не задано): Сервер на порті 3000

Давай fallback для змінних з розумним значенням за замовчуванням. Виключення при старті роби тільки для тих, що дефолту не мають, наприклад DATABASE_URL.

Проміжний: Express-додаток з dotenv

js
// npm install express dotenv require('dotenv').config(); // першим const express = require('express'); const app = express(); const port = parseInt(process.env.PORT || '3000', 10); const dbUrl = process.env.DATABASE_URL; if (!dbUrl) { console.error('DATABASE_URL обов\'язкова'); process.exit(1); } app.get('/health', (req, res) => { res.json({ status: 'ok', port }); }); app.listen(port, () => console.log(`Слухаємо на порті ${port}`)); // .env: // PORT=3000 // DATABASE_URL=postgres://user:pass@localhost/mydb

Зверни увагу на ранній вихід коли DATABASE_URL відсутня. Краще впасти при старті з чітким повідомленням, ніж падати на першому запиті до бази зі складним стектрейсом.

Розширений: валідація при старті через Zod

js
require('dotenv').config(); const { z } = require('zod'); const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), PORT: z.string().regex(/^\d+$/).transform(Number).default('3000'), DATABASE_URL: z.string().url(), JWT_SECRET: z.string().min(32), }); let env; try { env = envSchema.parse(process.env); } catch (err) { console.error('Невалідні змінні середовища:'); console.error(err.flatten().fieldErrors); process.exit(1); } module.exports = env; // Імпортуй цей модуль скрізь замість прямого звернення до process.env // Всі змінні перевірені один раз при старті

Цей патерн дає типізований конфіг по всьому додатку. Будь-який файл, що імпортує env, знає: валідація вже відбулась і типи відповідають схемі.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?