Деплой NextJS на свой VPS — nginx + PM2

Пошаговый гайд по деплою NextJS-приложения на собственный сервер. Настройка nginx, PM2, SSL, автодеплой через GitHub Actions.

Дмитрий Мещеряков
Дмитрий Мещеряков
📅 4 сентября 2026 г.📖 6 мин чтения

Развернуть Next.js на своём сервере — вечер работы. Сделать так, чтобы это пережило первый инцидент, — задача другого масштаба, и разница между двумя состояниями обнаруживается обычно ночью.

Ниже — рабочая конфигурация с PM2 и nginx, а в конце то, чего в ней не хватает для спокойного сна.

Почему свой VPS, а не Vercel

Vercel — отличный выбор для большинства проектов. Но иногда нужен свой сервер:

  • Данные должны храниться в России (152-ФЗ)
  • Нужна интеграция с локальной инфраструктурой
  • Высокий трафик — дешевле держать свой сервер
  • Специфические требования к окружению
💡 Совет

Что получите: NextJS на production с nginx reverse proxy, SSL от Let's Encrypt, автоперезапуск через PM2, CI/CD через GitHub Actions.

Шаг 1: Подготовка сервера

Арендуем VPS (Ubuntu 22.04 LTS). Минимум: 1 vCPU, 2 GB RAM.

bash
# Подключаемся к серверу
ssh root@your-server-ip
# Обновляем систему
apt update && apt upgrade -y
# Создаём пользователя для деплоя
adduser deploy
usermod -aG sudo deploy
# Настраиваем SSH-ключи
su - deploy
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Добавляем свой публичный ключ в ~/.ssh/authorized_keys

Шаг 2: Установка Node.js

bash
# Устанавливаем Node.js 20 LTS через nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20
nvm alias default 20
# Проверяем
node -v  # v20.x.x
npm -v   # 10.x.x

Шаг 3: Установка PM2

PM2 — менеджер процессов Node.js с автоперезапуском и мониторингом.

bash
npm install -g pm2
# Автозапуск при перезагрузке сервера
pm2 startup systemd
# Выполните команду, которую выдаст pm2

Шаг 4: Клонируем проект

bash
cd /var/www
sudo mkdir -p myapp
sudo chown deploy:deploy myapp
cd myapp
git clone https://github.com/yourusername/your-nextjs-app.git .
npm ci
npm run build

Шаг 5: Конфигурация PM2

Создаём конфигурационный файл:

javascript
// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'nextjs-app',
      script: 'npm',
      args: 'start',
      cwd: '/var/www/myapp',
      instances: 'max', // Используем все ядра CPU
      exec_mode: 'cluster',
      env: {
        NODE_ENV: 'production',
        PORT: 3000,
      },
      // Автоперезапуск при падении
      autorestart: true,
      watch: false,
      max_memory_restart: '1G',
      // Логи
      error_file: '/var/log/pm2/nextjs-error.log',
      out_file: '/var/log/pm2/nextjs-out.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    },
  ],
};

Запускаем:

bash
# Создаём папку для логов
sudo mkdir -p /var/log/pm2
sudo chown deploy:deploy /var/log/pm2
# Запускаем приложение
pm2 start ecosystem.config.js
# Сохраняем конфигурацию для автозапуска
pm2 save
# Проверяем статус
pm2 status
pm2 logs nextjs-app

Шаг 6: Настройка nginx

bash
sudo apt install nginx -y

Создаём конфигурацию сайта:

