Змінні оточення та конфігурація
Змінні оточення та конфігурація
🎯 Мета лекції
- Зрозуміти роль змінних оточення (environment variables) у конфігурації застосунків.
- Опанувати доступ до змінних через
process.envта механізми передачі значень. - Навчитися використовувати файли
.envта бібліотекуdotenvдля локальної розробки. - Освоїти паттерни валідації та типізації конфігурації у TypeScript.
- Вивчити best practices для безпечного зберігання секретів та ключів API.
🔑 Ключові терміни
- Environment Variables: змінні операційної системи, доступні процесу під час виконання.
- process.env: глобальний об'єкт Node.js, що містить усі змінні оточення у вигляді рядків.
- dotenv: бібліотека для завантаження змінних із файлу
.envуprocess.env. - NODE_ENV: конвенційна змінна для визначення середовища виконання (
development,production,test). - Secrets Management: практики безпечного зберігання конфіденційних даних (паролів, API ключів, токенів).
Проблема конфігурації: від хардкоду до змінних оточення
Антипаттерн: хардкодинг конфігурації
Найпростіший, але найнебезпечніший спосіб конфігурації — зберігання значень безпосередньо у коді:
// ❌ НЕБЕЗПЕЧНО: Секрети у вихідному коді
const config = {
database: {
host: 'prod-db-cluster.amazonaws.com',
port: 5432,
username: 'admin',
password: 'SuperSecret123!', // Витік у git історію!
},
apiKeys: {
stripe: 'sk_live_51H8xYz...', // Публічний доступ через GitHub!
sendgrid: 'SG.abc123...',
},
server: {
port: 3000,
},
};
export default config;
Проблеми цього підходу:
- Витік секретів: Паролі та API ключі потрапляють у систему контролю версій (Git) і стають доступними всім, хто має доступ до репозиторію.
- Відсутність гнучкості: Для зміни конфігурації між середовищами (локальна розробка, staging, production) потрібно редагувати код або підтримувати кілька файлів конфігурації.
- Порушення принципу Twelve-Factor App: Сучасні хмарні платформи (Heroku, AWS, Vercel) передають конфігурацію через змінні оточення, а не файли.
git log або GitHub commit history.Рішення: змінні оточення як контракт конфігурації
Змінні оточення (environment variables) — це пари ключ-значення, які операційна система надає процесу під час його запуску. Вони існують поза кодом застосунку і встановлюються адміністратором системи, CI/CD-пайплайном або платформою хостингу.
Переваги:
- Розділення коду та конфігурації: Один і той самий код може працювати у різних середовищах без змін.
- Безпека: Секрети зберігаються у зашифрованих сховищах (AWS Secrets Manager, HashiCorp Vault) або передаються через захищені канали.
- Простота розгортання: Хмарні платформи автоматично інжектують змінні у контейнери та процеси.
Доступ до змінних оточення: process.env
Глобальний об'єкт process.env є словником (dictionary) усіх змінних оточення, доступних поточному процесу Node.js. Всі значення зберігаються як рядки (string).
Базове читання змінних
// Читання змінної NODE_ENV
const environment = process.env.NODE_ENV;
console.log('Середовище:', environment); // "development" | "production" | undefined
// Читання порту сервера з fallback
const port = process.env.PORT || '3000';
console.log('Порт:', port); // "8080" або "3000"
// Перевірка наявності критичної змінної
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL не встановлено! Неможливо підключитися до БД.');
}
const dbUrl = process.env.DATABASE_URL;
console.log('База даних:', dbUrl);
process.env є рядками! Навіть якщо ви встановите PORT=3000, process.env.PORT буде "3000" (string), а не 3000 (number). Для числових значень потрібне явне перетворення через parseInt() або Number().Типові помилки при роботі з process.env
// ❌ Неправильно: очікування числа
const maxConnections = process.env.MAX_CONNECTIONS || 10;
console.log(maxConnections); // "50" (рядок!) або 10
// ✅ Правильно: явне перетворення типів
const maxConnections = parseInt(process.env.MAX_CONNECTIONS || '10', 10);
console.log(maxConnections); // 50 (число)
// ❌ Неправильно: булеві значення
const isProduction = process.env.NODE_ENV === 'production' || false;
console.log(isProduction); // Завжди true, якщо NODE_ENV !== 'production'!
// ✅ Правильно: явна перевірка
const isProduction = process.env.NODE_ENV === 'production';
console.log(isProduction); // true або false
// ❌ Неправильно: парсинг JSON без обробки помилок
const allowedOrigins = JSON.parse(process.env.CORS_ORIGINS);
// ✅ Правильно: безпечний парсинг
const allowedOrigins = process.env.CORS_ORIGINS
? JSON.parse(process.env.CORS_ORIGINS)
: ['http://localhost:3000'];
Встановлення змінних у різних операційних системах
Unix/Linux/macOS: експорт через shell
У Unix-подібних системах змінні встановлюються через команду export або inline перед командою запуску:
# Експорт змінної для всіх наступних команд у сесії
export NODE_ENV=production
export PORT=8080
export DATABASE_URL="postgresql://user:pass@localhost/mydb"
# Запуск застосунку (змінні вже у process.env)
node server.js
# Або inline (змінні діють лише для цієї команди)
NODE_ENV=production PORT=8080 node server.js
REM Встановлення змінної для поточної сесії
set NODE_ENV=production
set PORT=8080
set DATABASE_URL=postgresql://user:pass@localhost/mydb
REM Запуск застосунку
node server.js
# Встановлення змінної
$env:NODE_ENV = "production"
$env:PORT = "8080"
$env:DATABASE_URL = "postgresql://user:pass@localhost/mydb"
# Запуск застосунку
node server.js
VAR=value command) працює лише у Unix shell (Bash, Zsh, Fish). У Windows потрібно використовувати set або $env: перед командою, або застосувати кросплатформний пакет cross-env.Кросплатформний запуск: cross-env
Для універсальних npm-скриптів, які працюють на всіх ОС, використовують пакет cross-env:
npm install --save-dev cross-env
{
"scripts": {
"dev": "cross-env NODE_ENV=development ts-node src/server.ts",
"build": "cross-env NODE_ENV=production tsc",
"start": "cross-env NODE_ENV=production node dist/server.js",
"test": "cross-env NODE_ENV=test jest"
}
}
Файли .env: локальна розробка без хаосу
Для локальної розробки зручно зберігати змінні у текстовому файлі .env у корені проєкту. Цей файл не комітиться у Git і містить секрети, специфічні для локального середовища розробника.
Структура файлу .env
# .env — приклад файлу для локальної розробки
# Середовище виконання
NODE_ENV=development
# Сервер
PORT=3000
HOST=localhost
# База даних PostgreSQL
DATABASE_URL=postgresql://postgres:password@localhost:5432/myapp_dev
DB_POOL_MIN=2
DB_POOL_MAX=10
# Redis
REDIS_URL=redis://localhost:6379
# API ключі (секрети)
STRIPE_SECRET_KEY=sk_test_51Hxyz...
SENDGRID_API_KEY=SG.abc123...
JWT_SECRET=my-super-secret-jwt-key-change-in-production
# Зовнішні сервіси
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_REGION=eu-central-1
# Feature flags
ENABLE_BETA_FEATURES=true
LOG_LEVEL=debug
.env має бути обов'язково доданий у .gitignore! Інакше всі секрети потраплять у публічний репозиторій.Додавання .env у .gitignore
# .gitignore
# Змінні оточення з секретами
.env
.env.local
.env.*.local
# Дозволити .env.example (шаблон без секретів)
!.env.example
Створення .env.example як шаблону
Файл .env.example комітиться у Git і слугує документацією для інших розробників. Він містить всі необхідні змінні, але без реальних значень:
# .env.example — шаблон конфігурації для розробників
NODE_ENV=development
PORT=3000
# База даних PostgreSQL (встановіть свої значення)
DATABASE_URL=postgresql://USER:PASSWORD@localhost:5432/DATABASE_NAME
DB_POOL_MIN=2
DB_POOL_MAX=10
# Redis
REDIS_URL=redis://localhost:6379
# API ключі (отримайте на https://dashboard.stripe.com)
STRIPE_SECRET_KEY=sk_test_...
SENDGRID_API_KEY=SG...
JWT_SECRET=generate-random-secret-here
# AWS (для локальної розробки використовуйте LocalStack або MinIO)
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_REGION=eu-central-1
Новий розробник у команді копіює .env.example → .env і заповнює власними значеннями:
cp .env.example .env
# Редагувати .env та додати справжні секрети
Завантаження .env файлів: бібліотека dotenv
Пакет dotenv читає файл .env та автоматично додає всі змінні у process.env при старті застосунку. Це стандарт де-факто у Node.js-екосистемі.
Встановлення та базове використання
npm install dotenv
pnpm add dotenv
yarn add dotenv
Завантаження змінних на старті застосунку:
// src/server.ts — точка входу
import 'dotenv/config'; // Автоматично завантажує .env у process.env
import express from 'express';
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
environment: process.env.NODE_ENV,
database: process.env.DATABASE_URL?.split('@')[1], // Приховати credentials
});
});
app.listen(port, () => {
console.log(`Сервер запущено на http://localhost:${port}`);
console.log(`Середовище: ${process.env.NODE_ENV}`);
});
dotenv/config має бути першим у точці входу застосунку, до імпорту будь-яких модулів, які залежать від process.env. Інакше змінні не встигнуть завантажитися.Альтернативний синтаксис з явною конфігурацією
import dotenv from 'dotenv';
import path from 'node:path';
// Завантаження .env з кастомного шляху
dotenv.config({
path: path.resolve(process.cwd(), '.env.local'),
});
// Або із обробкою помилок
const result = dotenv.config();
if (result.error) {
console.warn('⚠️ Файл .env не знайдено, використовуються системні змінні');
} else {
console.log('✅ Завантажено змінних:', Object.keys(result.parsed || {}).length);
}
Багатосередовищна конфігурація: .env.development, .env.production
Для різних середовищ можна використовувати окремі файли:
project/
├── .env # Базові змінні (загальні для всіх)
├── .env.development # Локальна розробка
├── .env.test # Тестове середовище (Jest/Vitest)
├── .env.production # Production (не комітиться!)
└── .env.example # Шаблон для розробників
Завантаження правильного файлу залежно від NODE_ENV:
import dotenv from 'dotenv';
import path from 'node:path';
const env = process.env.NODE_ENV || 'development';
const envFile = `.env.${env}`;
// Завантажити базовий .env
dotenv.config();
// Перезаписати специфічними для середовища змінними
dotenv.config({
path: path.resolve(process.cwd(), envFile),
override: true, // Перезаписати існуючі змінні
});
console.log(`Завантажено конфігурацію для середовища: ${env}`);
dotenv-flow автоматизує завантаження багатьох файлів .env у правильному порядку пріоритетів. Він завантажує .env → .env.local → .env.${NODE_ENV} → .env.${NODE_ENV}.local, перезаписуючи попередні значення.NODE_ENV: конвенція середовища виконання
Змінна NODE_ENV є неофіційним стандартом у Node.js-екосистемі для визначення режиму роботи застосунку. Більшість фреймворків та бібліотек (Express, React, Webpack) використовують її для увімкнення/вимкнення оптимізацій.
Стандартні значення NODE_ENV
development
Локальна розробка
- Детальні логи та stack traces
- Гаряча перезагрузка (hot reload)
- Вимкнені оптимізації (швидша збірка)
- Source maps для налагодження
production
Продакшн-сервери
- Мінімальні логи (тільки помилки)
- Мініфікація та стиснення коду
- Увімкнені оптимізації (кешування, gzip)
- Відключені dev tools
test
Автоматичне тестування
- Швидка база даних у пам'яті
- Моки зовнішніх API
- Паралельне виконання тестів
- Очищення стану між тестами
Використання NODE_ENV у коді
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';
// Умовне логування
if (isDevelopment) {
console.log('[DEBUG] Деталі запиту:', req.body);
}
// Різна конфігурація для різних середовищ
const logLevel = isProduction ? 'error' : 'debug';
// Увімкнення CORS тільки у розробці
if (isDevelopment) {
app.use(cors({ origin: '*' }));
}
// Різні бази даних
const databaseUrl = isTest
? 'postgresql://localhost/test_db'
: process.env.DATABASE_URL;
Express: автоматична оптимізація за NODE_ENV
Express автоматично змінює поведінку залежно від NODE_ENV:
import express from 'express';
const app = express();
// У production Express:
// - Кешує шаблони view
// - Вимикає детальні помилки у відповідях
// - Оптимізує роутинг
if (process.env.NODE_ENV === 'production') {
app.enable('view cache');
app.set('trust proxy', 1); // За reverse proxy (Nginx, CloudFlare)
}
// Middleware для обробки помилок залежно від середовища
app.use((err, req, res, next) => {
const isDev = process.env.NODE_ENV !== 'production';
res.status(err.status || 500).json({
error: {
message: err.message,
// Stack trace тільки у розробці
...(isDev && { stack: err.stack }),
},
});
});
Типізація та валідація конфігурації у TypeScript
Прямий доступ до process.env не є type-safe: TypeScript не може гарантувати наявність змінних або їхній формат. Для надійності потрібна централізована валідація конфігурації.
Проблема: відсутність типів у process.env
// ❌ Небезпечно: TypeScript не перевіряє наявність змінної
const apiKey = process.env.STRIPE_API_KEY; // string | undefined
// Застосунок може впасти у runtime, якщо змінна відсутня
// ❌ Небезпечно: неявне перетворення типів
const port = process.env.PORT; // string | undefined
app.listen(port); // Помилка: очікується number, отримано string
Рішення 1: Ручна валідація з типами
// src/config/env.ts
interface EnvironmentConfig {
nodeEnv: 'development' | 'production' | 'test';
port: number;
database: {
url: string;
poolMin: number;
poolMax: number;
};
apiKeys: {
stripe: string;
sendgrid: string;
};
jwt: {
secret: string;
expiresIn: string;
};
}
function validateEnv(): EnvironmentConfig {
const requiredVars = [
'NODE_ENV',
'PORT',
'DATABASE_URL',
'STRIPE_SECRET_KEY',
'SENDGRID_API_KEY',
'JWT_SECRET',
];
// Перевірка наявності критичних змінних
const missing = requiredVars.filter((varName) => !process.env[varName]);
if (missing.length > 0) {
throw new Error(
`❌ Відсутні обов'язкові змінні оточення: ${missing.join(', ')}`
);
}
// Валідація NODE_ENV
const nodeEnv = process.env.NODE_ENV as string;
if (!['development', 'production', 'test'].includes(nodeEnv)) {
throw new Error(
`❌ NODE_ENV має бути development, production або test. Отримано: ${nodeEnv}`
);
}
// Валідація PORT
const port = parseInt(process.env.PORT as string, 10);
if (isNaN(port) || port < 1 || port > 65535) {
throw new Error(`❌ PORT має бути числом від 1 до 65535. Отримано: ${process.env.PORT}`);
}
return {
nodeEnv: nodeEnv as EnvironmentConfig['nodeEnv'],
port,
database: {
url: process.env.DATABASE_URL as string,
poolMin: parseInt(process.env.DB_POOL_MIN || '2', 10),
poolMax: parseInt(process.env.DB_POOL_MAX || '10', 10),
},
apiKeys: {
stripe: process.env.STRIPE_SECRET_KEY as string,
sendgrid: process.env.SENDGRID_API_KEY as string,
},
jwt: {
secret: process.env.JWT_SECRET as string,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},
};
}
// Єдине джерело істини для конфігурації
export const config = validateEnv();
Використання у застосунку:
// src/server.ts
import 'dotenv/config';
import { config } from './config/env';
import express from 'express';
const app = express();
// Type-safe доступ до конфігурації
app.listen(config.port, () => {
console.log(`🚀 Сервер запущено на порту ${config.port}`);
console.log(`📦 Середовище: ${config.nodeEnv}`);
console.log(`🗄️ База даних: ${config.database.url.split('@')[1]}`);
});
// TypeScript знає, що config.port — це number
const maxTimeout = config.port * 10; // OK
Рішення 2: Валідація через Zod
Бібліотека zod надає декларативний синтаксис для валідації та автоматичної генерації TypeScript типів:
npm install zod
import { z } from 'zod';
// Схема валідації з автоматичними типами
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().regex(/^\d+$/).transform(Number).pipe(z.number().min(1).max(65535)),
HOST: z.string().default('localhost'),
// База даних
DATABASE_URL: z.string().url().startsWith('postgresql://'),
DB_POOL_MIN: z.string().transform(Number).pipe(z.number().min(1)).default('2'),
DB_POOL_MAX: z.string().transform(Number).pipe(z.number().min(1)).default('10'),
// Redis
REDIS_URL: z.string().url().startsWith('redis://').optional(),
// API ключі
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
SENDGRID_API_KEY: z.string().startsWith('SG.'),
JWT_SECRET: z.string().min(32, 'JWT_SECRET має містити мінімум 32 символи'),
JWT_EXPIRES_IN: z.string().default('7d'),
// AWS
AWS_ACCESS_KEY_ID: z.string().optional(),
AWS_SECRET_ACCESS_KEY: z.string().optional(),
AWS_REGION: z.string().default('eu-central-1'),
// Feature flags
ENABLE_BETA_FEATURES: z
.string()
.transform((val) => val === 'true')
.pipe(z.boolean())
.default('false'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
// Валідація з детальними помилками
const parseEnv = () => {
const result = envSchema.safeParse(process.env);
if (!result.success) {
console.error('❌ Помилка валідації змінних оточення:');
console.error(result.error.format());
process.exit(1);
}
return result.data;
};
// Експорт типізованої конфігурації
export const env = parseEnv();
// TypeScript автоматично виводить тип
export type Env = z.infer<typeof envSchema>;
import { env } from './config/env';
// Автодоповнення та перевірка типів
console.log(env.NODE_ENV); // "development" | "production" | "test"
console.log(env.PORT); // number
console.log(env.ENABLE_BETA_FEATURES); // boolean
// TypeScript попереджає про неіснуючі властивості
console.log(env.NONEXISTENT_VAR); // ❌ Помилка компіляції
// Використання у конфігурації бази даних
import { Pool } from 'pg';
const pool = new Pool({
connectionString: env.DATABASE_URL,
min: env.DB_POOL_MIN,
max: env.DB_POOL_MAX,
});
zod для валідації змінних оточення є best practice у сучасних TypeScript-проєктах. Бібліотека автоматично генерує типи, забезпечує детальні повідомлення про помилки та підтримує трансформації (string → number, string → boolean).Best Practices: безпека та управління секретами
1. Ніколи не комітьте секрети у Git
# .gitignore — обов'язкові правила
# Файли з секретами
.env
.env.local
.env.*.local
*.pem
*.key
*.crt
# Виняток: шаблони без секретів
!.env.example
Перевірка перед комітом:
# Пошук потенційних секретів у staged файлах
git diff --cached --name-only | xargs grep -E 'password|secret|api_key' || echo "✅ Секретів не знайдено"
2. Використовуйте різні секрети для різних середовищ
// ❌ Небезпечно: один ключ для всіх середовищ
const stripeKey = 'sk_live_abc123'; // Якщо витече — зламано production!
// ✅ Правильно: різні ключі для dev та prod
const stripeKey =
process.env.NODE_ENV === 'production'
? process.env.STRIPE_LIVE_SECRET_KEY
: process.env.STRIPE_TEST_SECRET_KEY;
3. Шифруйте .env у приватних репозиторіях (опціонально)
Для команд, які зберігають .env у Git (не рекомендується для публічних репозиторіїв), можна використовувати git-crypt або sops:
# Встановлення git-crypt (macOS)
brew install git-crypt
# Ініціалізація шифрування у репозиторії
git-crypt init
# Додати користувача з GPG ключем
git-crypt add-gpg-user USER_GPG_KEY
# Зазначити файли для шифрування
echo ".env filter=git-crypt diff=git-crypt" >> .gitattributes
# Після комміту .env буде автоматично зашифрований
git add .env .gitattributes
git commit -m "Add encrypted .env"
4. Генеруйте криптографічно стійкі секрети
import crypto from 'node:crypto';
// Генерація випадкового JWT secret (32 байти = 256 біт)
const jwtSecret = crypto.randomBytes(32).toString('hex');
console.log('JWT_SECRET=' + jwtSecret);
// Генерація безпечного паролю (base64 для читабельності)
const password = crypto.randomBytes(24).toString('base64');
console.log('DATABASE_PASSWORD=' + password);
5. Використовуйте Secrets Manager у production
Для production-середовищ використовуйте спеціалізовані сервіси управління секретами:
AWS Secrets Manager
HashiCorp Vault
Doppler
Приклад інтеграції з AWS Secrets Manager:
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
async function getSecret(secretName: string): Promise<string> {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);
if (!response.SecretString) {
throw new Error(`Секрет ${secretName} не містить значення`);
}
return response.SecretString;
}
// Завантаження секретів при старті
const dbPassword = await getSecret('prod/database/password');
const stripeKey = await getSecret('prod/stripe/secret-key');
console.log('✅ Секрети завантажено з AWS Secrets Manager');
6. Логуйте конфігурацію без секретів
function logConfig(config: Record<string, any>, secretKeys: string[] = []): void {
const sanitized = { ...config };
// Приховати значення секретних ключів
secretKeys.forEach((key) => {
if (sanitized[key]) {
sanitized[key] = '***HIDDEN***';
}
});
console.log('📋 Конфігурація застосунку:', JSON.stringify(sanitized, null, 2));
}
// Використання
logConfig(
{
NODE_ENV: process.env.NODE_ENV,
PORT: process.env.PORT,
DATABASE_URL: process.env.DATABASE_URL,
JWT_SECRET: process.env.JWT_SECRET,
},
['DATABASE_URL', 'JWT_SECRET'] // Не показувати у логах
);
Практичні приклади конфігурації
Приклад 1: Express сервер з базою даних
// src/config/env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().transform(Number).pipe(z.number().positive()),
HOST: z.string().default('0.0.0.0'),
DATABASE_URL: z.string().url(),
DB_SSL: z.string().transform((v) => v === 'true').default('false'),
REDIS_URL: z.string().url().optional(),
CORS_ORIGINS: z
.string()
.transform((val) => val.split(',').map((s) => s.trim()))
.pipe(z.array(z.string().url())),
RATE_LIMIT_MAX: z.string().transform(Number).pipe(z.number().positive()).default('100'),
RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default('900000'), // 15 хвилин
});
export const env = envSchema.parse(process.env);
// src/server.ts
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { Pool } from 'pg';
import { createClient } from 'redis';
import { env } from './config/env';
// База даних PostgreSQL
const pool = new Pool({
connectionString: env.DATABASE_URL,
ssl: env.DB_SSL ? { rejectUnauthorized: false } : false,
});
// Redis для сесій
const redis = env.REDIS_URL ? await createClient({ url: env.REDIS_URL }).connect() : null;
// Express застосунок
const app = express();
// CORS з дозволеними origins
app.use(cors({ origin: env.CORS_ORIGINS, credentials: true }));
// Rate limiting
app.use(
rateLimit({
windowMs: env.RATE_LIMIT_WINDOW_MS,
max: env.RATE_LIMIT_MAX,
message: 'Забагато запитів з цієї IP-адреси, спробуйте пізніше.',
})
);
app.get('/health', async (req, res) => {
const dbStatus = await pool.query('SELECT 1').then(() => 'OK').catch(() => 'ERROR');
const redisStatus = redis ? await redis.ping().then(() => 'OK').catch(() => 'ERROR') : 'N/A';
res.json({
status: 'UP',
environment: env.NODE_ENV,
database: dbStatus,
redis: redisStatus,
});
});
app.listen(env.PORT, env.HOST, () => {
console.log(`🚀 Сервер запущено на http://${env.HOST}:${env.PORT}`);
console.log(`📦 Середовище: ${env.NODE_ENV}`);
console.log(`🔒 CORS origins: ${env.CORS_ORIGINS.join(', ')}`);
});
Приклад 2: Багатосередовищна конфігурація з fallback
// src/config/database.ts
import { env } from './env';
interface DatabaseConfig {
host: string;
port: number;
database: string;
username: string;
password: string;
ssl: boolean;
pool: {
min: number;
max: number;
};
}
export function getDatabaseConfig(): DatabaseConfig {
// Парсинг DATABASE_URL: postgresql://user:pass@host:port/dbname
const url = new URL(env.DATABASE_URL);
return {
host: url.hostname,
port: parseInt(url.port || '5432', 10),
database: url.pathname.slice(1), // Видалити початковий /
username: url.username,
password: url.password,
ssl: env.NODE_ENV === 'production',
pool: {
min: env.NODE_ENV === 'production' ? 5 : 2,
max: env.NODE_ENV === 'production' ? 20 : 10,
},
};
}
Питання для самоконтролю
Навіть у приватних репозиторіях є кілька ризиків:
- Витік через експорт: Якщо репозиторій буде експортовано у ZIP-архів або зроблено публічним — секрети стануть доступними миттєво.
- Історія Git: Навіть після видалення
.envз репозиторію, він залишається у історії комітів і може бути витягнутий черезgit logабоgit reflog. - Доступ співробітників: Колишні працівники, які мали доступ до репозиторію, зберігають клони з секретами.
- Інтеграції: CI/CD системи (GitHub Actions), code review інструменти (SonarQube) можуть копіювати дані репозиторію у свої сховища.
Рішення: Зберігайте секрети у спеціалізованих Secrets Manager (AWS Secrets Manager, HashiCorp Vault, Doppler) або у налаштуваннях CI/CD як encrypted secrets.
Якщо NODE_ENV не встановлено, більшість фреймворків використовують значення за замовчуванням:
- Express: Працює у development режимі (детальні помилки, відключено кешування).
- React (Create React App): Збірка у development режимі (великий розмір bundle, повільніше).
- Webpack: Без оптимізацій (не мініфікує код).
У production це призведе до:
- Повільної роботи застосунку (через відсутність оптимізацій).
- Витоку чутливої інформації (stack traces у відповідях API).
- Збільшеного споживання пам'яті та CPU.
Best practice: Завжди явно встановлюйте NODE_ENV=production у production-середовищах.
Існує три способи:
1. Через docker run (inline):
docker run -e NODE_ENV=production -e PORT=8080 myapp:latest
2. Через файл .env:
docker run --env-file .env myapp:latest
3. Через docker-compose.yml:
version: '3.8'
services:
app:
image: myapp:latest
environment:
- NODE_ENV=production
- PORT=8080
- DATABASE_URL=${DATABASE_URL} # З host .env
env_file:
- .env.production
4. Через Docker secrets (для чутливих даних):
echo "my-secret-password" | docker secret create db_password -
services:
app:
secrets:
- db_password
secrets:
db_password:
external: true
Так, process.env — це звичайний JavaScript-об'єкт, який можна модифікувати:
// Встановлення нової змінної у runtime
process.env.NEW_VARIABLE = 'value';
console.log(process.env.NEW_VARIABLE); // "value"
// Видалення змінної
delete process.env.SOME_VAR;
// Зміна існуючої змінної
process.env.PORT = '9000';
Однак це антипаттерн! Причини:
- Глобальний стан: Зміна
process.envвпливає на весь процес, включаючи сторонні бібліотеки. - Race conditions: У багатопотоковому коді (Worker Threads) можуть виникнути конфлікти.
- Тестування: Складно ізолювати тести, якщо вони змінюють глобальний стан.
Альтернатива: Створіть об'єкт конфігурації та передавайте його через dependency injection:
class AppConfig {
constructor(private env: Record<string, string>) {}
get port(): number {
return parseInt(this.env.PORT, 10);
}
}
const config = new AppConfig(process.env);
Кроки діагностики:
- Виведіть всі змінні при старті:
console.log('Доступні змінні оточення:', Object.keys(process.env));
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('DATABASE_URL:', process.env.DATABASE_URL ? '✅ Встановлено' : '❌ Відсутня');
- Перевірте порядок завантаження:
// ❌ Неправильно: config імпортується ДО завантаження dotenv
import { config } from './config/env';
import 'dotenv/config';
// ✅ Правильно: dotenv завантажується ПЕРШИМ
import 'dotenv/config';
import { config } from './config/env';
- Перевірте шлях до .env:
import dotenv from 'dotenv';
import path from 'node:path';
const envPath = path.resolve(process.cwd(), '.env');
console.log('Шлях до .env:', envPath);
const result = dotenv.config({ path: envPath });
if (result.error) {
console.error('Помилка завантаження .env:', result.error);
} else {
console.log('✅ .env завантажено:', Object.keys(result.parsed || {}).length, 'змінних');
}
- Використовуйте debug режим dotenv:
import dotenv from 'dotenv';
dotenv.config({ debug: true }); // Виводить детальну інформацію
Підсумок: правила роботи зі змінними оточення
✅ Робити
- Використовувати
.envдля локальної розробки - Додавати
.envу.gitignore - Створювати
.env.exampleяк документацію - Валідувати конфігурацію при старті (Zod)
- Генерувати криптостійкі секрети
- Використовувати різні ключі для dev/prod
- Логувати конфігурацію без секретів
- Встановлювати
NODE_ENVявно
❌ Не робити
- Комітити
.envу Git - Хардкодити секрети у коді
- Використовувати production-ключі локально
- Ігнорувати валідацію змінних
- Логувати паролі та API ключі
- Змінювати
process.envу runtime - Зберігати секрети у CI/CD логах
- Використовувати один
.envдля всіх середовищ
Чеклист для production
Крок 1: Валідація конфігурації
Переконайтеся, що всі критичні змінні присутні та мають правильний формат через Zod або власну функцію валідації.
Крок 2: Secrets Manager
Перенесіть секрети з .env у AWS Secrets Manager, HashiCorp Vault або аналогічний сервіс.
Крок 3: Різні ключі для середовищ
Переконайтеся, що production використовує sk_live_* (Stripe), prod-* (AWS) ключі, а не тестові.
Крок 4: Налаштування NODE_ENV
Встановіть NODE_ENV=production у Dockerfile, docker-compose.yml або платформі хостингу (Vercel, Railway).
Крок 5: Логування без секретів
Перевірте, що логи не містять паролів, API ключів або токенів. Використовуйте sanitization функції.
Крок 6: Моніторинг доступу
Увімкніть аудит логи у Secrets Manager для відстеження, хто та коли отримував доступ до секретів.
Діаграма: життєвий цикл конфігурації
У наступній лекції ми розглянемо інструменти налагодження Node.js-застосунків, включаючи вбудований debugger, Chrome DevTools та профілювання продуктивності.