Точне налаштування 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-порт за лічені хвилини після створення.
adduser deploy
usermod -aG sudo deployНа вашій локальній машині:
ssh-copy-id deploy@your-server-ipПотім повністю вимкніть автентифікацію за паролем:
sudo nano /etc/ssh/sshd_configPasswordAuthentication no
PermitRootLogin nosudo systemctl restart sshdЯкщо ви пропустите це, ви побачите тисячі невдалих спроб входу у ваших логах автентифікації за кілька днів. Це не параноя — це звичайний вівторок у публічному інтернеті.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableОсь і все. Чотири правила. Тільки SSH і веб-трафік проходять.
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Тепер ваш сервер автоматично встановлюватиме патчі безпеки. Однією турботою менше.
Я бачу це в кожному туторіалі: 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 — це різниця між "задеплоєно" та "готово до продакшену". Він обробляє керування процесами, кластеризацію, ротацію логів, автоперезапуск при збоях та скрипти запуску. Безкоштовно.
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 ГБ диск за три тижні на додатку із середнім трафіком.
"Навіщо не виставити 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 процеси не повинні витрачати на це час.
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 (який завантажує останній збережений стан). Це не ідеальний відкат, але краще, ніж залишити сервер у зламаному стані.
Ось що я реально дебажив на цьому точному налаштуванні:
Перші команди для запуску:
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 години — це ваша підказка.
sudo certbot certificatesПоказує всі сертифікати з датами закінчення. Якщо автоподовження не спрацювало:
sudo certbot renew --force-renewal
sudo systemctl reload nginxdf -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 — це все, що вам потрібно. Маленькі речі накопичуються.
Перш ніж вважати роботу завершеною:
Останній пункт — той, що люди пропускають. Не будьте цією людиною. Перезавантажте сервер, зачекайте 60 секунд і перевірте, чи ваш додаток живий. Якщо ні — ваші скрипти запуску налаштовані неправильно, і ви дізнаєтесь про це в найневідповідніший момент.
Ні. І в цьому суть.
Це налаштування обслуговує цей блог надійно менш ніж за $10/місяць. Деплоїться за 30 секунд однією командою. Я розумію кожну його частину. Коли щось ламається, я точно знаю, де шукати.
Чи міг би я використати Docker? Звісно. Чи міг би використати Kubernetes? Технічно. Чи міг би налаштувати повний CI/CD пайплайн зі стейджинг-середовищами та канарковими деплоями? Безумовно.
Але я навчився, що найкраща інфраструктура — це та, яку ви реально розумієте, можете дебажити о 2 ночі і яка не коштує більше, ніж заробляє проєкт. Для особистого сайту, SaaS MVP або невеликого стартапу — це саме те налаштування.
Спершу шіпніть. Масштабуйте, коли потрібно. І завжди, завжди тестуйте скрипт деплою на свіжому сервері.