सामग्री पर जाएं
·15 मिनट पढ़ने का समय

VPS Setup जो वास्तव में काम करता है: Node.js, PM2, Nginx, और Zero-Downtime Deploys

वह exact VPS deployment setup जो मैं production में उपयोग करता हूं — Ubuntu hardening, PM2 cluster mode, Nginx reverse proxy, SSL, और एक deploy script जिसने मुझे कभी निराश नहीं किया। कोई theory नहीं, बस जो काम करता है।

साझा करें:X / TwitterLinkedIn

यह ब्लॉग $10/month के VPS पर चलता है। Vercel नहीं, AWS नहीं, छह लोगों की टीम द्वारा managed Kubernetes cluster नहीं। एक single Ubuntu box जिसमें Nginx, PM2, और एक bash script है जो 30 seconds से कम में deploy करता है।

मैंने दूसरे रास्ते आज़माए हैं। मैंने Vercel उपयोग किया है (बढ़िया है जब तक आपको cron jobs, persistent WebSockets, या बस control नहीं चाहिए)। मैंने AWS उपयोग किया है (बढ़िया है अगर आप अपना आधा दिन IAM policies में बिताना enjoy करते हैं)। मैं हमेशा VPS पर वापस आ जाता हूं।

लेकिन यह रही समस्या: internet पर हर "deploy to VPS" tutorial happy path पर रुक जाता है। वे आपको दिखाते हैं कि Node.js कैसे install करें और node server.js run करें और इसे production कह दें। फिर आपका server SSH brute-force हो जाता है, आपकी process रात 3 बजे मर जाती है क्योंकि किसी ने process manager set up नहीं किया, और आपका SSL cert तीन महीने पहले expire हो गया।

यह वह guide है जो मैं चाहता था कि मेरे पास होती। यहां सब कुछ battle-tested है — यही exact setup वह page serve कर रहा है जो आप अभी पढ़ रहे हैं।

पहले Security, Code नहीं#

Node.js के बारे में सोचने से पहले, box को lock down करें। Fresh VPS instances targets हैं। Automated bots provisioning के minutes के भीतर आपके SSH port को hit करना शुरू कर देते हैं।

Non-Root User बनाएं#

bash
adduser deploy
usermod -aG sudo deploy

SSH Key Authentication Set Up करें#

अपनी local machine पर:

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

फिर password authentication पूरी तरह disable करें:

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

अगर आप यह skip करते हैं, तो कुछ दिनों के भीतर आपके auth logs में हज़ारों failed login attempts दिखेंगे। यह paranoia नहीं है — public internet पर यह सामान्य बात है।

UFW के साथ Firewall#

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

बस। चार rules। सिर्फ SSH और web traffic गुज़रती है।

Fail2Ban#

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

/etc/fail2ban/jail.local edit करें:

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

तीन failed SSH attempts और आप एक घंटे के लिए ban हैं। मैंने Fail2Ban को एक ही दिन में सैकड़ों IPs block करते देखा है। यह काम करता है।

Unattended Security Updates#

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

आपका server अब auto-install security patches करेगा। एक कम चीज़ भूलने के लिए।

Node.js: NVM उपयोग करें, apt नहीं#

मैं यह हर tutorial में देखता हूं: sudo apt install nodejs। ऐसा मत करें।

Ubuntu के package repos पुरानी Node.js versions ship करते हैं। NodeSource PPA भी पीछे रहता है। और जब आपको अलग-अलग projects के लिए Node 20 और Node 22 के बीच switch करना हो, तो आप फंस जाते हैं।

NVM यह solve करता है:

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

अब verify करें:

bash
node -v  # v22.x.x या जो भी current LTS है
npm -v

जो tip obvious नहीं है: जब आप NVM के साथ global packages install करते हैं (जैसे PM2), वे उस Node version से tied होते हैं। अगर आप nvm use से versions switch करते हैं, आपके globals गायब हो जाते हैं। Server पर अपना default set करें और उसी पर रहें:

