Пошук уроків, статей та іншого контенту
Чому конфігурація й секрети не мають жити в коді — і як env-змінні працюють у Node.js, Next.js та Docker.
Змінна середовища — це значення у форматі ІМ’Я=ЗНАЧЕННЯ, яке операційна система передає процесу під час запуску.
Наприклад:
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://user:password@db:5432/appУ програмі змінні середовища зазвичай використовують для:
конфігурації, яка відрізняється між середовищами;
адрес баз даних, черг і зовнішніх сервісів;
ключів API;
токенів доступу;
параметрів запуску;
увімкнення або вимкнення окремих можливостей.
Одна й та сама програма може працювати в розробці, тестовому середовищі та production без зміни вихідного коду. Змінюються лише значення, які передаються процесу.
Наприклад, код може використовувати:
DATABASE_URLА в різних середовищах ця змінна матиме різні значення:
# Локальна розробка
DATABASE_URL=postgres://localhost:5432/my_app
# Production
DATABASE_URL=postgres://app_user:password@production-db:5432/my_appЯкщо пароль або API-ключ записати безпосередньо в коді, він може опинитися:
у Git-репозиторії;
в історії комітів;
у pull request;
у логах CI/CD;
у зібраних JavaScript-файлах;
у резервних копіях репозиторію.
Навіть якщо потім видалити секрет із файлу, він часто залишиться в історії Git. Тому вже опублікований ключ потрібно вважати скомпрометованим і відкликати або замінити.
Жорстко задане значення змушує створювати окремі версії коду для локального запуску, тестів і production. Це збільшує ризик помилок.
Краще залишити в коді ім’я параметра:
const databaseUrl = process.env.DATABASE_URL;А саме значення передавати під час запуску.
Зміна адреси сервісу або рівня логування — це операційна задача. Для неї не має бути потрібним редагувати код і повторно проходити весь процес розробки.
Змінні середовища належать конкретному процесу та зазвичай успадковуються дочірніми процесами.
У Linux або macOS змінну можна передати команді безпосередньо:
PORT=4000 NODE_ENV=production node server.jsАбо спочатку експортувати її в поточній оболонці:
export PORT=4000
export NODE_ENV=production
node server.jsУ Windows PowerShell синтаксис інший:
$env:PORT = "4000"
$env:NODE_ENV = "production"
node server.jsЗначення змінних середовища завжди приходять у вигляді рядків. Навіть якщо записано:
PORT=4000
FEATURE_ENABLED=trueу програмі це будуть рядки "4000" і "true", а не число та boolean.
У Node.js змінні доступні через process.env.
const port = Number(process.env.PORT || 3000);
const nodeEnvironment = process.env.NODE_ENV || "development";
console.log(`Сервер працює на порту ${port}`);
console.log(`Середовище: ${nodeEnvironment}`);Оператор || використовують для значення за замовчуванням. Проте для обов’язкових параметрів краще не підставляти мовчазне значення, а завершувати запуск із помилкою.
Помилка конфігурації має виникати під час старту програми, а не через кілька хвилин після першого запиту.
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Не задано обов'язкову змінну середовища: ${name}`);
}
return value;
}
const config = {
port: Number(process.env.PORT || 3000),
databaseUrl: requireEnv("DATABASE_URL"),
apiKey: requireEnv("PAYMENT_API_KEY"),
};
if (Number.isNaN(config.port)) {
throw new Error("Значення PORT має бути числом");
}
console.log(`Сервер запускається на порту ${config.port}`);Такий підхід має кілька переваг:
помилка помітна одразу;
конфігурація централізована;
решта коду не працює безпосередньо з process.env;
типи й допустимі значення можна перевірити в одному місці.
Оскільки всі значення є рядками, їх потрібно явно перетворювати:
const port = Number(process.env.PORT || "3000");
const isDebugEnabled = process.env.DEBUG === "true";
const allowedOrigins = (process.env.ALLOWED_ORIGINS || "")
.split(",")
.map((origin) => origin.trim())
.filter(Boolean);Для прапорців краще перевіряти конкретне значення "true", а не робити так:
Boolean(process.env.DEBUG)Рядок "false" є непорожнім, тому Boolean("false") поверне true.
.env у локальній розробціДля локального запуску часто використовують файл .env:
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://localhost:5432/my_app
PAYMENT_API_KEY=local-keyЦей файл зручно використовувати на комп’ютері розробника, але він не повинен потрапляти в репозиторій, якщо містить секрети.
У .gitignore зазвичай додають:
.env
.env.*
!.env.exampleФайл .env.example може містити перелік потрібних змінних без справжніх секретів:
NODE_ENV=development
PORT=3000
DATABASE_URL=
PAYMENT_API_KEY=Це допомагає новому розробнику зрозуміти, які параметри потрібно налаштувати.
.envNode.js може отримувати змінні з файлу .env через відповідні можливості конкретної версії Node.js або за допомогою популярних інструментів на кшталт dotenv.
Важливий принцип залишається незмінним: файл .env — це лише спосіб локально підготувати середовище. У production змінні зазвичай передаються платформою запуску, системою CI/CD, оркестратором або менеджером секретів.
Next.js має особливе правило щодо доступності змінних у браузері.
Змінні без префікса NEXT_PUBLIC_ призначені для серверного коду:
DATABASE_URL=postgres://...
INTERNAL_API_TOKEN=secret-tokenЇх можна використовувати в серверних компонентах, route handlers, серверних функціях і коді, який не потрапляє до браузера.
Змінні з префіксом NEXT_PUBLIC_ можуть бути вбудовані в клієнтський JavaScript:
NEXT_PUBLIC_API_URL=https://api.example.comЇх можна використовувати у коді, який виконується в браузері:
const apiUrl = process.env.NEXT_PUBLIC_API_URL;Але така змінна не є секретом. Користувач може побачити її в інструментах розробника або в завантажених файлах.
Ніколи не слід називати секрет так:
NEXT_PUBLIC_DATABASE_PASSWORD=...
NEXT_PUBLIC_SECRET_KEY=...Префікс NEXT_PUBLIC_ фактично означає: «це значення можна показати користувачу».
Потрібно розрізняти:
змінні, доступні серверу під час запуску;
змінні, вбудовані під час збирання;
змінні, доступні клієнтському коду.
Публічні змінні зазвичай вбудовуються у JavaScript під час next build. Якщо змінити їх після збирання, уже створений клієнтський bundle може не змінитися.
Це важливо під час створення одного Docker-образу для кількох середовищ. Якщо значення було вбудовано під час build-етапу, передача іншого значення лише під час запуску контейнера не обов’язково змінить поведінку клієнтського коду.
Серверні змінні можуть читатися під час виконання серверного процесу, залежно від способу використання та конфігурації Next.js.
Практичне правило:
секрети використовуйте лише на сервері;
NEXT_PUBLIC_* вважайте відкритими;
з’ясовуйте, чи потрібна змінна під час build, чи під час runtime;
не покладайтеся на runtime-зміну для значень, уже вбудованих у клієнтський bundle.
У Docker є кілька різних механізмів, які часто плутають.
ENVІнструкція ENV задає змінну в образі або контейнері:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]Такі значення стають частиною конфігурації образу. ENV підходить для несекретних параметрів, але не є хорошим місцем для паролів і ключів.
Змінну можна передати контейнеру через docker run:
docker run --rm \
-p 3000:3000 \
-e NODE_ENV=production \
-e PORT=3000 \
-e DATABASE_URL="$DATABASE_URL" \
my-appМожна використати файл із локальними значеннями:
# .env для локального запуску
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://localhost:5432/my_appdocker run --rm \
--env-file .env \
my-appФайл із секретами не слід додавати до Git або копіювати в Docker-образ.
ARG і ENV — не одне й те самеARG доступний під час збирання образу:
ARG APP_VERSION
RUN echo "Версія: ${APP_VERSION}"Передати його можна так:
docker build --build-arg APP_VERSION=1.4.0 -t my-app .ENV доступний контейнеру під час виконання. ARG і ENV мають різні життєві цикли:
ARG потрібен для build-етапу;
ENV потрібен для runtime;
ARG не є безпечним сховищем секретів.
Не передавайте паролі через --build-arg. Значення, використане під час збирання, може залишитися в шарах образу, кеші або метаданих.
У Docker Compose змінні можна передати через секцію environment:
services:
app:
image: my-app
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: "3000"
DATABASE_URL: ${DATABASE_URL}Значення ${DATABASE_URL} Compose підставляє з доступного середовища або спеціального .env-файлу.
Для локального середовища можна мати окремий файл:
services:
app:
env_file:
- .envНе варто автоматично вважати .env безпечним лише тому, що його використовує Docker Compose. Якщо файл містить секрети, він має бути захищений і виключений із репозиторію.
Для production краще використовувати механізми секретів конкретної платформи або оркестратора, а не зберігати справжні паролі у відкритому Compose-файлі.
Змінні середовища кращі за секрети в коді, але вони не є повноцінним секретним сховищем.
Секрет може стати доступним через:
виведення process.env у лог;
діагностичні endpoint-и;
дампи процесів;
неправильні налаштування CI/CD;
історію команд;
конфігурацію контейнера;
помилки в клієнтському коді.
Тому дотримуйтеся таких правил:
Не комітьте .env із реальними секретами.
Не виводьте секрети в логи.
Не передавайте секрети у фронтенд.
Використовуйте різні ключі для development, staging і production.
Регулярно змінюйте ключі, якщо є підозра на витік.
Обмежуйте права доступу секрету.
Передавайте секрети через CI/CD або менеджер секретів.
Не додавайте секрети до Dockerfile через ARG або ENV.
Перевіряйте, що секрети не потрапляють у зібрані файли.
Не передавайте секрети через URL, де вони можуть залишитися в історії браузера, проксі або логах.
Для production краще використовувати спеціалізовані сховища секретів, які надає хмарна платформа, CI/CD-система або інфраструктурний інструмент. Такі системи зазвичай підтримують контроль доступу, аудит і ротацію значень.
Замість десятків звернень до process.env у різних модулях корисно створити один модуль конфігурації.
function required(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Змінна ${name} є обов'язковою`);
}
return value;
}
function booleanFromEnv(name, defaultValue = false) {
const value = process.env[name];
if (value === undefined) {
return defaultValue;
}
if (value !== "true" && value !== "false") {
throw new Error(`Змінна ${name} має бути true або false`);
}
return value === "true";
}
export const config = Object.freeze({
nodeEnvironment: process.env.NODE_ENV || "development",
port: Number(process.env.PORT || "3000"),
databaseUrl: required("DATABASE_URL"),
debug: booleanFromEnv("DEBUG"),
});Інший код працює вже з config, а не з глобальним process.env:
import { config } from "./config.js";
console.log(`Підключення до бази: ${config.databaseUrl}`);У реальному застосунку пароль у цьому прикладі, звичайно, не потрібно виводити. Приклад демонструє лише принцип централізованого читання конфігурації.
Перевіряйте не лише наявність значення, а й:
тип;
діапазон числа;
формат URL;
допустимий список значень;
взаємозалежність параметрів.
Наприклад, NODE_ENV може приймати лише відомі значення:
const allowedEnvironments = new Set([
"development",
"test",
"production",
]);
const nodeEnvironment = process.env.NODE_ENV || "development";
if (!allowedEnvironments.has(nodeEnvironment)) {
throw new Error(`Невідоме значення NODE_ENV: ${nodeEnvironment}`);
}Конкретний порядок завантаження залежить від інструмента, фреймворку та способу запуску. Важливо не покладатися на припущення, а перевіряти документацію саме для використовуваної версії.
Загальна модель така:
Значення, передане безпосередньо процесу, часто має найвищий пріоритет.
Значення з файлів конфігурації завантажуються під час старту.
Значення за замовчуванням використовуються, якщо нічого іншого не задано.
Проблеми виникають, коли в команді є кілька .env-файлів із різними значеннями. Варто заздалегідь визначити:
які файли дозволені;
який файл використовується локально;
де зберігається шаблон;
хто відповідає за production-конфігурацію;
які змінні обов’язкові.
У CI/CD змінні середовища часто використовують для:
токенів публікації;
облікових даних хмарних сервісів;
параметрів deployment;
адрес тестових сервісів;
feature flags;
підключення до тестової бази.
Секрети потрібно зберігати в захищеному сховищі CI/CD, а не в YAML-файлі pipeline.
Також важливо:
маскувати секрети в логах;
не запускати job із production-секретами для неперевірених pull request;
обмежувати секрети конкретними job;
використовувати короткоживучі токени, якщо це можливо;
не друкувати всі змінні для діагностики.
Команда на кшталт такої небезпечна:
printenvВона може вивести токени, паролі та внутрішні адреси в логи CI/CD.
.envФайл із секретами випадково додають у Git, а потім видаляють одним із наступних комітів. Це не очищає історію.
Якщо секрет уже потрапив у репозиторій:
відкличте або замініть його;
перевірте логи та доступи;
за потреби очистьте історію репозиторію;
додайте файл до .gitignore;
повідомте команду про інцидент.
NEXT_PUBLIC_ для секретівБудь-яка змінна з цим префіксом потенційно доступна користувачу. Не використовуйте його для паролів, приватних токенів або ключів, які повинні залишатися на сервері.
"false" — це falseУсі змінні є рядками:
process.env.FEATURE_ENABLED === "false";повертає true, якщо змінна має значення "false".
Потрібно явно перетворювати рядок у boolean.
Якщо програма запускається з порожнім DATABASE_URL, помилка може виникнути лише під час першого звернення до бази. Перевіряйте конфігурацію під час запуску.
process.env може містити багато секретних даних. Не виводьте його повністю навіть тимчасово в production-логах.
Змінна, вбудована у фронтенд під час збирання, не обов’язково зміниться після запуску контейнера. Для Next.js особливо важливо розуміти, де саме використовується змінна — на сервері чи в браузері.
ARG для паролівBuild-аргументи не призначені для безпечної передачі секретів. Секрет може залишитися в шарах образу або кеші збирання.
Файл .env має власні правила синтаксису. Значення з пробілами або спеціальними символами потрібно записувати коректно:
APP_NAME="Мій застосунок"Не копіюйте без перевірки синтаксис оболонки в .env-файл і навпаки.
Перед запуском застосунку перевірте:
чи немає секретів у вихідному коді;
чи додано .env до .gitignore;
чи існує безпечний .env.example;
чи перевіряються обов’язкові змінні;
чи перетворюються рядкові значення на потрібні типи;
чи не потрапляють секрети в логи;
чи не використовується NEXT_PUBLIC_ для приватних даних;
чи зрозуміло, які змінні потрібні під час build, а які — під час runtime;
чи не передаються секрети через Docker ARG;
чи різні середовища використовують різні ключі;
чи є спосіб швидко відкликати та замінити скомпрометований секрет.
Змінні середовища відокремлюють код від конфігурації та дають змогу запускати одну й ту саму програму в різних середовищах.
Найважливіші правила:
не зберігайте секрети в коді та Git;
у Node.js читайте значення через process.env;
перевіряйте обов’язкові параметри під час старту;
пам’ятайте, що всі змінні є рядками;
у Next.js вважайте NEXT_PUBLIC_* відкритими;
розрізняйте build-time і runtime-конфігурацію;
у Docker використовуйте ENV для несекретних параметрів, а секрети передавайте безпечними runtime-механізмами;
для production за можливості використовуйте спеціалізовані менеджери секретів;
мінімізуйте доступ до секретів і регулярно їх ротируйте.