Gå till innehåll
·13 min läsning

VPS-uppsättningen som faktiskt fungerar: Node.js, PM2, Nginx och nolltids-deploy

Den exakta VPS-deployuppsattningen jag anvander i produktion — Ubuntu-härdning, PM2 cluster mode, Nginx reverse proxy, SSL och ett deployscript som aldrig svikit mig. Ingen teori, bara det som fungerar.

Dela:X / TwitterLinkedIn

Den har bloggen kors pa en VPS for $10/manad. Inte Vercel, inte AWS, inte ett Kubernetes-kluster som hanteras av ett team pa sex. En enda Ubuntu-box med Nginx, PM2 och ett bash-script som deployar pa under 30 sekunder.

Jag har provst de andra vagarna. Jag har anvant Vercel (bra tills du behover cron-jobb, persistenta WebSockets eller bara kontroll). Jag har anvant AWS (bra om du tycker om att tilbringa halva dagen i IAM-policies). Jag hamnar alltid tillbaka pa en VPS.

Men har ar problemet: varje "deploya till VPS"-tutorial pa internet stannar vid den lyckliga vagen. De visar hur man installerar Node.js och kor node server.js och kallar det produktion. Sedan blir din server SSH-brute-forcad, din process dor klockan 3 pa natten for att ingen satte upp en processhanterare, och ditt SSL-certifikat gick ut for tre manader sedan.

Det har ar guiden jag onskade att jag hade. Allt har ar stridstestat — exakt den har uppsättningen serverar sidan du laser just nu.

Börja med säkerhet, inte kod#

Innan du ens tänker pa Node.js, las ner boxen. Farska VPS-instanser ar mal. Automatiserade bottar börjar traffa din SSH-port inom minuter efter provisionering.

Skapa en icke-root-anvandare#

bash
adduser deploy
usermod -aG sudo deploy

Stall in SSH-nyckelautentisering#

Pa din lokala maskin:

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

Inaktivera sedan losenordsautentisering helt:

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

Om du hoppar över detta kommer du att se tusentals misslyckade inloggningsforsoek i dina autentiseringsloggar inom dagarna. Det ar inte paranoia — det ar tisdag pa det publika internet.

Brandvagg med UFW#

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

Det ar allt. Fyra regler. Bara SSH och webbtrafik slipper igenom.

Fail2Ban#

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

Redigera /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

Tre misslyckade SSH-forsoek och du ar bannlyst i en timme. Jag har sett Fail2Ban blockera hundratals IP-adresser pa en enda dag. Det fungerar.

Obestamannade sakerhetsuppdateringar#

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

Din server kommer nu att autoinstallera sakerhetspatchar. En sak mindre att glomma.

Node.js: Anvand NVM, inte apt#

Jag ser det i varje tutorial: sudo apt install nodejs. Gor inte det.

Ubuntus paketrepon levererar urdaldriga Node.js-versioner. Även NodeSource PPA ligger efter. Och nar du behover vaxla mellan Node 20 och Node 22 for olika projekt sitter du fast.