bash
nvm alias default 22

यह मुझे ठीक एक बार काटा है। एक बार काफी था।

PM2: वह Process Manager जो अपनी कीमत साबित करता है#

PM2 "deployed" और "production-ready" के बीच का फ़र्क़ है। यह process management, clustering, log rotation, crashes पर auto-restart, और startup scripts handle करता है। मुफ्त में।

Install और Set Up#

bash
npm install -g pm2

Ecosystem Config#

CLI flags से apps start मत करें। एक ecosystem.config.js file उपयोग करें। यह version-controlled, reproducible, और self-documenting है।

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,
      },
      // Graceful shutdown
      kill_timeout: 5000,
      listen_timeout: 10000,
      wait_ready: false,
      // Logging
      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,
      // Failure पर auto-restart
      autorestart: true,
      max_restarts: 10,
      min_uptime: "10s",
      // Production में watch मत करें
      watch: false,
    },
  ],
};

आइए उन choices को समझते हैं जो मायने रखती हैं:

instances: 2 "max" की बजाय: 1-2 cores वाले छोटे VPS पर, "max" smart लगता है लेकिन builds के दौरान resources के लिए लड़ने वाली processes spawn करेगा। दो instances आपको zero-downtime reloads देता है headroom छोड़ते हुए। 4+ core machine पर, ज़रूर, "max" उपयोग करें।

exec_mode: "cluster": यही zero-downtime reloads enable करता है। Cluster mode के बिना, pm2 reload बस एक fancy restart है। Cluster mode के साथ, PM2 instances एक-एक करके restart करता है — आपकी app कभी पूरी तरह offline नहीं जाती।

max_memory_restart: "500M": आपकी Next.js app में memory leak है? PM2 आपके server को OOM-kill होने से पहले restart कर देगा। इसने मुझे रात 2 बजे के alerts से एक से ज़्यादा बार बचाया है।

kill_timeout: 5000: आपकी app को in-flight requests finish करने के लिए 5 seconds देता है PM2 द्वारा force-kill करने से पहले। Default (1600ms) database connections वाली apps के लिए बहुत aggressive है।

watch: false: मैंने लोगों को production में watch: true छोड़ते देखा है। PM2 फिर हर बार log file change होने पर app restart करता है। आपकी app restart loop में चली जाती है। ऐसा मत करें।

Startup Script#

PM2 को reboots survive कराएं:

bash
pm2 startup systemd
# जो command यह output करे उसे copy करके run करें
pm2 save

यह एक systemd service generate करता है। Server reboot के बाद, आपकी app automatically वापस आ जाती है। इसे test करें — अपना server reboot करें और verify करें। Assume मत करें।

Log Rotation#

Logs आखिरकार आपकी disk खा जाएंगे। Rotation module install करें:

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 प्रति file, 7 rotated files रखें, पुरानी compress करें। इसके बिना, मैंने एक moderately trafficked app पर /var/log को तीन हफ्तों में 25GB disk भरते देखा है।

Nginx: वह Reverse Proxy जो आप सोचते हैं उससे ज़्यादा करता है#

"Node.js को सीधे port 80 पर expose क्यों नहीं?"

क्योंकि Nginx वे चीज़ें handle करता है जिन पर Node.js को cycles waste नहीं करने चाहिए: SSL termination, static file serving, gzip compression, request buffering, connection limits, और slow clients का graceful handling। यह C में लिखा है और इसी काम के लिए बना है।

Install#

bash
sudo apt install nginx -y

