Přeskočit na obsah
·12 min čtení

VPS setup, který skutečně funguje: Node.js, PM2, Nginx a deploy bez vypadku

Přesný VPS deployment setup, který používám v produkci — zabezpečení Ubuntu, PM2 cluster mode, Nginx reverse proxy, SSL a deploy skript, který me ještě nezklamal. Žádná teorie, jen to, co funguje.

Sdílet:X / TwitterLinkedIn

Tento blog bezi na VPS za $10/mesic. Ne Vercel, ne AWS, ne Kubernetes cluster spravovany tymem sesti lidi. Jediný Ubuntu box s Nginx, PM2 a bash skriptem, který nasadi za mene nez 30 sekund.

Zkousel jsem i jiné cesty. Pouzival jsem Vercel (skvely, dokud nepotrebujete cron joby, persistentni WebSockety nebo proste kontrolu). Pouzival jsem AWS (skvely, pokud vas bavi travit pul dne v IAM politikach). Vzdy se vracim k VPS.

Ale tady je problem: každý tutorial "deploy na VPS" na internetu konci u happy path. Ukazi vam, jak nainstalovat Node.js a spustit node server.js, a nazvou to produkci. Pak vam server dostane SSH brute-force, vas proces umre ve tri ráno, protoze nikdo nenastavil správce procesu, a vas SSL certifikat vyprsel pred tremi mesici.

Toto je prirucka, kterou jsem si pral mit. Vse zde je overeno v boji — přesně tento setup obsluhuje stránku, kterou prave ctete.

Zacnete bezpecnosti, ne kodem#

Nez vubec zacnete premyslet o Node.js, zabezpecte box. Cerstve VPS instance jsou cile. Automatizovani boti zacnou utocit na vas SSH port behem minut od zprovozneni.

Vytvorte ne-root uzivatele#

bash
adduser deploy
usermod -aG sudo deploy

Nastavte SSH autentifikaci klicem#

Na vasem lokalnim pocitaci:

bash
ssh-copy-id deploy@your-server-ip

Pak zcela zakazte autentifikaci heslem:

bash
sudo nano /etc/ssh/sshd_config
bash
PasswordAuthentication no
PermitRootLogin no
bash
sudo systemctl restart sshd

Pokud toto preskocite, behem dnu uvidite tisice neuspesnych pokusu o prihlaseni ve vasich auth lozich. To neni paranoia — to je utery na verejnem internetu.

Firewall s UFW#

bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

To je vse. Ctyri pravidla. Projde jen SSH a webovy provoz.

Fail2Ban#

bash
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Upravte /etc/fail2ban/jail.local:

ini
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
bash
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Tri neuspesne pokusy o SSH a jste zabanovani na hodinu. Sledoval jsem, jak Fail2Ban zablokoval stovky IP adres za jediný den. Funguje to.

Automatické bezpecnostni aktualizace#

bash
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades

Vas server bude nyni automaticky instalovat bezpecnostni zaplatky. O jednu věc mene k zapomneni.

Node.js: Pouzijte NVM, ne apt#

Toto vidim v kazdem tutorialu: sudo apt install nodejs. Nedelejte to.

Repozitare balicku Ubuntu dodavaji prastaré verze Node.js. Dokonce i NodeSource PPA zaostava. A kdyz potřebujete prepinat mezi Node 20 a Node 22 pro ruzne projekty, jste v uvizli.

