Как развернуть Node.js-приложение на VPS с PM2, Nginx и SSL

Валерий Волков

Время прочтения 10 минут

В этом руководстве развернём небольшое 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.

Например:

TypeNameValue
Aapp203.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 может перезапустить один процесс, пока второй продолжает обслуживать запросы.

Список источников

  1. Node.js — Download / Releases
  2. PM2 Documentation — Quick Start and Startup Script
  3. Nginx Documentation — Proxying HTTP Traffic to a Group of Servers
  4. Certbot Documentation — Nginx

Подпишитесь на нашу рассылку и получайте статьи и новости

    Ознакомьтесь с другими нашими материалами