В этом руководстве развернём небольшое Node.js-приложение на VPS и подготовим его к работе в production: установим Node.js LTS, создадим отдельного системного пользователя, настроим переменные окружения, PM2, Nginx и HTTPS.
В процессе:
- Установим Node.js LTS и npm;
- Создадим отдельного пользователя для приложения;
- Подготовим тестовое Node.js-приложение и .env;
- Добавим health endpoint для проверки состояния сервиса;
- Запустим приложение через PM2;
- Настроим автозапуск после перезагрузки VPS;
- Подключим Nginx как reverse proxy;
- Привяжем домен и выпустим SSL-сертификат;
- Проверим логи приложения и Nginx;
- Протестируем /health;
- Обновим приложение через pm2 reload, чтобы сократить простой при перезапуске.
В результате Node.js-приложение будет работать как отдельный системный сервис за Nginx, автоматически запускаться после перезагрузки сервера и быть доступным по HTTPS. В этом How-to мы специально не будем использовать контейнеризацию - с Docker всё было бы проще, но так мы пройдём по шагам, для понимания внутренней кухни Node.js приложения
Устанавливаем Node.js LTS
Начнём с подготовки чистого VPS и установки актуальной LTS-ветки Node.js. LTS-релизы предназначены для длительной поддержки и обычно предпочтительнее для production-серверов, чем ветка Current.
Подготовка VPS
Подключимся к серверу по SSH и обновим индекс пакетов: sudo apt update
Установим доступные обновления: sudo apt upgrade -y
Также установим базовые утилиты, которые понадобятся дальше: sudo apt install -y curl ca-certificates
После обновления можно проверить версию Ubuntu: lsb_release -a
Для этого руководства используется Ubuntu 24.04.
Установка Node.js

Для установки Node.js подключим репозиторий NodeSource с LTS-веткой. Например, для Node.js 22: curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
Стоит отметить, что мы не рекомендуем запускать автоматически какие-либо скрипты из интернета, не убедившись в их безопасности, и что они получены из доверенных источников. В нашем примере это допущение тестового стенда.
После добавления репозитория установим Node.js: sudo apt install -y nodejs
Пакет включает сам runtime Node.js и npm.
Проверим версии:
node --version
npm --version
В выводе должны отображаться установленные версии Node.js и npm.
Теперь сервер готов к запуску Node.js-приложений. Но размещать рабочий процесс от имени root не стоит, поэтому следующим шагом создадим для приложения отдельного системного пользователя.
Создаём отдельного системного пользователя
Для приложения лучше использовать отдельную учётную запись Linux с минимально необходимыми правами. Это отделяет процесс приложения от административной учётной записи сервера и ограничивает последствия возможной ошибки или компрометации приложения.
Зачем запускать приложение не от root
Процесс, запущенный от root, получает практически неограниченный доступ к системе. При уязвимости в приложении или одной из npm-зависимостей такой уровень прав значительно увеличивает потенциальный ущерб.
Node.js-приложению обычно не нужны административные привилегии. Оно должно иметь доступ только к собственным файлам, рабочей директории и необходимым сетевым ресурсам.
Поэтому создадим отдельного пользователя nodeapp, от имени которого позднее запустим приложение через PM2.
Создание пользователя и рабочей директории