Config#

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;
 
    # सभी HTTP को HTTPS पर redirect करें
    return 301 https://$host$request_uri;
}
 
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name akousa.net www.akousa.net;
 
    # SSL (Certbot द्वारा managed — ये lines automatically add होती हैं)
    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;
 
    # Security headers
    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 compression
    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;
 
    # Proxy settings
    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 support (अगर कभी ज़रूरत हो)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
 
        # Timeouts — generous लेकिन infinite नहीं
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
 
        # Buffering — Nginx को slow clients handle करने दें
        proxy_buffering on;
        proxy_buffer_size 16k;
        proxy_buffers 4 32k;
        proxy_busy_buffers_size 64k;
    }
 
    # Next.js static assets — Nginx को सीधे serve करने दें
    location /_next/static/ {
        alias /var/www/akousa.net/.next/static/;
        expires 365d;
        access_log off;
        add_header Cache-Control "public, immutable";
    }
 
    # Public static files
    location /static/ {
        alias /var/www/akousa.net/public/static/;
        expires 30d;
        access_log off;
    }
 
    # Dot files तक access block करें
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Enable करें:

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

Reload करने से पहले हमेशा nginx -t run करें। मैंने एक बार broken config push की और site down कर दी क्योंकि मैंने syntax check skip किया। पांच characters nginx -t ने मुझे तीस मिनट की panicked debugging बचाई होती।

ज़्यादातर tutorials इस config में जो miss करते हैं:

upstream block keepalive 64 के साथ: Nginx हर request के लिए नया TCP connection खोलने की बजाय आपके Node.js backend से connections reuse करता है। Load के under यह मायने रखता है।

proxy_buffering on: Nginx Node.js से पूरी response memory में पढ़ता है, फिर client को जिस speed से client handle कर सके उस speed से भेजता है। इसके बिना, 3G connection पर एक slow client आपके Node.js worker को tie up करता है।

_next/static/ directly serve करना: ये hashed, immutable assets हैं। Nginx को 365-day cache header के साथ disk से serve करने दें। आपकी Node.js processes को इस पर समय waste नहीं करना चाहिए।

पांच मिनट में SSL#

Let's Encrypt ने SSL solve कर दिया। अगर आप 2026 में अभी भी certificates के लिए pay कर रहे हैं, रुकें।

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

Certbot आपका email पूछेगा, ToS accept करेगा, और automatically आपके Nginx config में SSL directives add करेगा। बस।

Auto-Renewal Verify करें#

Certbot एक systemd timer install करता है जो दिन में दो बार check करता है और expiration से 30 दिन पहले certificates renew करता है:

bash
sudo systemctl list-timers | grep certbot

Test करें कि renewal काम करता है:

bash
sudo certbot renew --dry-run

अगर dry run pass होता है, आप फिर कभी SSL के बारे में नहीं सोचेंगे। अगर fail होता है, तो आमतौर पर इसलिए कि port 80 blocked है (अपने UFW rules check करें) या Nginx run नहीं हो रहा।

एक बात जिसने मुझे पकड़ा: अगर आप Certbot run करने से पहले Nginx set up करते हैं, तो सुनिश्चित करें कि आपका server block HTTPS redirect बिना port 80 पर listen कर रहा हो। Certbot को HTTP-01 challenge के लिए port 80 तक पहुंचना ज़रूरी है। Certbot successfully run होने के बाद, तब redirect add करें।

Deploy Script#

यह वह script है जो हर बार जब मैं production में push करता हूं तब run होती है। कोई CI/CD platform नहीं, कोई GitHub Actions नहीं। बस SSH और bash।

bash
#!/bin/bash
# deploy.sh — zero-ish downtime 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 शुरू ==="
 
cd "$APP_DIR"
 
# Latest code pull करें
log "Latest changes pull कर रहे हैं..."
git pull origin main 2>&1 | tee -a "$LOG_FILE"
 
# Dependencies install करें
log "Dependencies install कर रहे हैं..."
npm install --legacy-peer-deps 2>&1 | tee -a "$LOG_FILE"
 
# Build
log "Application build कर रहे हैं..."
rm -rf .next
npm run build 2>&1 | tee -a "$LOG_FILE"
 
if [ $? -ne 0 ]; then
    log "ERROR: Build fail हुआ। Deploy abort कर रहे हैं।"
    exit 1
fi
 
