Пошук уроків, статей та іншого контенту
Перевірте конфігурацію, безпеку, спостережуваність, надійність, розгортання та готовність Node.js-застосунку до production.
Production-ready Node.js-застосунок має:
передбачувано запускатися з явною конфігурацією;
не розкривати секрети та внутрішні деталі;
повідомляти про стан і помилки;
коректно завершувати роботу;
витримувати тайм-аути, перевантаження та тимчасові збої залежностей;
мати відтворюване розгортання й план відновлення.
Чекліст потрібно перевіряти не лише перед першим релізом. Його варто повторювати після змін у коді, інфраструктурі, залежностях і процесі деплою.
Конфігурація не повинна залежати від локальних значень за замовчуванням.
Перевірте:
NODE_ENV=production, якщо застосунок або його залежності використовують цей режим;
порт і адресу прослуховування;
URL зовнішніх сервісів;
тайм-аути;
рівень логування;
параметри підключення до бази даних;
ліміти розміру запитів;
параметри кешування.
Не зберігайте production-секрети у .env-файлах, які потрапляють до репозиторію. Використовуйте секретне сховище або захищені змінні середовища платформи розгортання.
Помилка конфігурації має зупиняти застосунок під час запуску, а не проявлятися через кілька годин у першому запиті.
// config.js
function requiredString(name) {
const value = process.env[name];
if (!value || value.trim() === '') {
throw new Error(`Відсутня обов'язкова змінна середовища: ${name}`);
}
return value;
}
function positiveInteger(name, fallback) {
const rawValue = process.env[name] ?? String(fallback);
const value = Number(rawValue);
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`Змінна ${name} має бути додатним цілим числом`);
}
return value;
}
export const config = Object.freeze({
nodeEnv: process.env.NODE_ENV ?? 'development',
port: positiveInteger('PORT', 3000),
host: process.env.HOST ?? '0.0.0.0',
databaseUrl: requiredString('DATABASE_URL'),
requestTimeoutMs: positiveInteger('REQUEST_TIMEOUT_MS', 15_000)
});Під час запуску конфігурація має завантажуватися до створення HTTP-сервера:
// server.js
import { config } from './config.js';
console.log(`Запуск у режимі ${config.nodeEnv} на ${config.host}:${config.port}`);Перевірте також:
чи не виводяться значення секретних змінних у логах;
чи не використовуються небезпечні fallback-значення для production;
чи однаково називаються змінні в локальному середовищі, CI та production;
чи розділені конфігурації різних середовищ.
Перед розгортанням:
зафіксуйте версії залежностей через lock-файл;
запускайте перевірку вразливостей залежностей;
видаляйте невикористовувані пакети;
не встановлюйте пакети під час запуску production-контейнера без необхідності;
відокремлюйте dependencies від devDependencies.
Для npm типовий production-встановлення має виглядати так:
npm ci --omit=devnpm ci встановлює залежності за lock-файлом і зазвичай підходить для відтворюваних CI/CD-збірок.
Перевірте, що:
секрети не потрапляють у Git;
секрети не виводяться у stack trace або структуровані логи;
токени не передаються через URL;
персональні дані не логуються без потреби;
дампи помилок не містять паролів, cookie та заголовків авторизації;
доступ до production-секретів має мінімально необхідний персонал і процеси.
На рівні застосунку або reverse proxy налаштуйте:
HTTPS;
коректну обробку заголовків безпеки;
обмеження розміру тіла запиту;
тайм-аути;
rate limiting для чутливих endpoint-ів;
перевірку автентифікації та авторизації на сервері.
Не покладайтеся на перевірки лише у клієнтському JavaScript. Усі права доступу мають перевірятися на сервері.
Не повертайте клієнту:
stack trace;
внутрішні шляхи файлової системи;
SQL-запити;
конфігурацію;
текст необроблених винятків від внутрішніх сервісів.
Production-логи мають бути придатними для машинного пошуку. JSON-формат спрощує фільтрацію та агрегацію.
Записуйте щонайменше:
час події;
рівень (info, warn, error);
назву події;
ідентифікатор запиту;
тривалість операції;
код відповіді;
безпечний контекст помилки.
Не покладайтеся на довільні повідомлення на кшталт Something went wrong. Назви подій мають бути стабільними.
function log(level, event, fields = {}) {
process.stdout.write(
JSON.stringify({
timestamp: new Date().toISOString(),
level,
event,
...fields
}) + '\n'
);
}
log('info', 'server_started', {
port: 3000,
environment: 'production'
});Відстежуйте:
кількість запитів;
частоту помилок;
розподіл latency, зокрема p95 і p99;
використання пам’яті;
CPU;
кількість активних з’єднань;
тривалість і помилки зовнішніх залежностей;
перезапуски процесу.
Середня latency може приховувати повільні окремі запити, тому для production важливі перцентилі.
Розділяйте:
liveness — процес працює і не завис;
readiness — процес готовий приймати трафік.
Liveness не має перевіряти всі зовнішні залежності. Тимчасова недоступність бази даних не обов’язково означає, що процес потрібно перезапускати.
Readiness може враховувати стан критичних залежностей. Якщо застосунок більше не готовий приймати трафік, балансувальник має припинити надсилати йому нові запити.
Кожна мережна операція повинна мати обмеження часу:
вхідний HTTP-запит;
підключення до бази даних;
запит до зовнішнього API;
очікування відповіді;
graceful shutdown.
Без тайм-аутів завислий ресурс може утримувати сокети та пам’ять необмежено довго.
Тайм-аут не замінює повторні спроби. Retry застосовуйте лише для операцій, які безпечно повторювати. Використовуйте обмежену кількість спроб і backoff, інакше система може створити додаткове навантаження під час збою.
Розділяйте:
очікувані помилки вхідних даних;
помилки авторизації;
тимчасові помилки залежностей;
програмні помилки;
фатальні помилки процесу.
Не маскуйте програмні помилки відповіддю зі статусом 200. Клієнт, моніторинг і балансувальник повинні бачити коректний HTTP-статус.
uncaughtException і необроблене відхилення Promise свідчать про непередбачений стан. Після такої помилки безпечніше завершити процес і дозволити supervisor або оркестратору запустити його знову, ніж продовжувати роботу в потенційно пошкодженому стані.
Під час SIGTERM застосунок має:
припинити приймати нові з’єднання;
повідомити readiness-перевірці, що він більше не готовий;
завершити поточні запити в межах обмеженого часу;
закрити підключення до бази даних та інших ресурсів;
завершити процес;
примусово зупинитися після shutdown timeout.
Приклад мінімального HTTP-сервера без зовнішніх залежностей:
// server.js
import http from 'node:http';
import { randomUUID } from 'node:crypto';
const port = Number(process.env.PORT ?? 3000);
const host = process.env.HOST ?? '0.0.0.0';
const requestTimeoutMs = 15_000;
const shutdownTimeoutMs = 10_000;
if (!Number.isInteger(port) || port <= 0) {
throw new Error('PORT має бути додатним цілим числом');
}
let isShuttingDown = false;
function log(level, event, fields = {}) {
process.stdout.write(
JSON.stringify({
timestamp: new Date().toISOString(),
level,
event,
...fields
}) + '\n'
);
}
const server = http.createServer((request, response) => {
const requestId = request.headers['x-request-id'] || randomUUID();
const startedAt = process.hrtime.bigint();
response.setHeader('content-type', 'application/json; charset=utf-8');
response.setHeader('x-request-id', requestId);
if (isShuttingDown && request.url !== '/health/live') {
response.statusCode = 503;
response.end(JSON.stringify({ error: 'service_unavailable' }));
return;
}
if (request.url === '/health/live' && request.method === 'GET') {
response.statusCode = 200;
response.end(JSON.stringify({ status: 'ok' }));
return;
}
if (request.url === '/health/ready' && request.method === 'GET') {
response.statusCode = isShuttingDown ? 503 : 200;
response.end(JSON.stringify({
status: isShuttingDown ? 'not_ready' : 'ready'
}));
return;
}
if (request.url === '/' && request.method === 'GET') {
response.statusCode = 200;
response.end(JSON.stringify({ message: 'Hello from production' }));
return;
}
response.statusCode = 404;
response.end(JSON.stringify({ error: 'not_found' }));
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
log('info', 'http_request', {
requestId,
method: request.method,
url: request.url,
statusCode: response.statusCode,
durationMs: Math.round(durationMs * 100) / 100
});
});
server.requestTimeout = requestTimeoutMs;
server.headersTimeout = requestTimeoutMs;
server.keepAliveTimeout = 5_000;
server.listen(port, host, () => {
log('info', 'server_started', { host, port });
});
function shutdown(signal) {
if (isShuttingDown) {
return;
}
isShuttingDown = true;
log('info', 'shutdown_started', { signal });
const forceExitTimer = setTimeout(() => {
log('error', 'shutdown_timeout');
process.exit(1);
}, shutdownTimeoutMs);
forceExitTimer.unref();
server.close((error) => {
clearTimeout(forceExitTimer);
if (error) {
log('error', 'server_close_failed', { message: error.message });
process.exit(1);
}
log('info', 'shutdown_completed');
process.exit(0);
});
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('uncaughtException', (error) => {
log('error', 'uncaught_exception', {
message: error.message,
stack: error.stack
});
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
log('error', 'unhandled_rejection', {
reason: reason instanceof Error ? reason.message : String(reason)
});
process.exit(1);
});Для реального застосунку до server.close() потрібно додати закриття пулів бази даних, клієнтів черг, WebSocket-серверів та інших довгоживучих ресурсів.
CI/CD-процес має:
встановити залежності за lock-файлом;
виконати перевірку форматування та lint;
запустити unit- і integration-тести;
зібрати застосунок;
перевірити production-конфігурацію;
створити артефакт або контейнер;
розгорнути саме перевірену версію.
Не змінюйте код вручну на production-сервері. Однаковий коміт має створювати однаковий артефакт для всіх середовищ, а середовище повинно відрізнятися конфігурацією.
Production-процес повинен працювати під supervisor або оркестратором, який:
перезапускає процес після аварійного завершення;
обмежує ресурси;
збирає stdout і stderr;
підтримує health checks;
надсилає SIGTERM під час зупинки;
не запускає застосунок від root без необхідності.
Не покладайтеся на вбудований cluster або кілька процесів як на заміну масштабуванню та контролю ресурсів інфраструктурою. Обрана модель має відповідати платформі розгортання.
Перевірте:
чи запускаються міграції контрольовано;
чи сумісна нова версія коду зі старою схемою під час rolling deployment;
чи можна відкотити код без пошкодження даних;
чи є резервні копії;
чи перевірено відновлення з резервної копії.
Міграція, яка видаляє або змінює дані без можливості відновлення, потребує окремого плану та перевірки.
Перед розгортанням підтвердьте:
production-конфігурація валідна;
секрети надходять із правильного сховища;
застосунок стартує з чистого середовища;
readiness і liveness endpoint-и повертають очікувані статуси;
помилки не розкривають внутрішні деталі;
тайм-аути встановлені для всіх зовнішніх викликів;
graceful shutdown перевірений сигналом SIGTERM;
логи містять ідентифікатор запиту;
метрики та алерти підключені;
є rollback-процедура;
є відповідальні за реакцію на інциденти.
Одразу після деплою перевірте:
чи новий процес успішно запустився;
чи пройшов readiness check;
чи працює основний критичний сценарій;
чи не зросли помилки та latency;
чи коректно працюють зовнішні інтеграції;
чи не збільшується використання пам’яті;
чи не виникли циклічні перезапуски.
Використовуйте поступове розгортання, якщо це підтримує інфраструктура: canary або поетапне збільшення частки трафіку з можливістю швидкого rollback.
Запускати production із .env, випадково включеним до репозиторію.
Давати змінним середовища небезпечні значення за замовчуванням.
Логувати повний об’єкт запиту, включно з cookie та заголовком авторизації.
Вважати, що процес, який не завершився, обов’язково працює коректно.
Не встановлювати тайм-аути для HTTP-запитів до зовнішніх сервісів.
Повторювати всі помилки без backoff і ліміту спроб.
Використовувати /health із важкою перевіркою бази даних як liveness check.
Завершувати процес через process.exit() одразу після SIGTERM, не закривши активні з’єднання.
Розгортати код без lock-файла або встановлювати залежності командою, яка ігнорує зафіксовані версії.
Виконувати несумісні міграції під час rolling deployment.
Вважати відсутність помилок у перші хвилини доказом повної готовності системи.
Не перевіряти відновлення з резервної копії.
Production-чекліст Node.js охоплює весь життєвий цикл застосунку:
конфігурація валідовується до запуску;
секрети захищені;
залежності зафіксовані та перевірені;
помилки й запити спостерігаються через структуровані логи та метрики;
тайм-аути й обмеження захищають від зависання;
health checks розділяють живий і готовий стан;
SIGTERM обробляється через graceful shutdown;
деплой відтворюваний;
міграції та rollback сплановані;
після релізу перевіряються метрики, помилки й критичні сценарії.
Готовність до production — це не один прапорець у NODE_ENV, а перевірена поведінка застосунку під час нормальної роботи, збою та зупинки.