NVM to řeší:

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
nvm alias default lts/*

Overeni:

bash
node -v  # v22.x.x nebo jakakoliv aktualni LTS
npm -v

Neocividny tip: kdyz instalujete globalni balicky s NVM (jako PM2), jsou vazany na tu verzi Node. Pokud prepnete verzi pomoci nvm use, vase globalni balicky zmizi. Nastavte svuj vychozi a na serveru se ho drzte:

bash
nvm alias default 22

Tohle me stihlo přesně jednou. Jednou stacilo.

PM2: Správce procesu, který si zaslouzi sve místo#

PM2 je rozdil mezi "nasazeno" a "pripraveno na produkci." Zvlada spravu procesu, clustering, rotaci logu, automaticky restart pri padech a startovaci skripty. Zdarma.

Instalace a nastaveni#

bash
npm install -g pm2

Ecosystem konfigurace#

Nespoustejte aplikace s CLI flagy. Pouzijte soubor ecosystem.config.js. Je verzovany, reprodukovatelny a samodokumentujici.

javascript
// 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,
      },
      // Elegantni ukonceni
      kill_timeout: 5000,
      listen_timeout: 10000,
      wait_ready: false,
      // Logovani
      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,
      // Automaticky restart pri selhani
      autorestart: true,
      max_restarts: 10,
      min_uptime: "10s",
      // V produkci nesledovat
      watch: false,
    },
  ],
};

Vysvetlim volby, na kterých zalezi:

instances: 2 místo "max": Na malem VPS s 1-2 jadry zni "max" chytre, ale bude spoustet procesy, které si behem buildu konkuruji o prostredky. Dve instance vam daji reloady bez vypadku a zaroven ponechaji rezervu. Na stroji se 4+ jadry klidne pouzijte "max".

exec_mode: "cluster": Toto umoznuje reloady bez vypadku. Bez cluster modu je pm2 reload jen efektni restart. S cluster modem PM2 restartuje instance jednu po druhe — vase aplikace nikdy neni zcela offline.

max_memory_restart: "500M": Vase Next.js aplikace ma memory leak? PM2 ji restartuje driv, nez OOM-kill polozi vas server. Tohle me zachranilo od budiku ve dve ráno vic nez jednou.

kill_timeout: 5000: Dava vasi aplikaci 5 sekund na dokonceni rozpracovanych requestu, nez ji PM2 nasilne ukonci. Vychozi hodnota (1600ms) je prilis agresivni pro aplikace s databazovymi spojenimi.

watch: false: Videl jsem lidi, kteri nechali watch: true v produkci. PM2 pak restartuje aplikaci pokaždé, kdyz se zmeni log soubor. Vase aplikace vstoupi do smycky restartu. Nedelejte to.

Startovaci skript#

Zajistete, aby PM2 prezilo restarty serveru:

bash
pm2 startup systemd
# Zkopirujte a spuste prikaz, ktery vypise
pm2 save

Toto vygeneruje systemd sluzbu. Po restartu serveru se vase aplikace vrati automaticky. Otestujte to — restartujte server a overte. Nepredpokladejte.

Rotace logu#

Logy nakonec sezeru vas disk. Nainstalujte modul pro rotaci:

bash
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true

50MB maximum na soubor, uchovat 7 rotovanych souborů, komprimovat stare. Bez tohoto jsem videl, jak /var/log zaplnilo 25GB disk za tri tydny u stredne zatizene aplikace.

Nginx: Reverse proxy, který umí vic, nez si myslite#

"Proč proste nevystavit Node.js přímo na portu 80?"

Protoze Nginx zvlada věci, na kterých by Node.js nemel plytvat cykly: SSL terminace, servovani statickych souborů, gzip komprese, bufferovani requestu, limity připojení a elegantni obsluha pomalych klientu. Je napsany v C a ucelove vytvoreny pro toto.

Instalace#

bash
sudo apt install nginx -y

Konfigurace#

nginx
# /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;
 
    # Presmerovani veskeho 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 (spravovano Certbot — tyto radky se pridaji automaticky)
    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;
 
    # Bezpecnostni hlavicky
    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 komprese
    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;
 
    # Nastaveni proxy
    location / {
        proxy_pass http://node_app;
        proxy_http_version 1.1;
 
        # Hlavicky
        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;
 
        # Podpora WebSocket (pro pripad potreby)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
 
        # Timeouty — stedre, ale ne nekonecne
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
 
        # Bufferovani — nechte Nginx obslouzit pomale klienty
        proxy_buffering on;
        proxy_buffer_size 16k;
        proxy_buffers 4 32k;
        proxy_busy_buffers_size 64k;
    }
 
    # Staticke assety Next.js — nechte Nginx je servovat primo
    location /_next/static/ {
        alias /var/www/akousa.net/.next/static/;
        expires 365d;
        access_log off;
        add_header Cache-Control "public, immutable";
    }
 
    # Verejne staticke soubory
    location /static/ {
        alias /var/www/akousa.net/public/static/;
        expires 30d;
        access_log off;
    }
 
    # Blokovat pristup k dot souboorum
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Aktivace:

bash
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

Vzdy spustte nginx -t pred reloadem. Jednou jsem pushnnul rozbitou konfiguraci a polozil web, protoze jsem preskocil kontrolu syntaxe. Pet znaku nginx -t by mi usetrilp tricet minut panikareni pri debugovani.

Věci, které většina tutorialu v teto konfiguraci vynecha:

Blok upstream s keepalive 64: Nginx znovu pouziva připojení k vasemu Node.js backendu místo otevirani noveho TCP připojení pro každý request. Pod zatezi na tom zalezi.

proxy_buffering on: Nginx nacte celou odpoved z Node.js do pameti a pak ji odesle klientovi rychlosti, kterou klient zvladne. Bez tohoto pomaly klient na 3G připojení blokuje vaseho Node.js workera.

Prime servovani _next/static/: Jsou to hashovane, nemenne assety. Nechte Nginx je servovat z disku s 365denni cache hlavickou. Vase Node.js procesy by na tom nemely plytvat casem.

SSL za pet minut#

Let's Encrypt vyresil SSL. Pokud v roce 2026 stale platite za certifikaty, prestante.

bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d akousa.net -d www.akousa.net

Certbot se zepta na vas email, prijmete podminky a automaticky upravi vasi Nginx konfiguraci, aby zahrnovala SSL direktivy. To je vse.

Overeni automatickeho obnoveni#

Certbot nainstaluje systemd timer, který kontroluje dvakrat denne a obnovuje certifikaty 30 dni pred vyprsenim:

bash
sudo systemctl list-timers | grep certbot

Otestujte, ze obnoveni funguje:

bash
sudo certbot renew --dry-run

Pokud dry run projde, uz nikdy nebudete myslet na SSL. Pokud selze, je to obvykle proto, ze port 80 je zablokovany (zkontrolujte pravidla UFW) nebo Nginx nebezi.

Jedna věc, která me nachytala: pokud nastavite Nginx pred spustenim Certbot, ujistete se, ze vas server blok naslouchá na portu 80 bez HTTPS presmerovani. Certbot potrebuje přístup na port 80 pro HTTP-01 vyzvu. Pote co Certbot uspesne probehne, pak pridejte presmerovani.

Deploy skript#

Toto je skript, který se spousti pokaždé, kdyz pushnu do produkce. Žádná CI/CD platforma, žádné GitHub Actions. Jen SSH a bash.

bash
#!/bin/bash
# deploy.sh — nasazeni s (temer) nulovym vypadkem
 
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 zahajen ==="
 
cd "$APP_DIR"
 
# Stazeni nejnovsiho kodu
log "Stahuji posledni zmeny..."
git pull origin main 2>&1 | tee -a "$LOG_FILE"
 
# Instalace zavislosti
log "Instaluji zavislosti..."
npm install --legacy-peer-deps 2>&1 | tee -a "$LOG_FILE"
 
# Build
log "Builduji aplikaci..."
rm -rf .next
npm run build 2>&1 | tee -a "$LOG_FILE"
 
if [ $? -ne 0 ]; then
    log "CHYBA: Build selhal. Prerusuji deploy."
    exit 1
fi
 
# Reload PM2 (bez vypadku v cluster modu)
log "Reloaduji PM2..."
pm2 reload "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
pm2 save 2>&1 | tee -a "$LOG_FILE"
 
# Health check s opakovani
log "Spoustim 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 prosel (HTTP $HTTP_CODE)"
        log "=== Deploy uspesne dokoncen ==="
        exit 0
    fi
    log "Pokus health checku $i/$MAX_RETRIES (HTTP $HTTP_CODE). Opakuji za ${RETRY_INTERVAL}s..."
    sleep $RETRY_INTERVAL
done
 
log "CHYBA: Health check selhal po $MAX_RETRIES pokusech"
log "Vracim se k predchozimu stavu PM2..."
pm2 restart "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
exit 1

Nastaveni spustitelnosti:

bash
chmod +x deploy.sh

Deploy z vaseho lokalniho pocitace:

bash
ssh root@your-server-ip "bash /var/www/akousa.net/deploy.sh"

Klicova rozhodnutí v tomto skriptu:

set -euo pipefail: Skript se okamzite ukonci pri jakékoliv chybe. Bez tohoto neuspesny npm install tise pokracuje do build kroku a vy dostanete kryptickou chybu, jejiz debugovani zabere 20 minut.

rm -rf .next pred buildem: Next.js ma build cache, která obcas produkuje staly vystup. Jednou me to stihlo — stránka zobrazovala stary obsah, přesto ze zdrojovy kod byl aktualizovany. Smazani build adresare prida možná 15 sekund k buildu, ale zarucuje cerstvy vystup.

pm2 reload místo pm2 restart: Toto je část bez vypadku. V cluster modu reload provadi rolling restart — spusti nove instance s aktualizovanym kodem, pocka az budou pripraveny, pak elegantne ukonci stare. V zadnem okamziku nebezi nula instanci.

Health check s opakovani: Next.js potrebuje par sekund na nahrati po restartu. Skript ceka az 30 sekund (10 pokusu x 3 sekundy) a kontroluje, zda aplikace odpovida HTTP 200. Pokud ne, něco je špatně a potřebujete to vedet okamzite — ne az se to dozvite od uzivatele.

Rollback pri selhani: Pokud health check selze po všech pokusech, skript restartuje PM2 (který nacte posledni ulozeny stav). Neni to perfektni rollback, ale je to lepsi nez nechat server v rozbitém stavu.

Kdyz se věci rozbiji ve dve ráno#

Tady je, co jsem skutečně debugoval na přesně tomto setupu:

"Web je dole"#

Prvni prikazy ke spusteni:

bash
pm2 status
pm2 logs akousa --lines 50
sudo systemctl status nginx
sudo tail -50 /var/log/nginx/error.log

V deviti pripadech z deseti vam pm2 logs okamzite rekne, co se stalo. Chybejici promenna prostredi, selhale databazove připojení nebo neosetrene odmitni promise.

"Pamet stale roste"#

bash
pm2 monit

Toto vam da zive zobrazeni CPU a pameti pro každý proces. Pokud pamet stabilne roste bez ustálení, mate leak. Nastaveni max_memory_restart ve vasi ecosystem konfiguraci je vase zachranna sit — PM2 restartuje proces driv, nez polozi server.

Pro hlubsi vysetrovani:

bash
pm2 describe akousa

Ukaze uptime, pocet restartu a snimky pameti. Pokud vidite 47 restartu za poslednich 24 hodin, to je vas vodítko.

"SSL certifikat vyprsel"#

bash
sudo certbot certificates

Vypise všechny certifikaty s jejich daty vypr-seni. Pokud automatické obnoveni selhalo:

bash
sudo certbot renew --force-renewal
sudo systemctl reload nginx

"Disk je plny"#

bash
df -h
du -sh /var/log/*
pm2 flush

pm2 flush okamzite vymaze všechny log soubory PM2. Pokud jste nenastavili rotaci logu (rikal jsem vam), tady to pocitite.

Prikaz, který spoustim každé ráno#

bash
ssh deploy@akousa.net "pm2 status && df -h / && uptime"

Tri věci na jednom řádku: bezi moje procesy, je disk v poradku, je server pretizeny. Trva to dve sekundy. Zachyti problemy driv nez uzivatele.

Co vam většina prirucek nerekne#

Vas build krok je vase nejvetsi zranitelnost. Na VPS s 1GB RAM může npm run build pro Next.js aplikaci spotrebovat 800MB+ pameti. Pokud PM2 behem buildu bezi se dvema instancemi vasi aplikace, dojde vam pamet. Řešení: pouzijte swap soubor (minimalne 2GB), nebo zastavte aplikaci behem buildu a akceptujte par sekund vypadku. Ja používám swap.

bash
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 ve vasem install prikazu je code smell, ne řešení. Používám to, protoze některé balicky v mem stromu zavislosti neaktualizovaly sve rozsahy peer zavislosti. Kazdych par mesicu zkousim to odstranit. Jednou to bude fungovat. Do te doby dodavam.

Otestujte svuj deploy skript od nuly. Naklonujte svuj repozitar na cerstvy server a projdete každý krok rucne. Pocet problemu "funguje na mem pocitaci," které se skryvaji v deploy skriptech, je trapny. Ja jsem ve svem nasel tri — chybejici globalni balicky, spatna opravneni souborů a cesta, která existovala jen kvuli predchozimu rucnimu nastaveni.

Dejte IP adresu sveho serveru do SSH konfigurace. Prestante psát IP adresy:

bash
# ~/.ssh/config
Host akousa
    HostName 69.62.66.94
    User deploy
    IdentityFile ~/.ssh/id_ed25519

Ted stačí ssh akousa. Male věci se nasciitaji.

Kompletni checklist#

Nez to prohlasíte za hotove:

  • Ne-root uzivatel s pristupem sudo
  • Pouze SSH autentifikace klicem, hesla zakazana
  • UFW povoleno s pouze nezbytnymi porty
  • Fail2Ban chrani SSH
  • Automatické bezpecnostni aktualizace povoleny
  • Node.js nainstalovany přes NVM
  • PM2 bezi s vasi aplikaci v cluster modu
  • PM2 startovaci skript nakonfigurovany (prezije restart)
  • PM2 rotace logu nainstalovana
  • Nginx reverse proxy se spravnymi hlavickami
  • SSL přes Let's Encrypt s automatickym obnovenim
  • Deploy skript s health checky
  • Swap soubor nakonfigurovany (rezerva pro build)
  • Otestovano: restartujte server a overte, ze se vse vrati

Ta posledni polozka je ta, kterou lide preskakuji. Nebudte ten člověk. Restartujte server, pockejte 60 sekund a zkontrolujte, zda vase aplikace bezi. Pokud ne, vase startovaci skripty jsou špatně nastavene a zjistite to v tom nejhorsim moznem okamziku.

Je toto "enterprise-grade"?#

Ne. A to je pointa.

Tento setup spolehlivě obsluhuje tento blog za mene nez $10/mesic. Je nasazeny za 30 sekund jednim prikazem. Rozumim každé jeho části. Kdyz se něco rozbije, přesně vim, kam se podivat.

Mohl bych použít Docker? Jiste. Mohl bych použít Kubernetes? Technický ano. Mohl bych nastavit plny CI/CD pipeline se staging prostredimi a canary deploymenty? Rozhodne.

Ale naučil jsem se, ze nejlepsi infrastruktura je ta, které skutečně rozumite, kterou dokázhete debugovat ve dve ráno a která nestoji vic, nez projekt vydelava. Pro osobni web, SaaS MVP nebo maly startup — toto je ten setup.

Nejdriv dodejte. Skalujte, az to budete potrebovat. A vzdy, vzdycky, otestujte svuj deploy skript na cerstevém serveru.