nginx
# /etc/nginx/sites-available/myapp
upstream nextjs_upstream {
    server 127.0.0.1:3000;
    keepalive 64;
}
server {
    listen 80;
    server_name myapp.ru www.myapp.ru;
    # Редирект на HTTPS (после настройки SSL)
    # return 301 https://$server_name$request_uri;
    location / {
        proxy_pass http://nextjs_upstream;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
        # Таймауты
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    # Статика NextJS — отдаём напрямую через nginx
    location /_next/static {
        alias /var/www/myapp/.next/static;
        expires 1y;
        access_log off;
        add_header Cache-Control "public, immutable";
    }
    # Публичные файлы
    location /public {
        alias /var/www/myapp/public;
        expires 1y;
        access_log off;
    }
    # Favicon и robots
    location = /favicon.ico {
        alias /var/www/myapp/public/favicon.ico;
        access_log off;
    }
    location = /robots.txt {
        alias /var/www/myapp/public/robots.txt;
        access_log off;
    }
    # Gzip
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
    gzip_min_length 1000;
}

Активируем:

bash
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Шаг 7: SSL-сертификат (Let's Encrypt)

bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d myapp.ru -d www.myapp.ru
# Автопродление
sudo certbot renew --dry-run

После получения сертификата nginx-конфиг автоматически обновится.

Шаг 8: Автодеплой через GitHub Actions

yaml
# .github/workflows/deploy.yml
name: Deploy to VPS
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Deploy to server
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/myapp
            git pull origin main
            npm ci
            npm run build
            pm2 reload nextjs-app

Настройка секретов

В Settings → Secrets → Actions добавьте:

  • SSH_HOST — IP сервера
  • SSH_USER — пользователь (deploy)
  • SSH_PRIVATE_KEY — приватный ключ
⚠️ Важно

Безопасность: Никогда не коммитьте приватные ключи в репозиторий. Используйте GitHub Secrets или аналоги.

Шаг 9: Мониторинг

PM2 Dashboard

bash
# Статус процессов
pm2 status
# Мониторинг в реальном времени
pm2 monit
# Логи
pm2 logs nextjs-app --lines 100

Интеграция с PM2.io (опционально)

bash
pm2 link <secret_key> <public_key>

Полезные команды

bash
# Перезапуск приложения
pm2 reload nextjs-app
# Полный рестарт
pm2 restart nextjs-app
# Остановка
pm2 stop nextjs-app
# Удаление из PM2
pm2 delete nextjs-app
# Очистка логов
pm2 flush
# Проверка nginx
sudo nginx -t
sudo systemctl reload nginx
# Просмотр логов nginx
sudo tail -f /var/log/nginx/error.log

Оптимизация производительности

1. Увеличиваем лимиты системы

bash
# /etc/security/limits.conf
* soft nofile 65535
* hard nofile 65535

2. Настройка nginx для высокой нагрузки

nginx
# /etc/nginx/nginx.conf
worker_processes auto;
worker_rlimit_nofile 65535;
events {
    worker_connections 4096;
    use epoll;
    multi_accept on;
}

3. Включаем HTTP/2

nginx
listen 443 ssl http2;

Чего здесь не хватает для продакшена

Описанная конфигурация рабочая, но её стоит дополнить — перечисляю в порядке важности.

Развёртывание не атомарно. git pull плюс npm run build плюс перезапуск означает, что во время сборки приложение либо работает на старом коде с новыми файлами, либо недоступно. Схема с отдельными директориями релизов и переключением симлинка решает и это, и откат: вернуться на предыдущую версию становится вопросом одной команды, а не повторного развёртывания.

Нет отката. Сейчас единственный способ вернуться назад — выкатить предыдущий коммит заново, то есть заново собрать. В момент инцидента это те самые минуты, которых не хватает.

Резервное копирование не настроено. Оно и не относится к развёртыванию напрямую, но именно на своём сервере это полностью ваша задача — включая проверку восстановления. Бэкап, который ни разу не разворачивали, бэкапом не является.

Нет мониторинга снаружи. PM2 перезапустит упавший процесс, но не заметит, что приложение отвечает ошибкой или что закончилось место на диске. Нужна внешняя проверка доступности с уведомлением — это бесплатно и настраивается за десять минут.

Автообновления безопасности. unattended-upgrades на Debian/Ubuntu — минимальная гигиена для сервера, смотрящего в интернет.

Firewall и SSH. Доступ по ключу, вход root по паролю отключён, открыты только нужные порты. Пункт очевидный ровно до момента, когда в логах обнаруживаются тысячи попыток подбора.

Итоги

Настроенная конфигурация закрывает базу: сборка, процесс-менеджер с автоперезапуском, обратный прокси с кэшированием статики, сертификат и автоматическое развёртывание из репозитория.

Что стоит держать в голове дальше:

PM2 — не единственный вариант. systemd даёт то же самое без дополнительной зависимости и лучше интегрирован с системой; Docker удобнее, когда окружений несколько или нужна воспроизводимость. PM2 хорош простотой и кластеризацией из коробки — этого достаточно для большинства проектов, но выбор стоит сделать осознанно.

Число процессов в кластере подбирайте по памяти, а не по числу ядер. Каждый экземпляр Next.js держит свою копию приложения в памяти, и на сервере с двумя гигабайтами instances: 'max' приводит к тому, что процессы начинают убиваться системой под нагрузкой — то есть ровно тогда, когда они нужнее всего.

Проверьте, что приложение переживает перезагрузку сервера. pm2 startup и pm2 save — две команды, отсутствие которых обнаруживается после первой же незапланированной перезагрузки.

Частые вопросы

Как запустить Next.js на VPS?

Установить Node LTS через nvm, склонировать проект, выполнить npm ci и npm run build, запустить через PM2 с конфигурацией в ecosystem.config.js и поставить перед приложением nginx обратным прокси на 127.0.0.1:3000. Статику из /_next/static отдавайте напрямую через nginx с годовым сроком кеширования — до Node эти запросы доходить не должны. Сертификат ставится утилитой certbot с плагином nginx.

Сколько процессов ставить в кластере PM2?

Подбирайте по памяти, а не по числу ядер. Каждый экземпляр Next.js держит свою копию приложения в памяти, поэтому на сервере с двумя гигабайтами значение instances в режиме max приводит к тому, что процессы начинают убиваться системой под нагрузкой — то есть ровно тогда, когда они нужнее всего. Ограничение max_memory_restart тоже стоит выставить осознанно.

PM2, systemd или Docker?

systemd даёт то же самое без дополнительной зависимости и лучше интегрирован с системой. Docker удобнее, когда окружений несколько или нужна воспроизводимость. PM2 хорош простотой и кластеризацией из коробки — этого достаточно для большинства проектов, но выбор стоит сделать осознанно, а не по инерции. И не забудьте команды pm2 startup и pm2 save: их отсутствие обнаруживается после первой же незапланированной перезагрузки сервера.

Почему развёртывание через git pull не атомарно?

Потому что связка git pull, npm ci, npm run build и перезапуск означает, что во время сборки приложение либо работает на старом коде с новыми файлами, либо недоступно. Схема с отдельными директориями релизов и переключением симлинка решает и это, и откат: вернуться на предыдущую версию становится вопросом одной команды, а не повторной сборки. Сейчас единственный способ откатиться — выкатить предыдущий коммит заново, и в момент инцидента это те самые минуты, которых не хватает.

Достаточно ли PM2 для мониторинга?

Нет. PM2 перезапустит упавший процесс, но не заметит, что приложение отвечает ошибкой, что закончилось место на диске или что сайт недоступен снаружи из-за конфигурации. Нужна внешняя проверка доступности с уведомлением — это бесплатно и настраивается за десять минут.

Что настроить на сервере кроме приложения?

Автообновления безопасности через unattended-upgrades — минимальная гигиена для сервера, смотрящего в интернет. Firewall с открытыми только нужными портами, вход по SSH-ключу и отключённый вход root по паролю: пункт очевидный ровно до момента, когда в логах обнаруживаются тысячи попыток подбора. И резервное копирование, которое на своём сервере целиком ваша задача, включая проверку восстановления: бэкап, который ни разу не разворачивали, бэкапом не является.