Создадим системного пользователя с домашним каталогом: sudo adduser --disabled-password --gecos "" nodeapp
Подготовим каталог приложения: sudo mkdir -p /var/www/nodeapp
Назначим владельцем нового пользователя: sudo chown -R nodeapp:nodeapp /var/www/nodeapp
Проверим созданную учётную запись: id nodeapp
И права на рабочую директорию: ls -ld /var/www/nodeapp
Владельцем и группой каталога должен быть nodeapp.
При необходимости перейти в оболочку нового пользователя можно командой: sudo -iu nodeapp
Рабочая директория приложения при этом останется: /var/www/nodeapp
Далее в этой директории создадим небольшое рабочее Node.js-приложение, вынесем его настройки в .env и добавим отдельный endpoint для проверки состояния сервиса.
Разворачиваем тестовое Node.js-приложение
Теперь создадим небольшое рабочее приложение на Express. Оно будет возвращать основную страницу, читать настройки из .env и предоставлять отдельный /health endpoint для проверки состояния сервиса.
Создание приложения
Переключимся на пользователя nodeapp: sudo -iu nodeapp
Перейдём в рабочую директорию: cd /var/www/nodeapp
Инициализируем новый Node.js-проект: npm init -y
Установим Express и пакет для работы с .env: npm install express dotenv
Создадим основной файл приложения: nano server.js
Добавим код:
require('dotenv').config();
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const APP_NAME = process.env.APP_NAME || 'Node.js VPS Demo';
const APP_VERSION = process.env.APP_VERSION || '1.0.0';
app.get('/', (req, res) => {
res.json({
app: APP_NAME,
version: APP_VERSION,
message: 'Application is running'
});
});
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
app: APP_NAME,
version: APP_VERSION
});
});
app.listen(PORT, '127.0.0.1', () => {
console.log(`${APP_NAME} is listening on 127.0.0.1:${PORT}`);
});
Приложение будет слушать только 127.0.0.1:3000. Напрямую из интернета этот порт открывать не потребуется: внешние HTTP- и HTTPS-запросы позднее будет принимать Nginx.
В package.json добавим команду запуска:
"scripts": {
"start": "node server.js"
}
После этого приложение можно будет запускать стандартной командой: npm start
Настройка health endpoint
Endpoint /health используется для быстрой проверки того, что процесс приложения запущен и способен обрабатывать HTTP-запросы.
В нашем приложении запрос: GET /health
возвращает:
{
"status": "ok",
"app": "Node.js VPS Demo",
"version": "1.0.0"
}
Такой endpoint можно использовать в мониторинге, healthcheck-ах load balancer, reverse proxy или внешней системе контроля доступности.
После запуска приложения локальную проверку можно выполнить командой: curl http://127.0.0.1:3000/health
При нормальной работе сервер должен вернуть JSON со статусом ok.
Создание файла .env

Переменные, которые могут отличаться между окружениями, вынесем из исходного кода в .env.
Создадим файл: nano .env
Добавим:
PORT=3000
APP_NAME=Node.js VPS Demo
APP_VERSION=1.0.0
NODE_ENV=production
Ограничим права доступа: chmod 600 .env
Теперь параметры приложения можно менять без редактирования server.js.
Проверим структуру проекта: ls -la
В каталоге должны находиться примерно такие файлы:
.env
node_modules/
package.json
package-lock.json
server.js
После подготовки приложения запустим его через PM2, чтобы процесс автоматически перезапускался после ошибок и восстановления VPS.
Запускаем приложение через PM2
PM2 — менеджер процессов для Node.js-приложений. Он позволяет запускать приложение в фоне, отслеживать состояние процесса, хранить логи и автоматически восстанавливать процессы после перезагрузки сервера.
Установка PM2
Оставаясь под пользователем nodeapp, установим PM2 глобально: npm install -g pm2
Проверим установленную версию: pm2 --version
Запуск приложения
Перейдём в директорию проекта: cd /var/www/nodeapp
Запустим server.js через PM2: pm2 start server.js --name nodeapp
Проверим список процессов: pm2 status
Приложение nodeapp должно находиться в состоянии: online
Дополнительно проверим endpoint: curl http://127.0.0.1:3000/health
Если возвращается JSON со статусом ok, приложение успешно работает под управлением PM2.
Настройка автозапуска после перезагрузки

Сначала сохраним текущий список процессов: pm2 save
Затем сгенерируем команду для интеграции PM2 с systemd: pm2 startup systemd
PM2 выведет готовую команду с sudo, которую необходимо выполнить от пользователя с административными правами.
Она будет выглядеть примерно так: sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u nodeapp --hp /home/nodeapp
После выполнения ещё раз сохраним список процессов: pm2 save
Проверить созданный systemd-сервис можно командой: systemctl status pm2-nodeapp --no-pager
При необходимости сервер можно перезагрузить: sudo reboot
После повторного подключения проверим: sudo -iu nodeapp pm2 status
Если nodeapp снова находится в статусе online, автозапуск настроен корректно.
Настраиваем Nginx
Node.js-приложение уже работает локально на 127.0.0.1:3000 под управлением PM2. Теперь поставим перед ним Nginx, который будет принимать внешние HTTP- и HTTPS-запросы и передавать их приложению как reverse proxy.
Такая схема позволяет не публиковать порт 3000 напрямую в интернет и централизованно управлять доменом, TLS-сертификатом и HTTP-заголовками.
Установка Nginx
Установим Nginx:
sudo apt update
sudo apt install -y nginx
Проверим состояние сервиса: sudo systemctl status nginx --no-pager
Также можно быстро проверить конфигурацию: sudo nginx -t
При корректной установке Nginx сообщит:
syntax is ok
test is successful
Если используется UFW, разрешим входящие HTTP- и HTTPS-подключения: sudo ufw allow 'Nginx Full'
Проверим правила: sudo ufw status
Порт 3000 отдельно открывать не требуется, поскольку Node.js-приложение принимает подключения только через localhost.
Создание reverse proxy
Создадим отдельную конфигурацию сайта: sudo nano /etc/nginx/sites-available/nodeapp
Добавим:
server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Здесь app.example.com необходимо заменить на домен или поддомен, который будет использоваться для приложения.
Основная директива: proxy_pass http://127.0.0.1:3000;
направляет запросы от Nginx к локальному Node.js-процессу.
Заголовки X-Real-IP, X-Forwarded-For и X-Forwarded-Proto позволяют приложению получать информацию об исходном клиенте и протоколе запроса.
Активируем конфигурацию:
sudo ln -s /etc/nginx/sites-available/nodeapp \
/etc/nginx/sites-enabled/nodeapp
Стандартный сайт Nginx можно отключить: sudo rm -f /etc/nginx/sites-enabled/default
Проверка конфигурации

