Konfiguracja VPS, która naprawdę działa: Node.js, PM2, Nginx i wdrożenia bez przestojów
Dokładna konfiguracja wdrożeniowa VPS, której używam na produkcji — hardening Ubuntu, tryb cluster PM2, reverse proxy Nginx, SSL i skrypt deploy, który mnie jeszcze nie zawiódł. Bez teorii, tylko to, co działa.
Ten blog działa na VPS za 10$/miesiąc. Nie Vercel, nie AWS, nie klaster Kubernetes zarządzany przez sześcioosobowy zespół. Pojedynczy serwer Ubuntu z Nginx, PM2 i skryptem bash, który wdraża w mniej niż 30 sekund.
Próbowałem innych ścieżek. Używałem Vercel (świetny, dopóki nie potrzebujesz zadań cron, trwałych WebSocketów, albo po prostu kontroli). Używałem AWS (świetny, jeśli lubisz spędzać pół dnia w politykach IAM). Zawsze wracam do VPS.
Ale jest problem: każdy poradnik "wdróż na VPS" w internecie kończy się na happy path. Pokazują ci, jak zainstalować Node.js i uruchomić node server.js, i nazywają to produkcją. Potem twój serwer zostaje zbrute-force'owany przez SSH, twój proces umiera o 3 w nocy, bo nikt nie skonfigurował menedżera procesów, a twój certyfikat SSL wygasł trzy miesiące temu.
To jest poradnik, którego chciałbym wtedy mieć. Wszystko tutaj jest przetestowane w boju — dokładnie ta konfiguracja serwuje stronę, którą teraz czytasz.
Zacznij od bezpieczeństwa, nie od kodu#
Zanim w ogóle pomyślisz o Node.js, zabezpiecz maszynę. Świeże instancje VPS są celami. Zautomatyzowane boty zaczynają atakować twój port SSH w ciągu minut od uruchomienia.
Utwórz użytkownika bez uprawnień root#
adduser deploy
usermod -aG sudo deploySkonfiguruj uwierzytelnianie kluczem SSH#
Na twoim lokalnym komputerze:
ssh-copy-id deploy@your-server-ipNastępnie wyłącz uwierzytelnianie hasłem całkowicie:
sudo nano /etc/ssh/sshd_configPasswordAuthentication no
PermitRootLogin nosudo systemctl restart sshdJeśli to pominiesz, zobaczysz tysiące nieudanych prób logowania w swoich logach auth w ciągu kilku dni. To nie jest paranoja — to zwykły wtorek w publicznym internecie.
Firewall z UFW#
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableTo wszystko. Cztery reguły. Tylko ruch SSH i webowy przechodzą.
Fail2Ban#
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.localEdytuj /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 fail2banTrzy nieudane próby SSH i jesteś zbanowany na godzinę. Obserwowałem, jak Fail2Ban blokował setki adresów IP w ciągu jednego dnia. To działa.
Automatyczne aktualizacje bezpieczeństwa#
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgradesTwój serwer będzie teraz automatycznie instalować poprawki bezpieczeństwa. Jedna rzecz mniej do zapamiętania.
Node.js: użyj NVM, nie apt#
Widzę to w każdym poradniku: sudo apt install nodejs. Nie rób tego.
Repozytoria pakietów Ubuntu dostarczają starożytne wersje Node.js. Nawet PPA NodeSource pozostaje w tyle. A kiedy musisz przełączać się między Node 20 a Node 22 dla różnych projektów, utkniesz.
NVM rozwiązuje ten problem:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm alias default lts/*Teraz zweryfikuj:
node -v # v22.x.x lub aktualna wersja LTS
npm -vNieoczywista wskazówka: kiedy instalujesz globalne pakiety z NVM (jak PM2), są one powiązane z tą wersją Node. Jeśli przełączysz wersję za pomocą nvm use, twoje pakiety globalne znikają. Ustaw domyślną i trzymaj się jej na serwerze:
nvm alias default 22To ugryzło mnie dokładnie raz. Raz wystarczyło.
PM2: menedżer procesów, który zasługuje na swoje miejsce#
PM2 to różnica między "wdrożonym" a "gotowym na produkcję". Obsługuje zarządzanie procesami, klastrowanie, rotację logów, auto-restart po crashach i skrypty startowe. Za darmo.
Instalacja i konfiguracja#
npm install -g pm2Konfiguracja ecosystem#
Nie uruchamiaj aplikacji flagami CLI. Użyj pliku ecosystem.config.js. Jest wersjonowany, powtarzalny i samodokumentujący się.
// 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,
},
// Graceful shutdown
kill_timeout: 5000,
listen_timeout: 10000,
wait_ready: false,
// Logowanie
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,
// Auto-restart przy awarii
autorestart: true,
max_restarts: 10,
min_uptime: "10s",
// Nie obserwuj zmian na produkcji
watch: false,
},
],
};Pozwólcie, że wyjaśnię wybory, które mają znaczenie:
instances: 2 zamiast "max": Na małym VPS z 1-2 rdzeniami "max" brzmi mądrze, ale uruchomi procesy, które walczą o zasoby podczas buildów. Dwie instancje dają ci przeładowania bez przestojów, zostawiając zapas. Na maszynie z 4+ rdzeniami, jasne, użyj "max".
exec_mode: "cluster": To jest to, co umożliwia przeładowania bez przestojów. Bez trybu cluster pm2 reload to tylko ozdobny restart. Z trybem cluster PM2 restartuje instancje jedna po drugiej — twoja aplikacja nigdy nie jest w pełni offline.
max_memory_restart: "500M": Twoja aplikacja Next.js ma wyciek pamięci? PM2 zrestartuje ją, zanim OOM-kill położy twój serwer. To uratowało mnie od alertów o 2 w nocy nie raz.
kill_timeout: 5000: Daje twojej aplikacji 5 sekund na dokończenie aktualnych żądań, zanim PM2 ją ubije na siłę. Domyślna wartość (1600ms) jest zbyt agresywna dla aplikacji z połączeniami do bazy danych.
watch: false: Widziałem ludzi, którzy zostawiali watch: true na produkcji. PM2 wtedy restartuje aplikację za każdym razem, gdy zmienia się plik logów. Twoja aplikacja wchodzi w pętlę restartów. Nie rób tego.
Skrypt startowy#
Spraw, by PM2 przetrwał restarty serwera:
pm2 startup systemd
# Skopiuj i uruchom komendę, którą wyświetli
pm2 saveTo generuje usługę systemd. Po restarcie serwera twoja aplikacja wraca automatycznie. Przetestuj to — zrestartuj serwer i zweryfikuj. Nie zakładaj.
Rotacja logów#
Logi w końcu zjedzą twój dysk. Zainstaluj moduł rotacji:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress trueMaks. 50MB na plik, zachowaj 7 zrotowanych plików, kompresuj stare. Bez tego widziałem, jak /var/log zapełniało 25GB dysk w trzy tygodnie na umiarkowanie obciążonej aplikacji.
Nginx: reverse proxy, który robi więcej niż myślisz#
"Dlaczego po prostu nie wystawić Node.js bezpośrednio na porcie 80?"
Ponieważ Nginx obsługuje rzeczy, na które Node.js nie powinien marnować cykli: terminację SSL, serwowanie plików statycznych, kompresję gzip, buforowanie żądań, limity połączeń i graceful handling wolnych klientów. Jest napisany w C i stworzony specjalnie do tego.
Instalacja#
sudo apt install nginx -yKonfiguracja#
# /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;
# Przekieruj cały HTTP na HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name akousa.net www.akousa.net;
# SSL (zarządzany przez Certbot — te linie dodawane są automatycznie)
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;
# Nagłówki bezpieczeństwa
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;
# Kompresja 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;
# Ustawienia proxy
location / {
proxy_pass http://node_app;
proxy_http_version 1.1;
# Nagłówki
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;
# Obsługa WebSocket (jeśli kiedykolwiek będziesz potrzebować)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouty — hojne, ale nie nieskończone
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buforowanie — niech Nginx obsługuje wolnych klientów
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
}
# Statyczne zasoby Next.js — niech Nginx serwuje je bezpośrednio
location /_next/static/ {
alias /var/www/akousa.net/.next/static/;
expires 365d;
access_log off;
add_header Cache-Control "public, immutable";
}
# Publiczne pliki statyczne
location /static/ {
alias /var/www/akousa.net/public/static/;
expires 30d;
access_log off;
}
# Blokuj dostęp do plików z kropką
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}Włącz konfigurację:
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 nginxZawsze uruchamiaj nginx -t przed przeładowaniem. Kiedyś wrzuciłem uszkodzoną konfigurację i położyłem stronę, bo pominąłem sprawdzenie składni. Pięć znaków nginx -t zaoszczędziłoby mi trzydziestu minut panicznego debugowania.
Rzeczy, które większość poradników pomija w tej konfiguracji:
Blok upstream z keepalive 64: Nginx ponownie używa połączeń do twojego backendu Node.js zamiast otwierać nowe połączenie TCP dla każdego żądania. To ma znaczenie pod obciążeniem.
proxy_buffering on: Nginx odczytuje całą odpowiedź z Node.js do pamięci, a następnie wysyła ją klientowi z prędkością, jaką klient jest w stanie obsłużyć. Bez tego wolny klient na połączeniu 3G blokuje twój worker Node.js.
Serwowanie _next/static/ bezpośrednio: To są zahashowane, niemutowalne zasoby. Niech Nginx serwuje je z dysku z 365-dniowym nagłówkiem cache. Twoje procesy Node.js nie powinny na to marnować czasu.
SSL w pięć minut#
Let's Encrypt rozwiązał SSL. Jeśli nadal płacisz za certyfikaty w 2026 roku, przestań.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d akousa.net -d www.akousa.netCertbot zapyta o twój email, zaakceptuje regulamin i automatycznie zmodyfikuje twoją konfigurację Nginx, dodając dyrektywy SSL. To wszystko.
Zweryfikuj automatyczne odnawianie#
Certbot instaluje timer systemd, który sprawdza dwa razy dziennie i odnawia certyfikaty w ciągu 30 dni przed wygaśnięciem:
sudo systemctl list-timers | grep certbotPrzetestuj, czy odnawianie działa:
sudo certbot renew --dry-runJeśli próbny przebieg się powiedzie, nigdy więcej nie pomyślisz o SSL. Jeśli się nie powiedzie, to zwykle dlatego, że port 80 jest zablokowany (sprawdź reguły UFW) lub Nginx nie działa.
Jedna rzecz, która mnie zaskoczyła: jeśli skonfigurujesz Nginx przed uruchomieniem Certbot, upewnij się, że twój blok server nasłuchuje na porcie 80 bez przekierowania HTTPS na początku. Certbot musi dotrzeć do portu 80 dla wyzwania HTTP-01. Po pomyślnym uruchomieniu Certbot wtedy dodaj przekierowanie.
Skrypt wdrożeniowy#
To jest skrypt, który uruchamiam za każdym razem, gdy wdrażam na produkcję. Żadnej platformy CI/CD, żadnych GitHub Actions. Tylko SSH i bash.
#!/bin/bash
# deploy.sh — wdrożenie z (prawie) zerowym przestojem
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 "=== Deploy started ==="
cd "$APP_DIR"
# Pobierz najnowszy kod
log "Pulling latest changes..."
git pull origin main 2>&1 | tee -a "$LOG_FILE"
# Zainstaluj zależności
log "Installing dependencies..."
npm install --legacy-peer-deps 2>&1 | tee -a "$LOG_FILE"
# Zbuduj
log "Building application..."
rm -rf .next
npm run build 2>&1 | tee -a "$LOG_FILE"
if [ $? -ne 0 ]; then
log "ERROR: Build failed. Aborting deploy."
exit 1
fi
# Przeładuj PM2 (zero przestojów w trybie cluster)
log "Reloading PM2..."
pm2 reload "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
pm2 save 2>&1 | tee -a "$LOG_FILE"
# Sprawdzenie zdrowia z powtórzeniami
log "Running health check..."
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 "Health check passed (HTTP $HTTP_CODE)"
log "=== Deploy completed successfully ==="
exit 0
fi
log "Health check attempt $i/$MAX_RETRIES (HTTP $HTTP_CODE). Retrying in ${RETRY_INTERVAL}s..."
sleep $RETRY_INTERVAL
done
log "ERROR: Health check failed after $MAX_RETRIES attempts"
log "Rolling back to previous PM2 state..."
pm2 restart "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
exit 1Nadaj mu uprawnienia do wykonywania:
chmod +x deploy.shWdróż z lokalnego komputera:
ssh root@your-server-ip "bash /var/www/akousa.net/deploy.sh"Kluczowe decyzje w tym skrypcie:
set -euo pipefail: Skrypt natychmiast kończy działanie przy jakimkolwiek błędzie. Bez tego nieudany npm install cicho kontynuuje do kroku budowania, a ty dostajesz tajemniczy błąd, którego debugowanie zabiera 20 minut.
rm -rf .next przed budowaniem: Next.js ma cache builda, który czasami produkuje nieaktualne dane wyjściowe. Raz mnie to ugryzło — strona pokazywała stare treści mimo zaktualizowanego kodu źródłowego. Usunięcie katalogu builda dodaje może 15 sekund do budowania, ale gwarantuje świeże dane wyjściowe.
pm2 reload zamiast pm2 restart: To jest część z zerowym przestojem. W trybie cluster reload wykonuje rolling restart — uruchamia nowe instancje z zaktualizowanym kodem, czeka aż będą gotowe, następnie gracefully wyłącza stare. W żadnym momencie nie ma zero działających instancji.
Sprawdzenie zdrowia z powtórzeniami: Next.js potrzebuje kilku sekund na rozgrzewkę po restarcie. Skrypt czeka do 30 sekund (10 powtórzeń x 3 sekundy), sprawdzając czy aplikacja odpowiada kodem HTTP 200. Jeśli nie odpowiada, coś jest nie tak i musisz o tym wiedzieć natychmiast — nie dowiedzieć się od użytkownika.
Rollback przy błędzie: Jeśli sprawdzenie zdrowia nie powiedzie się po wszystkich powtórzeniach, skrypt restartuje PM2 (który ładuje ostatni zapisany stan). To nie jest idealny rollback, ale jest lepszy niż zostawienie serwera w uszkodzonym stanie.
Kiedy coś się psuje o 2 w nocy#
Oto co faktycznie debugowałem na dokładnie tej konfiguracji:
"Strona nie działa"#
Pierwsze komendy do uruchomienia:
pm2 status
pm2 logs akousa --lines 50
sudo systemctl status nginx
sudo tail -50 /var/log/nginx/error.logW dziewięciu na dziesięć przypadków pm2 logs natychmiast ci mówi, co się stało. Brakująca zmienna środowiskowa, nieudane połączenie z bazą danych lub nieobsłużona odrzucona obietnica.
"Pamięć ciągle rośnie"#
pm2 monitTo daje ci live dashboard procesora i pamięci na proces. Jeśli pamięć stale rośnie bez stabilizacji, masz wyciek. Ustawienie max_memory_restart w konfiguracji ecosystem to twoja siatka bezpieczeństwa — PM2 zrestartuje proces, zanim położy serwer.
Dla głębszego zbadania:
pm2 describe akousaTo pokazuje uptime, liczbę restartów i snapshoty pamięci. Jeśli widzisz 47 restartów w ciągu ostatnich 24 godzin, to twoja podpowiedź.
"Certyfikat SSL wygasł"#
sudo certbot certificatesWyświetla wszystkie certyfikaty z datami wygaśnięcia. Jeśli automatyczne odnawianie zawiodło:
sudo certbot renew --force-renewal
sudo systemctl reload nginx"Dysk jest pełny"#
df -h
du -sh /var/log/*
pm2 flushpm2 flush natychmiast czyści wszystkie pliki logów PM2. Jeśli nie skonfigurowałeś rotacji logów (mówiłem ci), tu odczujesz ból.
Komenda, którą uruchamiam każdego ranka#
ssh deploy@akousa.net "pm2 status && df -h / && uptime"Trzy rzeczy w jednej linii: czy moje procesy działają, czy dysk jest w porządku, czy serwer jest przeciążony. Zajmuje dwie sekundy. Wyłapuje problemy, zanim zrobią to użytkownicy.
Czego większość poradników ci nie powie#
Twój krok budowania to twoja największa podatność. Na VPS z 1GB RAM npm run build dla aplikacji Next.js może zużyć 800MB+ pamięci. Jeśli PM2 uruchamia twoją aplikację w dwóch instancjach podczas budowania, dostaniesz OOM. Rozwiązania: użyj swap file (co najmniej 2GB) albo zatrzymaj aplikację na czas budowania i zaakceptuj kilka sekund przestoju. Ja używam 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 w komendzie instalacji to code smell, nie rozwiązanie. Używam tego, bo niektóre pakiety w moim drzewie zależności nie zaktualizowały swoich zakresów peer dependency. Co kilka miesięcy próbuję to usunąć. Kiedyś zadziała. Do tego czasu dostarczam.
Przetestuj swój skrypt wdrożeniowy od zera. Sklonuj swoje repozytorium na świeżym serwerze i uruchom każdy krok ręcznie. Liczba problemów typu "działa na mojej maszynie", które ukrywają się w skryptach wdrożeniowych, jest zawstydzająca. Znalazłem trzy problemy w swoim, kiedy to zrobiłem — brakujące pakiety globalne, złe uprawnienia plików i ścieżka, która istniała tylko z powodu poprzedniej ręcznej konfiguracji.
Wpisz IP swojego serwera do konfiguracji SSH. Przestań wpisywać adresy IP:
# ~/.ssh/config
Host akousa
HostName 69.62.66.94
User deploy
IdentityFile ~/.ssh/id_ed25519Teraz ssh akousa to wszystko, czego potrzebujesz. Małe rzeczy się kumulują.
Pełna lista kontrolna#
Zanim uznasz, że skończyłeś:
- Użytkownik bez uprawnień root z dostępem sudo
- Tylko uwierzytelnianie kluczem SSH, uwierzytelnianie hasłem wyłączone
- UFW włączony z otwartymi tylko niezbędnymi portami
- Fail2Ban chroniący SSH
- Automatyczne aktualizacje bezpieczeństwa włączone
- Node.js zainstalowany przez NVM
- PM2 uruchamiający twoją aplikację w trybie cluster
- Skrypt startowy PM2 skonfigurowany (przetrwa restart)
- Rotacja logów PM2 zainstalowana
- Reverse proxy Nginx z odpowiednimi nagłówkami
- SSL przez Let's Encrypt z automatycznym odnawianiem
- Skrypt wdrożeniowy ze sprawdzeniami zdrowia
- Swap file skonfigurowany (na zapas przy budowaniu)
- Przetestowane: zrestartuj serwer i zweryfikuj, że wszystko wraca
Ten ostatni punkt to ten, który ludzie pomijają. Nie bądź tą osobą. Zrestartuj serwer, odczekaj 60 sekund i sprawdź, czy twoja aplikacja jest online. Jeśli nie jest, twoje skrypty startowe są źle skonfigurowane i dowiesz się o tym w najgorszym możliwym momencie.
Czy to jest "klasy enterprise"?#
Nie. I o to chodzi.
Ta konfiguracja niezawodnie serwuje tego bloga za mniej niż 10$/miesiąc. Wdraża się w 30 sekund jedną komendą. Rozumiem każdy jej element. Kiedy coś się psuje, wiem dokładnie, gdzie szukać.
Czy mógłbym użyć Docker? Jasne. Czy mógłbym użyć Kubernetes? Technicznie. Czy mógłbym skonfigurować pełny pipeline CI/CD ze środowiskami staging i wdrożeniami canary? Absolutnie.
Ale nauczyłem się, że najlepsza infrastruktura to ta, którą faktycznie rozumiesz, możesz debugować o 2 w nocy i nie kosztuje więcej, niż projekt zarabia. Dla osobistej strony, MVP SaaS lub małego startupu — to jest ta konfiguracja.
Najpierw dostarczaj. Skaluj, kiedy potrzebujesz. I zawsze, zawsze, testuj swój skrypt wdrożeniowy na świeżym serwerze.