# PM2 reload करें (cluster mode में zero-downtime)
log "PM2 reload कर रहे हैं..."
pm2 reload "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
pm2 save 2>&1 | tee -a "$LOG_FILE"
 
# Retries के साथ health check
log "Health check run कर रहे हैं..."
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 pass हुआ (HTTP $HTTP_CODE)"
        log "=== Deploy सफलतापूर्वक पूरा हुआ ==="
        exit 0
    fi
    log "Health check attempt $i/$MAX_RETRIES (HTTP $HTTP_CODE)। ${RETRY_INTERVAL}s में retry कर रहे हैं..."
    sleep $RETRY_INTERVAL
done
 
log "ERROR: $MAX_RETRIES attempts के बाद health check fail हुआ"
log "पिछली PM2 state में rollback कर रहे हैं..."
pm2 restart "$APP_NAME" 2>&1 | tee -a "$LOG_FILE"
exit 1

इसे executable बनाएं:

bash
chmod +x deploy.sh

अपनी local machine से deploy करें:

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

इस script में key decisions:

set -euo pipefail: किसी भी error पर script immediately exit हो जाती है। इसके बिना, एक failed npm install चुपचाप build step में continue करता है, और आपको एक cryptic error मिलता है जिसे debug करने में 20 मिनट लगते हैं।

Build से पहले rm -rf .next: Next.js का एक build cache है जो कभी-कभी stale output produce करता है। यह मुझे एक बार काटा — source code update होने के बावजूद एक page पुराना content दिखा रहा था। Build directory nuke करना build में शायद 15 seconds जोड़ता है लेकिन fresh output guarantee करता है।

pm2 restart की बजाय pm2 reload: यही zero-downtime वाला हिस्सा है। Cluster mode में, reload rolling restart करता है — updated code के साथ नए instances लाता है, उनके ready होने का इंतज़ार करता है, फिर gracefully पुरानों को shut down करता है। किसी भी point पर zero instances running नहीं होते।

Retries के साथ health check: Next.js को restart के बाद warm up होने में कुछ seconds लगते हैं। Script 30 seconds तक wait करती है (10 retries x 3 seconds), check करती है कि app HTTP 200 के साथ respond करती है। अगर नहीं करती, कुछ गलत है और आपको तुरंत जानना होगा — किसी user से पता चलने पर नहीं।

Failure पर rollback: अगर सभी retries के बाद health check fail होता है, script PM2 restart करती है (जो last saved state load करता है)। यह perfect rollback नहीं है, लेकिन server को broken state में छोड़ने से बेहतर है।

जब रात 2 बजे चीज़ें टूटती हैं#

यह रहा जो मैंने इसी exact setup पर वास्तव में debug किया है:

"Site down है"#

पहले commands जो run करें:

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

दस में से नौ बार, pm2 logs तुरंत बता देता है क्या हुआ। एक missing environment variable, एक failed database connection, या एक unhandled promise rejection।

"Memory बढ़ती जा रही है"#

bash
pm2 monit

यह आपको प्रति process CPU और memory का live dashboard देता है। अगर memory level off हुए बिना steadily climb करती है, आपके पास leak है। आपके ecosystem config में max_memory_restart setting आपका safety net है — PM2 server down होने से पहले process restart कर देगा।

गहरी investigation के लिए:

bash
pm2 describe akousa

यह uptime, restart count, और memory snapshots दिखाता है। अगर आप पिछले 24 hours में 47 restarts देखते हैं, यही आपका hint है।

"SSL certificate expire हो गया"#

bash
sudo certbot certificates

सभी certificates उनकी expiration dates के साथ list करता है। अगर auto-renewal fail हुआ:

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

"Disk space full है"#

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

pm2 flush सभी PM2 log files तुरंत clear करता है। अगर आपने log rotation set up नहीं किया (मैंने कहा था), यहां आपको दर्द महसूस होता है।

वह Command जो मैं हर सुबह Run करता हूं#

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

