Налаштування VPS, яке реально працює: Node.js, PM2, Nginx і деплої без простою
Точне налаштування VPS для деплою, яке я використовую в продакшені — захист Ubuntu, кластерний режим PM2, зворотний проксі Nginx, SSL і скрипт деплою, який мене ще не підводив. Без теорії, тільки те, що працює.
Цей блог працює на VPS за $10/місяць. Не Vercel, не AWS, не кластер Kubernetes, яким керує команда з шести осіб. Один сервер Ubuntu з Nginx, PM2 і bash-скриптом, який деплоїть менш ніж за 30 секунд.
Я пробував інші шляхи. Використовував Vercel (чудово, поки вам не потрібні cron-завдання, постійні WebSocket-з'єднання або просто контроль). Використовував AWS (чудово, якщо вам подобається проводити половину дня в IAM-політиках). Я завжди повертаюся до VPS.
Але ось проблема: кожен туторіал "деплой на VPS" в інтернеті зупиняється на щасливому шляху. Вам показують, як встановити Node.js і запустити node server.js, і називають це продакшеном. Потім ваш сервер брутфорсять через SSH, процес падає о 3 ночі, бо ніхто не налаштував менеджер процесів, а ваш SSL-сертифікат закінчився три місяці тому.
Це гайд, який я хотів би мати. Все тут перевірено в бою — саме це налаштування обслуговує сторінку, яку ви зараз читаєте.
Почніть з безпеки, а не з коду#
Перш ніж навіть думати про Node.js, захистіть сервер. Свіжі інстанси VPS — це мішені. Автоматизовані боти починають атакувати ваш SSH-порт за лічені хвилини після створення.
Створіть користувача без root-доступу#
adduser deploy
usermod -aG sudo deployНалаштуйте SSH-автентифікацію за ключем#
На вашій локальній машині:
ssh-copy-id deploy@your-server-ipПотім повністю вимкніть автентифікацію за паролем:
sudo nano /etc/ssh/sshd_configPasswordAuthentication no
PermitRootLogin nosudo systemctl restart sshdЯкщо ви пропустите це, ви побачите тисячі невдалих спроб входу у ваших логах автентифікації за кілька днів. Це не параноя — це звичайний вівторок у публічному інтернеті.
Фаєрвол з UFW#
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableОсь і все. Чотири правила. Тільки SSH і веб-трафік проходять.
Fail2Ban#
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.localВідредагуйте /etc/fail2ban/jail.local:
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600sudo systemctl enable fail2ban
sudo systemctl start fail2banТри невдалі спроби SSH — і вас банять на годину. Я спостерігав, як Fail2Ban блокує сотні IP за один день. Це працює.
Автоматичні оновлення безпеки#
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgradesТепер ваш сервер автоматично встановлюватиме патчі безпеки. Однією турботою менше.
Node.js: використовуйте NVM, а не apt#
Я бачу це в кожному туторіалі: sudo apt install nodejs. Не робіть так.
Репозиторії пакетів Ubuntu постачають древні версії Node.js. Навіть NodeSource PPA відстає. І коли вам потрібно перемикатися між Node 20 і Node 22 для різних проєктів, ви застрягаєте.
NVM вирішує це:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm alias default lts/*Тепер перевірте:
node -v # v22.x.x або яка там зараз LTS
npm -vНеочевидна порада: коли ви встановлюєте глобальні пакети з NVM (як PM2), вони прив'язані до конкретної версії Node. Якщо ви перемикаєте версію через nvm use, ваші глобальні пакети зникають. Встановіть версію за замовчуванням і дотримуйтесь її на сервері:
nvm alias default 22Мене це вкусило рівно один раз. Одного разу було достатньо.
PM2: менеджер процесів, який виправдовує себе#
PM2 — це різниця між "задеплоєно" та "готово до продакшену". Він обробляє керування процесами, кластеризацію, ротацію логів, автоперезапуск при збоях та скрипти запуску. Безкоштовно.
Встановлення та налаштування#
npm install -g pm2Конфігурація екосистеми#
Не запускайте додатки з CLI-прапорцями. Використовуйте файл ecosystem.config.js. Він версіонується, відтворюваний і самодокументований.
// ecosystem.config.js
module.exports = {
apps: [
{
name: "akousa",
script: "node_modules/.bin/next",
args: "start -p 3002",
cwd: "/var/www/akousa.net",
instances: 2,
exec_mode: "cluster",
max_memory_restart: "500M",
env: {
NODE_ENV: "production",
PORT: 3002,
},
// Граціозне завершення
kill_timeout: 5000,
listen_timeout: 10000,
wait_ready: false,
// Логування
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
error_file: "/var/log/pm2/akousa-error.log",
out_file: "/var/log/pm2/akousa-out.log",
merge_logs: true,
// Автоперезапуск при збої
autorestart: true,
max_restarts: 10,
min_uptime: "10s",
// Не спостерігати в продакшені
watch: false,
},
],
};Дозвольте пояснити рішення, які мають значення:
instances: 2 замість "max": На невеликому VPS з 1-2 ядрами "max" звучить розумно, але породжує процеси, які змагаються за ресурси під час збірки. Два інстанси дають вам перезапуск без простою, залишаючи запас. На машині з 4+ ядрами — так, використовуйте "max".
exec_mode: "cluster": Саме це дає можливість перезавантаження без простою. Без кластерного режиму pm2 reload — просто гарний рестарт. З кластерним режимом PM2 перезапускає інстанси по одному — ваш додаток ніколи повністю не йде в офлайн.
max_memory_restart: "500M": У вашому додатку Next.js витік пам'яті? PM2 перезапустить його до того, як він вб'є ваш сервер через OOM. Це рятувало мене від тривог о 2 ночі не раз.
kill_timeout: 5000: Дає вашому додатку 5 секунд для завершення поточних запитів перед тим, як PM2 примусово зупинить його. Значення за замовчуванням (1600 мс) занадто агресивне для додатків з підключеннями до бази даних.
watch: false: Я бачив, як люди залишають watch: true в продакшені. Тоді PM2 перезапускає додаток щоразу, коли змінюється файл логу. Ваш додаток входить у цикл перезапусків. Не робіть так.
Скрипт запуску#
Зробіть так, щоб PM2 переживав перезавантаження:
pm2 startup systemd
# Скопіюйте і виконайте команду, яку він виведе
pm2 saveЦе генерує systemd-сервіс. Після перезавантаження сервера ваш додаток повертається автоматично. Перевірте — перезавантажте сервер і переконайтеся. Не припускайте.
Ротація логів#
Логи рано чи пізно з'їдять ваш диск. Встановіть модуль ротації:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true50 МБ максимум на файл, зберігати 7 ротованих файлів, стискати старі. Без цього я бачив, як /var/log заповнював 25 ГБ диск за три тижні на додатку із середнім трафіком.
Nginx: зворотний проксі, який робить більше, ніж ви думаєте#
"Навіщо не виставити Node.js напряму на порт 80?"
Тому що Nginx обробляє речі, на які Node.js не повинен витрачати цикли: термінація SSL, роздача статичних файлів, gzip-стиснення, буферизація запитів, ліміти з'єднань і граціозна обробка повільних клієнтів. Він написаний на C і створений саме для цього.
Встановлення#
sudo apt install nginx -yКонфігурація#
# /etc/nginx/sites-available/akousa.net
upstream node_app {
server 127.0.0.1:3002;
keepalive 64;
}
server {
listen 80;
listen [::]:80;
server_name akousa.net www.akousa.net;
# Перенаправлення всього HTTP на HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name akousa.net www.akousa.net;
# SSL (керується Certbot — ці рядки додаються автоматично)
ssl_certificate /etc/letsencrypt/live/akousa.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/akousa.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Заголовки безпеки
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Gzip-стиснення
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
application/wasm;
# Налаштування проксі
location / {
proxy_pass http://node_app;
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;
# Підтримка WebSocket (якщо колись знадобиться)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Таймаути — щедрі, але не безкінечні
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Буферизація — нехай Nginx обробляє повільних клієнтів
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
}
# Статичні ресурси Next.js — нехай Nginx роздає їх напряму
location /_next/static/ {
alias /var/www/akousa.net/.next/static/;
expires 365d;
access_log off;
add_header Cache-Control "public, immutable";
}
# Публічні статичні файли
location /static/ {
alias /var/www/akousa.net/public/static/;
expires 30d;
access_log off;
}
# Блокування доступу до прихованих файлів
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}Увімкніть конфігурацію:
sudo ln -s /etc/nginx/sites-available/akousa.net /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxЗавжди запускайте nginx -t перед перезавантаженням. Я одного разу запушив зламану конфігурацію і поклав сайт, бо пропустив перевірку синтаксису. П'ять символів nginx -t зберегли б мені тридцять хвилин панічного дебагу.
Речі, які більшість туторіалів пропускають у цій конфігурації:
Блок upstream з keepalive 64: Nginx перевикористовує з'єднання до вашого Node.js бекенду замість відкриття нового TCP-з'єднання для кожного запиту. Це має значення під навантаженням.
proxy_buffering on: Nginx зчитує всю відповідь від Node.js у пам'ять, а потім відправляє її клієнту з тією швидкістю, яку клієнт може обробити. Без цього повільний клієнт на 3G-з'єднанні блокує ваш Node.js worker.
Роздача _next/static/ напряму: Це хешовані, незмінні ресурси. Нехай Nginx роздає їх з диска із заголовком кешу на 365 днів. Ваші Node.js процеси не повинні витрачати на це час.
SSL за п'ять хвилин#
Let's Encrypt вирішив проблему SSL. Якщо ви все ще платите за сертифікати у 2026 році, зупиніться.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d akousa.net -d www.akousa.netCertbot запитає вашу електронну пошту, прийме ToS і автоматично змінить вашу конфігурацію Nginx, додавши SSL-директиви. Ось і все.
Перевірка автоподовження#
Certbot встановлює systemd-таймер, який перевіряє двічі на день і подовжує сертифікати за 30 днів до закінчення:
sudo systemctl list-timers | grep certbotПеревірте, що подовження працює:
sudo certbot renew --dry-runЯкщо тестовий запуск пройшов — ви більше ніколи не думатимете про SSL. Якщо він зазнав невдачі, зазвичай причина в тому, що порт 80 заблокований (перевірте правила UFW) або Nginx не запущений.
Одна річ, що мене зловила: якщо ви налаштували Nginx до запуску Certbot, переконайтеся, що ваш серверний блок слухає порт 80 без HTTPS-перенаправлення спочатку. Certbot потрібен доступ до порту 80 для HTTP-01 челенджу. Після успішного запуску Certbot тоді додавайте перенаправлення.
Скрипт деплою#
Це скрипт, який запускається щоразу, коли я пушу в продакшен. Без CI/CD платформи, без GitHub Actions. Просто SSH і bash.
#!/bin/bash
# deploy.sh — деплой майже без простою
set -euo pipefail
APP_DIR="/var/www/akousa.net"
APP_NAME="akousa"
LOG_FILE="/var/log/deploy.log"
HEALTH_URL="http://localhost:3002"
MAX_RETRIES=10
RETRY_INTERVAL=3
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "=== Деплой розпочато ==="
cd "$APP_DIR"
# Отримання останнього коду
log "Отримання останніх змін..."
git pull origin main 2>&1 | tee -a "$LOG_FILE"
# Встановлення залежностей
log "Встановлення залежностей..."
npm install --legacy-peer-deps 2>&1 | tee -a "$LOG_FILE"
# Збірка
log "Збірка додатку..."
rm -rf .next
npm run build 2>&1 | tee -a "$LOG_FILE"
if [ $? -ne 0 ]; then
log "ПОМИЛКА: Збірка не вдалася. Деплой перервано."
exit 1
fi
# Перезавантаження PM2 (без простою в кластерному режимі)
log "Перезавантаження PM2..."
pm2 reload "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
pm2 save 2>&1 | tee -a "$LOG_FILE"
# Перевірка здоров'я з повторними спробами
log "Запуск перевірки здоров'я..."
for i in $(seq 1 $MAX_RETRIES); do
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' "$HEALTH_URL" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
log "Перевірка здоров'я пройдена (HTTP $HTTP_CODE)"
log "=== Деплой завершено успішно ==="
exit 0
fi
log "Спроба перевірки здоров'я $i/$MAX_RETRIES (HTTP $HTTP_CODE). Повтор через ${RETRY_INTERVAL}с..."
sleep $RETRY_INTERVAL
done
log "ПОМИЛКА: Перевірка здоров'я не вдалася після $MAX_RETRIES спроб"
log "Відкат до попереднього стану PM2..."
pm2 restart "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
exit 1Зробіть його виконуваним:
chmod +x deploy.shДеплой з локальної машини:
ssh root@your-server-ip "bash /var/www/akousa.net/deploy.sh"Ключові рішення в цьому скрипті:
set -euo pipefail: Скрипт негайно завершується при будь-якій помилці. Без цього невдалий npm install мовчки продовжить виконання до етапу збірки, і ви отримаєте криптичну помилку, на дебаг якої витратите 20 хвилин.
rm -rf .next перед збіркою: Next.js має кеш збірки, який іноді видає застарілий результат. Мене це вкусило одного разу — сторінка показувала старий контент, хоча вихідний код було оновлено. Видалення директорії збірки додає може 15 секунд до збірки, але гарантує свіжий результат.
pm2 reload замість pm2 restart: Це і є частина без простою. У кластерному режимі reload виконує поступовий перезапуск — піднімає нові інстанси з оновленим кодом, чекає їх готовності, потім граціозно зупиняє старі. В жоден момент не працює нуль інстансів.
Перевірка здоров'я з повторними спробами: Next.js потребує кілька секунд для розігріву після перезапуску. Скрипт чекає до 30 секунд (10 спроб x 3 секунди), перевіряючи, чи додаток відповідає HTTP 200. Якщо ні — щось не так, і вам потрібно знати негайно, а не дізнатися від користувача.
Відкат при невдачі: Якщо перевірка здоров'я не вдається після всіх спроб, скрипт перезапускає PM2 (який завантажує останній збережений стан). Це не ідеальний відкат, але краще, ніж залишити сервер у зламаному стані.
Коли щось ламається о 2 ночі#
Ось що я реально дебажив на цьому точному налаштуванні:
"Сайт не працює"#
Перші команди для запуску:
pm2 status
pm2 logs akousa --lines 50
sudo systemctl status nginx
sudo tail -50 /var/log/nginx/error.logУ дев'яти випадках з десяти pm2 logs одразу показує, що сталося. Відсутня змінна середовища, невдале підключення до бази даних або необроблений відхилений проміс.
"Пам'ять постійно зростає"#
pm2 monitЦе дає вам живий дашборд CPU та пам'яті по кожному процесу. Якщо пам'ять стабільно зростає без вирівнювання, у вас витік. Налаштування max_memory_restart у конфігурації екосистеми — ваша сітка безпеки — PM2 перезапустить процес до того, як він покладе сервер.
Для глибшого дослідження:
pm2 describe akousaЦе показує аптайм, кількість перезапусків та знімки пам'яті. Якщо бачите 47 перезапусків за останні 24 години — це ваша підказка.
"SSL-сертифікат закінчився"#
sudo certbot certificatesПоказує всі сертифікати з датами закінчення. Якщо автоподовження не спрацювало:
sudo certbot renew --force-renewal
sudo systemctl reload nginx"Дисковий простір закінчився"#
df -h
du -sh /var/log/*
pm2 flushpm2 flush негайно очищує всі файли логів PM2. Якщо ви не налаштували ротацію логів (я вас попереджав), саме тут ви відчуєте біль.
Команда, яку я запускаю щоранку#
ssh deploy@akousa.net "pm2 status && df -h / && uptime"Три речі в одному рядку: чи працюють мої процеси, чи все гаразд з диском, чи не перевантажений сервер. Займає дві секунди. Ловить проблеми раніше за користувачів.
Чого більшість гайдів не розкаже#
Ваш етап збірки — ваша найбільша вразливість. На VPS з 1 ГБ RAM npm run build для додатку Next.js може споживати 800+ МБ пам'яті. Якщо PM2 запускає ваш додаток у двох інстансах під час збірки, ви отримаєте OOM. Рішення: використовуйте swap-файл (мінімум 2 ГБ) або зупиніть додаток під час збірки і погодьтеся з кількома секундами простою. Я використовую swap.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab--legacy-peer-deps у вашій команді install — це кодовий запах, а не рішення. Я використовую його, бо деякі пакети в моєму дереві залежностей не оновили діапазони peer-залежностей. Раз на кілька місяців я пробую прибрати його. Колись це спрацює. А поки що — я шіпаю.
Тестуйте скрипт деплою з нуля. Клонуйте репозиторій на свіжий сервер і виконайте кожен крок вручну. Кількість проблем "працює на моїй машині", що ховаються в скриптах деплою, — вражаюча. Я знайшов три проблеми у своєму, коли зробив це — відсутні глобальні пакети, неправильні права на файли і шлях, який існував тільки через попереднє ручне налаштування.
Додайте IP сервера в SSH-конфіг. Перестаньте набирати IP-адреси:
# ~/.ssh/config
Host akousa
HostName 69.62.66.94
User deploy
IdentityFile ~/.ssh/id_ed25519Тепер ssh akousa — це все, що вам потрібно. Маленькі речі накопичуються.
Повний чекліст#
Перш ніж вважати роботу завершеною:
- Користувач без root-доступу з sudo
- Тільки SSH-ключова автентифікація, парольна вимкнена
- UFW увімкнений з відкритими лише необхідними портами
- Fail2Ban захищає SSH
- Автоматичні оновлення безпеки увімкнені
- Node.js встановлений через NVM
- PM2 запускає ваш додаток у кластерному режимі
- Скрипт запуску PM2 налаштований (переживає перезавантаження)
- Ротація логів PM2 встановлена
- Зворотний проксі Nginx з правильними заголовками
- SSL через Let's Encrypt з автоподовженням
- Скрипт деплою з перевірками здоров'я
- Swap-файл налаштований (запас для збірки)
- Перевірено: перезавантажте сервер і переконайтеся, що все повертається
Останній пункт — той, що люди пропускають. Не будьте цією людиною. Перезавантажте сервер, зачекайте 60 секунд і перевірте, чи ваш додаток живий. Якщо ні — ваші скрипти запуску налаштовані неправильно, і ви дізнаєтесь про це в найневідповідніший момент.
Чи це "ентерпрайз-рівень"?#
Ні. І в цьому суть.
Це налаштування обслуговує цей блог надійно менш ніж за $10/місяць. Деплоїться за 30 секунд однією командою. Я розумію кожну його частину. Коли щось ламається, я точно знаю, де шукати.
Чи міг би я використати Docker? Звісно. Чи міг би використати Kubernetes? Технічно. Чи міг би налаштувати повний CI/CD пайплайн зі стейджинг-середовищами та канарковими деплоями? Безумовно.
Але я навчився, що найкраща інфраструктура — це та, яку ви реально розумієте, можете дебажити о 2 ночі і яка не коштує більше, ніж заробляє проєкт. Для особистого сайту, SaaS MVP або невеликого стартапу — це саме те налаштування.
Спершу шіпніть. Масштабуйте, коли потрібно. І завжди, завжди тестуйте скрипт деплою на свіжому сервері.