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.
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#
adduser deploy
usermod -aG sudo deployStall in SSH-nyckelautentisering#
Pa din lokala maskin:
ssh-copy-id deploy@your-server-ipInaktivera sedan losenordsautentisering helt:
sudo nano /etc/ssh/sshd_configPasswordAuthentication no
PermitRootLogin nosudo systemctl restart sshdOm 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#
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enableDet ar allt. Fyra regler. Bara SSH och webbtrafik slipper igenom.
Fail2Ban#
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.localRedigera /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 fail2banTre 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#
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgradesDin 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:
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:
node -v # v22.x.x eller vilken LTS som ar aktuell
npm -vDet 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:
nvm alias default 22Det 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#
npm install -g pm2Ekosystemkonfigurationen#
Starta inte appar med CLI-flaggor. Anvand en ecosystem.config.js-fil. Den ar versionshanterad, reproducerbar och sjalvdokumenterande.
// 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:
pm2 startup systemd
# Kopiera och kor kommandot det skriver ut
pm2 saveDet 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:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true50MB 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#
sudo apt install nginx -yKonfigurationen#
# /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:
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 nginxKor 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.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d akousa.net -d www.akousa.netCertbot 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:
sudo systemctl list-timers | grep certbotTesta att fornyelse fungerar:
sudo certbot renew --dry-runOm 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.
#!/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 1Gor det korbart:
chmod +x deploy.shDeploya fran din lokala maskin:
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:
pm2 status
pm2 logs akousa --lines 50
sudo systemctl status nginx
sudo tail -50 /var/log/nginx/error.logNio 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"#
pm2 monitDet 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:
pm2 describe akousaDet har visar drifttid, omstartsraknare och minnesogonblicksbilder. Om du ser 47 omstarter de senaste 24 timmarna ar det din ledtrad.
"SSL-certifikatet har gatt ut"#
sudo certbot certificatesListar alla certifikat med deras utgangsdatum. Om automatisk fornyelse misslyckades:
sudo certbot renew --force-renewal
sudo systemctl reload nginx"Diskutrymmet ar fullt"#
df -h
du -sh /var/log/*
pm2 flushpm2 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#
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.
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:
# ~/.ssh/config
Host akousa
HostName 69.62.66.94
User deploy
IdentityFile ~/.ssh/id_ed25519Nu 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.