एक line में तीन चीज़ें: क्या मेरी processes run हो रही हैं, क्या मेरी disk ठीक है, क्या server overloaded है। दो seconds लगते हैं। Users से पहले problems पकड़ता है।

जो ज़्यादातर Guides नहीं बताएंगे#

आपका build step आपकी सबसे बड़ी vulnerability है। 1GB RAM VPS पर, Next.js app के लिए npm run build 800MB+ memory consume कर सकता है। अगर PM2 build के दौरान आपकी app दो instances में run कर रहा है, आप OOM हो जाएंगे। Solutions: swap file उपयोग करें (कम से कम 2GB), या builds के दौरान app stop करें और कुछ seconds का downtime accept करें। मैं 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

आपके install command में --legacy-peer-deps एक code smell है, solution नहीं। मैं इसे इसलिए उपयोग करता हूं क्योंकि मेरी dependency tree में कुछ packages ने अपनी peer dependency ranges update नहीं की हैं। हर कुछ महीने मैं इसे हटाने की कोशिश करता हूं। कभी न कभी काम करेगा। तब तक, मैं ship करता हूं।

अपनी deploy script को scratch से test करें। अपने repo को fresh server पर clone करें और हर step manually run करें। Deploy scripts में छिपी "works on my machine" issues की संख्या शर्मनाक है। मैंने अपनी में तीन issues पाए जब मैंने यह किया — missing global packages, wrong file permissions, और एक path जो सिर्फ पिछले manual setup की वजह से exist करता था।

अपने server का IP अपने SSH config में डालें। IP addresses type करना बंद करें:

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

अब ssh akousa बस इतना चाहिए। छोटी चीज़ें compound करती हैं।

पूरी Checklist#

इससे पहले कि आप कहें "हो गया":

  • Sudo access वाला non-root user
  • सिर्फ SSH key auth, password auth disabled
  • सिर्फ ज़रूरी ports open के साथ UFW enabled
  • SSH protect करता Fail2Ban
  • Unattended security upgrades enabled
  • NVM के माध्यम से Node.js installed
  • Cluster mode में PM2 आपकी app run कर रहा
  • PM2 startup script configured (reboot survive करता है)
  • PM2 log rotation installed
  • Proper headers के साथ Nginx reverse proxy
  • Auto-renewal के साथ Let's Encrypt SSL
  • Health checks के साथ deploy script
  • Swap file configured (build headroom के लिए)
  • Tested: server reboot करें और verify करें कि सब कुछ वापस आता है

वह आखिरी item वह है जो लोग skip करते हैं। वह person मत बनें। Server reboot करें, 60 seconds wait करें, और check करें कि आपकी app live है। अगर नहीं है, आपकी startup scripts misconfigured हैं और आपको सबसे बुरे possible time पर पता चलेगा।

क्या यह "Enterprise-Grade" है?#

नहीं। और यही point है।

यह setup इस ब्लॉग को $10/month से कम में reliably serve करता है। यह एक single command से 30 seconds में deploy होता है। मैं इसके हर हिस्से को समझता हूं। जब कुछ टूटता है, मुझे ठीक-ठीक पता है कहां देखना है।

क्या मैं Docker उपयोग कर सकता हूं? ज़रूर। क्या मैं Kubernetes उपयोग कर सकता हूं? Technically। क्या मैं staging environments और canary deployments के साथ पूरी CI/CD pipeline set up कर सकता हूं? बिल्कुल।

लेकिन मैंने सीखा है कि सबसे अच्छी infrastructure वह है जिसे आप वास्तव में समझते हैं, रात 2 बजे debug कर सकते हैं, और project जितना earn करता है उससे ज़्यादा cost नहीं करती। एक personal site, SaaS MVP, या छोटे startup के लिए — यही वह setup है।

पहले ship करें। ज़रूरत पड़ने पर scale करें। और हमेशा, हमेशा, अपनी deploy script fresh server पर test करें।