DEV Community

Alex Chen
Alex Chen

Posted on

Deploying a Node.js App to Production: The Complete 2026 Guide

Deploying a Node.js App to Production: The Complete 2026 Guide

From local development to live server with HTTPS, monitoring, and auto-restarts. Everything you need.

What You'll End Up With

Your Domain (https://app.example.com)
    ↓ Cloudflare (CDN + SSL)
    ↓ Nginx (reverse proxy)
    ↓ Node.js (your app)
    ↓ SQLite / PostgreSQL (database)
    ↓ PM2 (process manager)
    ↓ Systemd (auto-restart on boot)
Enter fullscreen mode Exit fullscreen mode

Total cost: $5/month for a VPS. No Heroku, no Railway, no Render fees.

Step 1: Get a Server

I use a $5/month VPS (2 cores, 4GB RAM). Any provider works:

  • DigitalOcean — Developer-friendly, good docs ($5-20/mo)
  • Hetzner — Best price/performance in Europe (~€4/mo)
  • Linode (Akamai) — Reliable, global network ($5/mo)
  • Vultr — Many locations, hourly billing ($5/mo)
# After creating your VPS, SSH in:
ssh root@your-server-ip

# First things first:
apt update && apt upgrade -y
# Create a non-root user (security best practice)
adduser deploy
usermod -aG sudo deploy
su - deploy
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Node.js

# Don't use apt's old Node! Use nvm or NodeSource:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

node --version   # v22.x
npm --version    # 10.x

# Global tools you'll need:
sudo npm i -g pm2
Enter fullscreen mode Exit fullscreen mode

Step 3: Prepare Your App

# Clone your repo
cd /var/www
git clone https://github.com/you/your-app.git
cd your-app
npm ci --production  # Only production deps

# Environment variables
cp .env.example .env
nano .env  # Fill in your secrets
Enter fullscreen mode Exit fullscreen mode

Step 4: PM2 Process Manager

PM2 keeps your app running, restarts it if it crashes, and handles log management.

# Start your app with PM2
pm2 start server.js --name "myapp"

# Or use an ecosystem file for multiple apps:
pm2 ecosystem init  # Generates ecosystem.config.js
Enter fullscreen mode Exit fullscreen mode
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'myapp',
    script: 'server.js',
    cwd: '/var/www/your-app',

    // Auto-restart on crash
    autorestart: true,
    max_restarts: 10,
    max_memory_restart: '512M',

    // Environment
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000,
    },

    // Logging
    error_file: '/var/log/myapp/error.log',
    out_file: '/var/log/myapp/out.log',
    merge_logs: true,

    // Cluster mode (optional, for multi-core)
    instances: 'max',
    exec_mode: 'cluster',
  }]
};
Enter fullscreen mode Exit fullscreen mode
pm2 start ecosystem.config.js --env production
pm2 save                    # Save process list (survives reboot)
pm2 startup                 # Generate systemd startup script
# Run the command it outputs to enable auto-start on boot
Enter fullscreen mode Exit fullscreen mode

Step 5: Nginx Reverse Proxy

Nginx sits in front of your app. It handles SSL, static files, rate limiting, and proxies requests to Node.js.

sudo apt install nginx
Enter fullscreen mode Exit fullscreen mode
# /etc/nginx/sites-available/your-app.conf
server {
    listen 80;
    server_name app.example.com;

    # Redirect HTTP → HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name app.example.com;

    # SSL certificates (Let's Encrypt, see below)
    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # Security headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=general:10r/s;
    limit_req zone=general burst=20 nodelay;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Static files (if any) — serve directly without hitting Node
    location /static/ {
        alias /var/www/your-app/public/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}
Enter fullscreen mode Exit fullscreen mode
sudo ln -s /etc/nginx/sites-available/your-app.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Step 6: Free SSL with Let's Encrypt

sudo apt install certbot python3-certbot-nginx

# Automatic SSL setup (handles Nginx config too):
sudo certbot --nginx -d app.example.com

# Test auto-renewal:
sudo certbot renew --dry-run
Enter fullscreen mode Exit fullscreen mode

It sets up automatic renewal via cron. Your SSL just works forever.

Step 7: Firewall & Hardening

# UFW firewall
sudo ufw allow OpenSSH      # Port 22
sudo ufw allow 'Nginx Full'  # Ports 80 + 443
sudo ufw enable              # Block everything else

# Fail2Ban (auto-ban brute force attempts)
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Disable root login (after setting up deploy user)
sudo sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart sshd
Enter fullscreen mode Exit fullscreen mode

Step 8: Monitoring

PM2 Monitoring

pm2 monit          # Terminal dashboard
pm2 list           # List all processes
pm2 logs myapp     # View logs
pm2 show myapp     # Detailed info
Enter fullscreen mode Exit fullscreen mode

Basic Health Check Endpoint

Add this to your app:

app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    timestamp: new Date().toISOString(),
  });
});
Enter fullscreen mode Exit fullscreen mode