NVM loser detta:

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/*

Verifiera nu:

bash
node -v  # v22.x.x eller vilken LTS som ar aktuell
npm -v

Det icke-uppenbara tipset: nar du installerar globala paket med NVM (som PM2) ar de bundna till den Node-versionen. Om du byter version med nvm use forsvinner dina globaler. Stall in din standard och hall dig till den pa servern:

bash
nvm alias default 22

Det har har bitit mig exakt en gang. En gang racker.

PM2: Processhanteraren som fortjanar sin plats#

PM2 ar skillnaden mellan "deployad" och "produktionsklar." Den hanterar processhantering, klustring, loggrotation, automatisk omstart vid krascher och startskript. Gratis.

Installera och stall in#

bash
npm install -g pm2

Ekosystemkonfigurationen#

Starta inte appar med CLI-flaggor. Anvand en ecosystem.config.js-fil. Den ar versionshanterad, reproducerbar och sjalvdokumenterande.

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,
      },
      // Gracios avstangning
      kill_timeout: 5000,
      listen_timeout: 10000,
      wait_ready: false,
      // Loggning
      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,
      // Automatisk omstart vid fel
      autorestart: true,
      max_restarts: 10,
      min_uptime: "10s",
      // Overvaka inte i produktion
      watch: false,
    },
  ],
};

Lat mig förklara valen som spelar roll:

instances: 2 istallet for "max": Pa en liten VPS med 1-2 karnor later "max" smart men det kommer att starta processer som strider om resurser under byggen. Tva instanser ger dig nolltidsomstarter samtidigt som utrymme lamnas. Pa en maskin med 4+ karnor, visst, anvand "max".

exec_mode: "cluster": Det ar detta som mojliggor nolltidsomstarter. Utan cluster mode ar pm2 reload bara en fancy omstart. Med cluster mode startar PM2 om instanser en at gangen — din app gar aldrig helt offline.

max_memory_restart: "500M": Har din Next.js-app en minneslacka? PM2 startar om den innan den OOM-dodad din server. Det har har raddat mig fran larm klockan 2 pa natten mer an en gang.

kill_timeout: 5000: Ger din app 5 sekunder att slutfora pagaende forfragan innan PM2 tvangsavslutar den. Standardvardet (1600ms) ar for aggressivt for appar med databasanslutningar.

watch: false: Jag har sett folk lamna watch: true i produktion. PM2 startar da om appen varje gang en loggfil ändras. Din app hamnar i en omstartsloop. Gor inte det.

Startskript#

Lat PM2 overleva omstarter:

bash
pm2 startup systemd
# Kopiera och kor kommandot det skriver ut
pm2 save

Det har genererar en systemd-tjänst. Efter en serveromstart kommer din app tillbaka automatiskt. Testa det — starta om din server och verifiera. Anta inte.

Loggrotation#

Loggar kommer att ata din disk till slut. Installera rotationsmodulen:

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 max per fil, behall 7 roterade filer, komprimera de gamla. Utan detta har jag sett /var/log fylla en 25GB-disk pa tre veckor for en matligt trafikerad app.

Nginx: Reverse proxyn som gor mer an du tror#

"Varför inte bara exponera Node.js direkt pa port 80?"

For att Nginx hanterar säker Node.js inte borde slosa cykler pa: SSL-terminering, servering av statiska filer, gzip-komprimering, request buffering, anslutningsberansningar och gracios hantering av langa klienter. Den ar skriven i C och specialbyggd for detta.

Installera#

bash
sudo apt install nginx -y

Konfigurationen#

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;
 
    # Omdirigera all HTTP till HTTPS
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name akousa.net www.akousa.net;
 
    # SSL (hanteras av Certbot — dessa rader laggs till automatiskt)
    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;
 
    # Sakerhetsheaders
    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-komprimering
    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;
 
    # Proxyinstallningar
    location / {
        proxy_pass http://node_app;
        proxy_http_version 1.1;
 
        # Headers
        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-stod (om du nagonsin behover det)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
 
        # Timeouts — generosa men inte oandliga
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
 
        # Buffring — lat Nginx hantera langa klienter
        proxy_buffering on;
        proxy_buffer_size 16k;
        proxy_buffers 4 32k;
        proxy_busy_buffers_size 64k;
    }
 
    # Next.js statiska tillgangar — lat Nginx servera dem direkt
    location /_next/static/ {
        alias /var/www/akousa.net/.next/static/;
        expires 365d;
        access_log off;
        add_header Cache-Control "public, immutable";
    }
 
    # Publika statiska filer
    location /static/ {
        alias /var/www/akousa.net/public/static/;
        expires 30d;
        access_log off;
    }
 
    # Blockera atkomst till punktfiler
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Aktivera den:

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

Kor alltid nginx -t fore omladdning. Jag pushade en gang en trasig konfiguration och tog ner sajten for att jag hoppade över syntaxkontrollen. De fem tecknen nginx -t hade sparat mig trettio minuters panisk felsökning.

Säker de flesta tutorials missar i den har konfigurationen:

upstream-block med keepalive 64: Nginx ateranvander anslutningar till din Node.js-backend istallet for att oppna en ny TCP-anslutning for varje forfragan. Det har spelar roll under belastning.

proxy_buffering on: Nginx laser hela svaret fran Node.js till minnet, sedan skickar det till klienten i den hastighet klienten klarar av. Utan detta binder en lang klient pa en 3G-anslutning upp din Node.js-worker.

Servera _next/static/ direkt: Det har ar hashade, oforanderliga tillgangar. Lat Nginx servera dem fran disk med en 365-dagars cache-header. Dina Node.js-processer borde inte slosa tid pa det har.

SSL pa fem minuter#

Let's Encrypt loste SSL. Om du fortfarande betalar for certifikat 2026, sluta.

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

Certbot kommer att fraga efter din e-post, acceptera villkoren och automatiskt modifiera din Nginx-konfiguration for att inkludera SSL-direktiven. Det ar allt.

Verifiera automatisk fornyelse#

Certbot installerar en systemd-timer som kontrollerar tva ganger om dagen och fornyar certifikat inom 30 dagar fore utgangen:

bash
sudo systemctl list-timers | grep certbot

Testa att fornyelse fungerar:

bash
sudo certbot renew --dry-run

Om torkkorningen gar igenom behover du aldrig tanka pa SSL igen. Om den misslyckas ar det vanligtvis for att port 80 ar blockerad (kontrollera dina UFW-regler) eller Nginx inte kors.

En sak som fangade mig: om du stallde in Nginx innan du korde Certbot, se till att ditt server-block lyssnar pa port 80 utan HTTPS-omdirigeringen forst. Certbot behover na port 80 for HTTP-01-utmaningen. Efter att Certbot har korts framgangsrikt, da lagger du till omdirigeringen.

Deployscriptet#

Det har ar scriptet som kors varje gang jag pushar till produktion. Ingen CI/CD-plattform, inga GitHub Actions. Bara SSH och bash.

bash
#!/bin/bash
# deploy.sh — nolltids-deployment
 
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 startad ==="
 
cd "$APP_DIR"
 
# Hamta senaste koden
log "Hamtar senaste andringarna..."
git pull origin main 2>&1 | tee -a "$LOG_FILE"
 
# Installera beroenden
log "Installerar beroenden..."
npm install --legacy-peer-deps 2>&1 | tee -a "$LOG_FILE"
 
# Bygg
log "Bygger applikationen..."
rm -rf .next
npm run build 2>&1 | tee -a "$LOG_FILE"
 
if [ $? -ne 0 ]; then
    log "FEL: Bygget misslyckades. Avbryter deploy."
    exit 1
fi
 
# Ladda om PM2 (nolltid i cluster mode)
log "Laddar om PM2..."
pm2 reload "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
pm2 save 2>&1 | tee -a "$LOG_FILE"
 
# Halsokontroll med omforsoek
log "Kor halsokontroll..."
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 "Halsokontrollen godkand (HTTP $HTTP_CODE)"
        log "=== Deploy slutford framgangsrikt ==="
        exit 0
    fi
    log "Halsokontrollforsok $i/$MAX_RETRIES (HTTP $HTTP_CODE). Forsoker igen om ${RETRY_INTERVAL}s..."
    sleep $RETRY_INTERVAL
done
 
log "FEL: Halsokontrollen misslyckades efter $MAX_RETRIES forsok"
log "Aterstaller till tidigare PM2-tillstand..."
pm2 restart "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
exit 1

Gor det korbart:

bash
chmod +x deploy.sh

Deploya fran din lokala maskin:

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

Viktiga beslut i det har scriptet:

set -euo pipefail: Scriptet avslutas omedelbart vid varje fel. Utan detta fortsatter en misslyckad npm install tyst in i byggsteget, och du far ett kryptiskt felmeddelande som tar 20 minuter att debugga.

rm -rf .next fore bygget: Next.js har en byggcache som ibland producerar gammal utdata. Jag blev biten av det en gang — en sida visade gammalt innehall trots att kallkoden var uppdaterad. Att radera byggkatalogen lagger kanske 15 sekunder till bygget men garanterar farskt resultat.

pm2 reload istallet for pm2 restart: Det ar den har som ger nolltid. I cluster mode utfor reload en rullande omstart — den startar nya instanser med den uppdaterade koden, vantar tills de ar redo och stangar sedan graciost de gamla. Vid inget tillfalle kors noll instanser.

Halsokontroll med omforsoek: Next.js tar några sekunder att varma upp efter omstart. Scriptet vantar upp till 30 sekunder (10 omforsoek x 3 sekunder) och kontrollerar om appen svarar med HTTP 200. Om den inte gor det ar något fel och du behover veta det omedelbart — inte fa reda pa det fran en anvandare.

Aterstallning vid misslyckande: Om halsokontrollen misslyckas efter alla forsoek startar scriptet om PM2 (som laddar det senast sparade tillstandet). Det ar inte en perfekt aterstallning, men det ar bättre an att lamna servern i ett trasigt tillstand.

Nar säker gar sönder klockan 2 pa natten#

Har ar vad jag faktiskt har debuggat pa exakt den har uppsättningen:

"Sajten ar nere"#

Forsta kommandon att kora:

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

Nio ganger av tio berattar pm2 logs omedelbart vad som hande. En saknad miljovariabel, en misslyckad databasanslutning eller ett ohanterat promise rejection.

"Minnet vaxer hela tiden"#

bash
pm2 monit

Det har ger dig en live-dashboard över CPU och minne per process. Om minnet klattrar stadigt utan att plana ut har du en lacka. Installningen max_memory_restart i din ekosystemkonfiguration ar ditt sakerhetsnat — PM2 startar om processen innan den tar ner servern.

For djupare undersokning:

bash
pm2 describe akousa

Det har visar drifttid, omstartsraknare och minnesogonblicksbilder. Om du ser 47 omstarter de senaste 24 timmarna ar det din ledtrad.

"SSL-certifikatet har gatt ut"#

bash
sudo certbot certificates

Listar alla certifikat med deras utgangsdatum. Om automatisk fornyelse misslyckades:

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

"Diskutrymmet ar fullt"#

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

pm2 flush rensar alla PM2-loggfiler omedelbart. Om du inte stallde in loggrotation (jag sa ju det) ar det har du kanner smartan.

Kommandot jag kor varje morgon#

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

Tre säker pa en rad: kor mina processer, ar min disk okej, ar servern overbelastad. Tar tva sekunder. Fangar problem innan anvandarna gor det.

Vad de flesta guider inte berattar#

Ditt byggsteg ar din storsta sårbarhet. Pa en 1GB RAM VPS kan npm run build for en Next.js-app forbruka 800MB+ minne. Om PM2 kor din app i tva instanser under bygget far du OOM. Lösningar: anvand en swapfil (minst 2GB), eller stoppa appen under byggen och acceptera några sekunders nedtid. Jag anvander 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 i ditt installationskommando ar en kodlukt, inte en lösning. Jag anvander det for att vissa paket i mitt beroendetrad inte har uppdaterat sina peer dependency-intervall. Var någon manad försöker jag ta bort det. Någon dag kommer det att fungera. Tills dess levererar jag.

Testa ditt deployscript fran grunden. Klona ditt repo pa en farski server och kor varje steg manuellt. Antalet "fungerar pa min maskin"-problem som gommer sig i deployscript ar pinsamt. Jag hittade tre problem i mitt nar jag gjorde det — saknade globala paket, fel filratigheter och en sokvag som bara existerade pa grund av en tidigare manuell uppsättning.

Lagg din servers IP i din SSH-konfiguration. Sluta skriva IP-adresser:

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

Nu ar ssh akousa allt du behover. Sma säker ackumuleras.

Den fullstandiga checklistan#

Innan du kallar det klart:

  • Icke-root-anvandare med sudo-åtkomst
  • Enbart SSH-nyckelautentisering, losenordsautentisering inaktiverad
  • UFW aktiverad med bara nödvändiga portar oppna
  • Fail2Ban skyddar SSH
  • Obestamannade sakerhetsuppgraderingar aktiverade
  • Node.js installerad via NVM
  • PM2 kor din app i cluster mode
  • PM2-startskript konfigurerat (overlever omstart)
  • PM2-loggrotation installerad
  • Nginx reverse proxy med korrekta headers
  • SSL via Let's Encrypt med automatisk fornyelse
  • Deployscript med halsokontroller
  • Swapfil konfigurerad (for byggutrymme)
  • Testat: starta om servern och verifiera att allt kommer tillbaka

Den sista punkten ar den folk hoppar över. Var inte den personen. Starta om servern, vanta 60 sekunder och kontrollera om din app ar live. Om den inte ar det ar dina startskript felkonfigurerade och du kommer att fa reda pa det vid samsta mojliga tillfalle.

Ar det har "enterprise-grade"?#

Nej. Och det ar hela poangen.

Den har uppsättningen serverar den har bloggen palitligt for under $10/manad. Den deployar pa 30 sekunder med ett enda kommando. Jag förstår varje del av den. Nar något gar sönder vet jag exakt var jag ska titta.

Skulle jag kunna anvanda Docker? Visst. Skulle jag kunna anvanda Kubernetes? Tekniskt sett. Skulle jag kunna satta upp en fullstandig CI/CD-pipeline med staging-miljoer och canary-deployer? Absolut.

Men jag har lart mig att den basta infrastrukturen ar den du faktiskt förstår, kan debugga klockan 2 pa natten och som inte kostar mer an vad projektet tjanar. For en personlig sajt, en SaaS-MVP eller en liten startup — det ar den har uppsättningen.

Leverera forst. Skala nar du behover. Och testa alltid, alltid, ditt deployscript pa en farski server.