# Как развернуть Node.js-приложение: pm2 и nginx

> Пошаговый деплой Node.js на Linux-сервер: установка Node, npm ci, запуск через pm2 с автозапуском, nginx как обратный прокси, trust proxy в Express, HTTPS через certbot и 301-редиректы.

- URL: https://pakulichev.com/wiki/nodejs-pm2-nginx
- Author: Pakulichev (https://pakulichev.com/)
- Language: ru
- Updated: 2026-10-16
- Whole site for LLMs: https://pakulichev.com/llms.txt (index), https://pakulichev.com/llms-full.txt (full text)

Схема ниже подходит для VPS на Ubuntu или Debian: приложение на Node.js слушает локальный порт, pm2 держит его запущенным и поднимает после перезагрузки, nginx принимает трафик на 80 и 443 портах, терминирует TLS и проксирует запросы в Node. Во всех примерах домен `example.com`, приложение слушает порт 3000.

## Установка Node.js

Версия Node из стандартного репозитория дистрибутива обычно отстаёт на одну-две мажорные версии. Есть два нормальных способа поставить актуальную LTS.

Через менеджер версий (nvm, fnm). Node ставится в домашний каталог пользователя, версии легко переключать, `sudo` для `npm install -g` не нужен. Установщик nvm берите из README репозитория `nvm-sh/nvm`, затем:

```bash
nvm install --lts
nvm alias default 'lts/*'
node -v
```

Через репозиторий NodeSource. Node ставится системно в `/usr/bin`, обновляется вместе с остальными пакетами через `apt`:

```bash
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
```

Цифра в `setup_24.x` – мажорная версия. Проверьте на nodejs.org, какая ветка сейчас в статусе Active LTS, и подставьте её.

Приложение лучше запускать от отдельного непривилегированного пользователя, например `deploy`, и держать код в `/var/www/app` или `/home/deploy/app`.

## Зависимости и переменные окружения

На сервере зависимости ставятся строго по `package-lock.json`:

```bash
cd /var/www/app
npm ci --omit=dev
```

`npm ci` удаляет `node_modules` и ставит ровно те версии, что записаны в lock-файле. Если `package.json` и lock-файл расходятся, команда завершится ошибкой, и это полезно: на прод не уедут неожиданные версии. `--omit=dev` пропускает devDependencies (линтеры, тестовые фреймворки, сборщики). Если сборка фронтенда идёт на сервере, сначала выполните `npm ci`, затем `npm run build`, а потом `npm prune --omit=dev`.

Секреты (пароль к базе, API-ключи) хранятся в файле `.env` рядом с приложением, с правами `600`, и не попадают в git. Начиная с Node 20.12 файл можно прочитать без сторонних пакетов:

```js
process.loadEnvFile(); // читает .env из текущего каталога, бросает ошибку, если файла нет
const port = Number(process.env.PORT) || 3000;
```

На старых версиях то же делает пакет `dotenv`: `require("dotenv").config()`.

## Запуск через pm2

```bash
npm install -g pm2
pm2 start server.js --name app
```

Для постоянной конфигурации удобнее `ecosystem.config.js` в корне проекта:

```js
module.exports = {
  apps: [
    {
      name: "app",
      script: "./server.js",
      cwd: "/var/www/app",
      instances: 2,
      exec_mode: "cluster",
      max_memory_restart: "300M",
      env: {
        NODE_ENV: "production",
        PORT: 3000,
      },
    },
  ],
};
```

```bash
pm2 start ecosystem.config.js
pm2 save
pm2 startup
```

`pm2 save` записывает текущий список процессов в `~/.pm2/dump.pm2`. `pm2 startup` печатает команду с `sudo`, которую нужно скопировать и выполнить: она создаёт systemd-юнит, и после перезагрузки сервера pm2 восстановит сохранённый список. После любого изменения состава процессов снова выполняйте `pm2 save`.

Повседневные команды:

```bash
pm2 ls                         # список процессов, статус, память, число рестартов
pm2 logs app --lines 100       # последние 100 строк логов и дальше в реальном времени
pm2 reload app                 # перезапуск без простоя в cluster mode
pm2 restart app --update-env   # перезапуск с переменными из текущего окружения shell
pm2 install pm2-logrotate      # ротация логов, иначе ~/.pm2/logs растёт бесконечно
```

pm2 запоминает окружение процесса на момент первого запуска. Если вы поменяли переменную в shell или в `env` ecosystem-файла, обычный `pm2 restart` её не подхватит, нужен флаг `--update-env`. Переменные из `.env` приложение перечитывает само при каждом старте, поэтому для них хватает обычного рестарта.

## nginx как обратный прокси

Node должен слушать только локальный интерфейс, чтобы порт 3000 не был доступен снаружи:

```js
app.listen(port, "127.0.0.1");
```

Конфиг `/etc/nginx/sites-available/example.com`:

```nginx
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.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 $connection_upgrade;
    }
}
```

```bash
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```

`Host` передаёт исходный домен, без него Express увидит `127.0.0.1:3000`. `X-Forwarded-For` несёт IP клиента, `X-Forwarded-Proto` – схему (http или https). `map` и заголовки `Upgrade`/`Connection` нужны для WebSocket; если их нет в приложении, эти строки можно убрать.

## trust proxy в Express

За прокси Express по умолчанию игнорирует заголовки `X-Forwarded-*`: `req.ip` равен `127.0.0.1`, `req.protocol` всегда `http`, `req.secure` всегда `false`. От этого ломаются rate limiting по IP, логи, cookie с флагом `secure` в `express-session` и любые редиректы, которые строят абсолютный URL.

```js
app.set("trust proxy", 1);
```

Число `1` означает «доверять одному прокси перед приложением», то есть nginx. Значение `true` доверяет всей цепочке `X-Forwarded-For`, и клиент сможет подставить в заголовок любой IP. Если перед nginx стоит ещё CDN или балансировщик, ставьте `2` или перечислите их адреса.

## HTTPS через certbot и редиректы

```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run
```

Certbot получит сертификат Let's Encrypt на оба имени, допишет в конфиг блок `listen 443 ssl` и редирект с HTTP. Автопродление ставится вместе с пакетом.

Дальше стоит оставить один канонический адрес: все варианты (http, www, дополнительные домены) отдают 301 на `https://example.com`. Итоговая структура конфига:

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;

    location / {
        proxy_pass http://127.0.0.1:3000;
        # те же proxy_set_header, что выше
    }
}
```

Дополнительный домен (например, старый `example.net`) добавляется в `server_name` первого и второго блоков, но для редиректа по HTTPS сертификат должен покрывать и его: перевыпустите сертификат с дополнительными `-d`. HTTP/2 в nginx 1.25.1 и новее включается директивой `http2 on;` внутри блока `server`, в более старых версиях – параметром `listen 443 ssl http2`. Версию покажет `nginx -v`.

Код 301 сообщает поисковикам, что адрес переехал навсегда, и они склеивают сигналы со старых URL на канонический. Что ещё настроить на стороне Express для поисковиков, разобрано в статье [SEO для сайта на Express](https://pakulichev.com/wiki/express-seo).

## FAQ

### Зачем pm2, если есть systemd?

systemd тоже умеет перезапускать Node.js-процесс и поднимать его после перезагрузки. pm2 добавляет cluster mode с перезапуском без простоя (pm2 reload), просмотр логов и метрик одной командой и удобное управление несколькими приложениями. Для одного простого сервиса хватит и unit-файла systemd.

### Почему req.ip в Express за nginx всегда 127.0.0.1?

Express видит соединение от nginx. Нужно передавать заголовок X-Forwarded-For в nginx (proxy_set_header) и включить app.set("trust proxy", 1), тогда req.ip возьмёт адрес клиента из заголовка.

### Нужно ли продлевать сертификат certbot вручную?

Нет. Пакет certbot ставит systemd-таймер или cron-задачу, которая дважды в сутки проверяет сертификаты и продлевает те, что истекают в ближайшие 30 дней. Проверить, что продление сработает, можно командой sudo certbot renew --dry-run.