Uptime Monitor (Free)

Set up UptimeRobot (free tier: 50 monitors):

  • Monitor https://app.example.com/health
  • Get email alerts when down
  • Track uptime percentage

Step 9: Deployment Script

Stop manually copying files. Use a deployment script:

#!/bin/bash
# deploy.sh — run this to deploy latest code
set -e

APP_DIR="/var/www/your-app"
APP_NAME="myapp"

echo "🔄 Pulling latest code..."
cd "$APP_DIR"
git pull origin main

echo "📦 Installing dependencies..."
npm ci --production

echo "🔄 Restarting app..."
pm2 restart "$APP_NAME"

echo "✅ Deployed!"
pm2 logs "$APP_NAME" --lines 10 --nostream
Enter fullscreen mode Exit fullscreen mode
chmod +x deploy.sh
./deploy.sh
Enter fullscreen mode Exit fullscreen mode

For zero-downtime deployments with PM2:

# Instead of pm2 restart, use reload (graceful):
pm2 reload myapp  # Reloads cluster workers one by one
Enter fullscreen mode Exit fullscreen mode

Step 10: Daily Backups

#!/bin/bash
# /opt/backup.sh
BACKUP_DIR="/opt/backups"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

# Backup database (SQLite example)
if [ -f "/var/www/your-app/data.db" ]; then
    cp "/var/www/your-app/data.db" "$BACKUP_DIR/data_$DATE.db"
fi

# Backup app code (optional)
cd /var/www
tar czf "$BACKUP_DIR/app_$DATE.tar.gz" your-app/

# Keep only last 7 days
find "$BACKUP_DIR" -mtime +7 -delete

echo "Backup complete: $(date)"
Enter fullscreen mode Exit fullscreen mode
# Add to cron (runs daily at 3 AM):
crontab -e
# Add line: 0 3 * * * /opt/backup.sh >> /var/log/backup.log 2>&1
Enter fullscreen mode Exit fullscreen mode

The Complete Checklist

  • [ ] Server created, non-root user set up
  • [ ] Node.js v22+ installed (via NodeSource)
  • [ ] App cloned, npm ci --production ran
  • [ ] .env file configured (not committed to git)
  • [ ] PM2 running with ecosystem.config.js
  • [ ] PM2 startup script installed (pm2 startup)
  • [ ] Nginx installed and configured as reverse proxy
  • [ ] Let's Encrypt SSL certificate active
  • [ ] UFW firewall enabled (only 22, 80, 443 open)
  • [ ] Fail2Ban running
  • [ ] Health check endpoint working
  • [ ] Deployment script ready
  • [ ] Daily backups configured
  • [ ] Uptime monitor set up

Cost Breakdown

Item Monthly Cost
VPS (2 cores, 4GB) $5
Domain name ~$1
SSL (Let's Encrypt) $0
CDN (Cloudflare free tier) $0
Monitoring (UptimeRobot free) $0
Total ~$6/month

Compare that to:

  • Heroku: $7-50+/month (dyno costs)
  • Railway: $5-20/month (free tier ends fast)
  • Render: $7+/month (free tier limited)
  • Vercel: Free for frontend, $20+/month for backend

Self-hosting saves money AND gives you full control.


Need help with deployment? Drop a comment or find me on GitHub: @armorbreak001

Follow @armorbreak for more practical deployment guides.

Top comments (0)