Перед перезагрузкой Nginx обязательно проверим синтаксис: sudo nginx -t
Если ошибок нет, применим настройки: sudo systemctl reload nginx
До настройки DNS работу reverse proxy можно проверить локально, передав нужный Host header: curl -H "Host: app.example.com" http://127.0.0.1/
Nginx должен передать запрос Node.js-приложению и вернуть его ответ:
{
"app": "Node.js VPS Demo",
"version": "1.0.0",
"message": "Application is running"
}
Аналогично проверим health endpoint через Nginx: curl -H "Host: app.example.com" http://127.0.0.1/health
Теперь внешние запросы могут поступать через Nginx, а само приложение остаётся привязанным к локальному интерфейсу.
Подключаем домен
Для работы HTTPS приложению понадобится доменное имя, которое разрешается в публичный IP VPS.
DNS-запись
У DNS-провайдера создадим запись типа A.
Например:
| Type | Name | Value |
| A | app | 203.0.113.10 |
В результате app.example.com будет указывать на VPS.
203.0.113.10 здесь используется как документационный пример. В реальной DNS-записи необходимо указать фактический публичный IPv4-адрес сервера.
Если используется Cloudflare и сертификат выпускается непосредственно через Certbot на VPS, на этапе первоначальной настройки можно временно использовать режим DNS only. После получения сертификата проксирование при необходимости можно включить обратно.
Изменения DNS могут примениться не мгновенно: время зависит от TTL и DNS-провайдера.
Проверка разрешения домена

После создания записи проверим, какой адрес возвращает DNS: dig +short A app.example.com
Если dig не установлен: sudo apt install -y dnsutils
Также можно воспользоваться: nslookup app.example.com
Полученный адрес должен совпадать с публичным IP VPS.
Дополнительно проверим HTTP уже по доменному имени: curl http://app.example.com/
И health endpoint: curl http://app.example.com/health
Если DNS настроен правильно, запрос сначала попадёт на Nginx, а затем будет передан приложению на 127.0.0.1:3000.
После этого домен готов к выпуску TLS-сертификата. На следующем этапе установим Certbot и переведём приложение на HTTPS.
Настраиваем HTTPS
После настройки DNS и проверки HTTP-подключения переведём приложение на HTTPS. Для этого установим Certbot и выпустим бесплатный TLS-сертификат Let's Encrypt для домена приложения.
Установка Certbot
Установим Certbot и модуль интеграции с Nginx:
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
Проверим установленную версию: certbot --version
Перед выпуском сертификата убедимся, что конфигурация Nginx корректна: sudo nginx -t
Также домен должен уже указывать на публичный IP VPS, а порты 80 и 443 должны быть доступны извне.
Выпуск сертификата Let's Encrypt
Запустим Certbot для домена приложения: sudo certbot --nginx -d app.example.com
Во время первого запуска Certbot запросит email для уведомлений, согласие с условиями Let's Encrypt и предложит настроить перенаправление HTTP на HTTPS.
После успешной проверки домена Certbot получит сертификат и автоматически обновит конфигурацию Nginx.
Проверить существующие сертификаты можно командой: sudo certbot certificates
В выводе будет указан домен, срок действия и пути к файлам сертификата.
Let's Encrypt выдаёт сертификаты с ограниченным сроком действия, поэтому Certbot также настраивает автоматическое продление.
Проверить механизм обновления без реального выпуска нового сертификата можно так: sudo certbot renew --dry-run
Перенаправление HTTP → HTTPS
После настройки Certbot запросы к http://app.example.com должны автоматически перенаправляться на https://app.example.com.
Проверим заголовки HTTP: curl -I http://app.example.com
В ответе должен присутствовать redirect-код, например:
HTTP/1.1 301 Moved Permanently
Location: https://app.example.com/
Затем проверим HTTPS: curl -I https://app.example.com
Шифрование завершается на Nginx, поэтому внутреннее соединение между Nginx и Node.js может оставаться локальным HTTP.
Проверяем работу приложения
После настройки reverse proxy и HTTPS проверим основные маршруты приложения уже через публичный домен.
Проверка основной страницы
Откроем: https://app.example.com
Или выполним: curl https://app.example.com/
Node.js-приложение должно вернуть:
{
"app": "Node.js VPS Demo",
"version": "1.0.0",
"message": "Application is running"
}
Это подтверждает, что запрос проходит всю цепочку:
HTTPS
↓
Nginx
↓
127.0.0.1:3000
↓
Node.js
Также можно проверить HTTP-код без вывода тела ответа: curl -o /dev/null -s -w "%{http_code}\n" https://app.example.com/
Ожидаемый результат: 200
Проверка health endpoint

Теперь проверим отдельный endpoint состояния: curl https://app.example.com/health
Ожидаемый ответ:
{
"status": "ok",
"app": "Node.js VPS Demo",
"version": "1.0.0"
}
Дополнительно проверим HTTP-статус: curl -o /dev/null -s -w "%{http_code}\n" https://app.example.com/health
Результат: 200
Health endpoint удобен тем, что позволяет проверять доступность приложения отдельно от основной страницы. Его можно использовать в системах мониторинга и автоматических проверках состояния сервиса.
На этом этапе приложение уже полностью доступно через Nginx и HTTPS. Дальше проверим логи PM2 и Nginx, а затем обновим приложение с минимальным простоем.
Проверяем логи
После настройки PM2 и Nginx полезно проверить, что приложение работает без ошибок и корректно обрабатывает запросы. Для этого посмотрим логи самого Node.js-процесса и журналы reverse proxy.
Логи PM2
PM2 сохраняет стандартный вывод приложения и ошибки процесса.
Посмотреть последние записи можно командой: pm2 logs nodeapp --lines 50
В нашем случае в логе должна появиться строка запуска: Node.js VPS Demo is listening on 127.0.0.1:3000
Если приложение выводит дополнительные сообщения через console.log() или console.error(), они также будут отображаться здесь.
Для просмотра только ошибок можно использовать: pm2 logs nodeapp --err --lines 50
А пути к файлам журналов можно посмотреть командой: pm2 show nodeapp
PM2 особенно полезен при диагностике ситуаций, когда процесс неожиданно завершается, не может прочитать переменные окружения или получает необработанное исключение.
Логи Nginx
Nginx ведёт отдельные журналы запросов и ошибок.
Последние HTTP-запросы можно посмотреть так: sudo tail -n 50 /var/log/nginx/access.log
После обращения к основной странице и /health в журнале появятся записи примерно такого вида:
"GET / HTTP/1.1" 200
"GET /health HTTP/1.1" 200
Ошибки reverse proxy записываются в: sudo tail -n 50 /var/log/nginx/error.log
Если Node.js-приложение остановлено или Nginx не может подключиться к 127.0.0.1:3000, именно здесь обычно можно увидеть сообщения об ошибке соединения с upstream.
Для наблюдения за журналом в реальном времени используется: sudo tail -f /var/log/nginx/access.log
Совместная проверка PM2 и Nginx помогает быстро определить, на каком уровне возникла проблема: внутри самого Node.js-приложения или между клиентом, reverse proxy и локальным процессом.
Обновляем приложение без длительного простоя
В production приложение приходится регулярно обновлять. При обычной остановке и повторном запуске процесса между двумя операциями может возникнуть короткий период недоступности.
PM2 поддерживает команду reload, которая позволяет аккуратно заменить работающий процесс новой версией.
Изменение версии приложения
Для демонстрации изменим номер версии в .env.
Откроем файл: nano /var/www/nodeapp/.env
Заменим APP_VERSION=1.0.0 на APP_VERSION=1.1.0
Сохраним файл.
Также можно изменить текст ответа основной страницы или добавить новый функционал в server.js. Для простого теста достаточно изменения версии.
Перезапуск через PM2 reload
Поскольку изменились переменные окружения, выполним reload с их обновлением: pm2 reload nodeapp --update-env
После этого проверим статус: pm2 status
Приложение должно остаться в состоянии: online
Для более предсказуемого zero-downtime reload в production PM2 обычно используют в cluster mode с несколькими экземплярами приложения. В простом single-process сценарии reload сокращает простой, но не гарантирует его полное отсутствие.
Например, для cluster mode приложение можно запускать через ecosystem-файл с несколькими экземплярами:
module.exports = {
apps: [{
name: 'nodeapp',
script: 'server.js',
instances: 2,
exec_mode: 'cluster'
}]
};
Тогда PM2 может перезапускать экземпляры по очереди, сохраняя хотя бы один активный процесс во время обновления.
Проверка новой версии
После reload проверим основную страницу: curl https://app.example.com/
Теперь приложение должно вернуть:
{
"app": "Node.js VPS Demo",
"version": "1.1.0",
"message": "Application is running"
}
Health endpoint также должен продолжать отвечать: curl https://app.example.com/health
Ожидаемый результат:
{
"status": "ok",
"app": "Node.js VPS Demo",
"version": "1.1.0"
}
Дополнительно проверим PM2: pm2 status
Такой подход позволяет обновлять Node.js-приложение управляемо: PM2 отвечает за жизненный цикл процессов, а Nginx продолжает принимать внешние HTTPS-запросы и направлять их на локальный backend.
Заключение

Node.js-приложение на VPS можно развернуть без сложной платформы управления, если разделить инфраструктуру на несколько понятных компонентов: само приложение, PM2 для управления процессом и Nginx как внешний reverse proxy.
В этом руководстве мы установили Node.js LTS, создали отдельного системного пользователя, подготовили приложение и .env, настроили health endpoint, запустили сервис через PM2 и включили автозапуск после перезагрузки VPS. Затем подключили Nginx, домен и HTTPS через Let's Encrypt.
В завершение проверили логи PM2 и Nginx, убедились в корректной работе /health и обновили приложение через pm2 reload. Для production-нагрузки с требованиями к минимальному простою стоит использовать cluster mode с несколькими экземплярами процесса, чтобы PM2 мог заменять их поочерёдно.
FAQ
Зачем использовать PM2, если Node.js можно запустить через node server.js?
Команда node server.js запускает приложение только в текущем терминале. После закрытия SSH-сессии или перезагрузки VPS процесс остановится.
PM2 запускает приложение в фоне, отслеживает его состояние, сохраняет логи и позволяет автоматически восстановить процессы после перезагрузки сервера.
Нужно ли открывать порт 3000 в интернет?
Нет. В нашей схеме Node.js слушает только 127.0.0.1:3000, а внешние запросы принимает Nginx через порты 80 и 443.
Так backend не публикуется напрямую в интернет.
Зачем создавать отдельного пользователя nodeapp?
Запуск приложения от root даёт процессу лишние привилегии. Отдельный системный пользователь ограничивает доступ приложения к остальной системе и снижает последствия возможной уязвимости.
Где хранить .env?
Файл .env можно хранить в рабочей директории приложения, но доступ к нему следует ограничить правами файловой системы.
Его не следует добавлять в публичный Git-репозиторий. Обычно .env добавляют в .gitignore.
Для чего нужен health endpoint?
Endpoint вроде /health позволяет автоматически проверять, что приложение запущено и отвечает на HTTP-запросы.
Его можно использовать в системах мониторинга, reverse proxy, load balancer и внешних проверках доступности.
Чем отличаются логи PM2 и Nginx?
PM2 показывает вывод и ошибки самого Node.js-приложения.
Nginx хранит информацию о входящих HTTP-запросах и ошибках reverse proxy. Поэтому при диагностике полезно проверять оба уровня.
Что делать, если Nginx возвращает 502 Bad Gateway?
В первую очередь проверьте pm2 status и curl http://127.0.0.1:3000/health.
Если приложение не отвечает локально, проблема находится на стороне Node.js или PM2.
Если локальный запрос работает, следует проверить конфигурацию Nginx и журнал: sudo tail -n 50 /var/log/nginx/error.log
Обязательно ли использовать HTTPS?
Для публичного production-приложения HTTPS практически обязателен. Он защищает передаваемые данные и требуется многими браузерными API и внешними сервисами.
Let's Encrypt позволяет получить бесплатный TLS-сертификат и автоматически продлевать его через Certbot.
PM2 reload гарантирует полностью zero-downtime обновление?
Не всегда.
Для одного процесса reload может значительно сократить простой, но полноценное поочерёдное обновление лучше работает в cluster mode с несколькими экземплярами приложения.
Например, при двух экземплярах PM2 может перезапустить один процесс, пока второй продолжает обслуживать запросы.



