Back

VPS Deployment Made Easy: Your Complete Guide to Node.js/Next.js Setup

GeneralFebruary 2, 2026Marc Tyson CLEBERT

Complete VPS Deployment Guide: Node.js/Next.js Production Setup

A step-by-step guide to deploying a production-ready web application on a VPS with security hardening, SSL, reverse proxy, and automated backups.


Table of Contents

  1. Prerequisites
  2. Initial VPS Setup & SSH Access
  3. Create Sudo User & SSH Key Authentication
  4. Harden SSH Security
  5. Setup Automatic Security Updates
  6. Configure Firewall (UFW)
  7. Install & Configure Nginx
  8. Setup DNS Records
  9. Install SSL Certificates (Certbot)
  10. Install Node.js, NVM, and PM2
  11. Setup Git SSH Authentication
  12. Install & Configure PostgreSQL
  13. Deploy Your Application
  14. Configure Nginx Reverse Proxy
  15. Setup Automated Database Backups
  16. Configure Subdomains (Optional)
  17. Maintenance & Troubleshooting

Prerequisites

What you need:

  • A VPS (Virtual Private Server) with Ubuntu 22.04 or 24.04
  • A domain name (e.g., yourdomain.com)
  • Access to your domain's DNS settings
  • Your VPS IP address
  • Root access to your VPS (temporarily, for initial setup)

Example values used in this guide:

  • VPS IP: 123.456.789.012
  • Domain: yourdomain.com
  • VPS Username: deployuser
  • Database Name: appdb
  • Database User: dbuser

Replace these with your actual values!


1. Initial VPS Setup & SSH Access

Step 1: First Login as Root

From your local machine:

ssh root@123.456.789.012

Enter the root password provided by your VPS provider.


Step 2: Update System Packages

apt update && apt upgrade -y

2. Create Sudo User & SSH Key Authentication

Step 1: Create New User

# Create user (replace 'deployuser' with your preferred username)
adduser deployuser

You'll be prompted to:

  • Set a strong password
  • Fill in user info (optional, press Enter to skip)

Step 2: Add User to Sudo Group

usermod -aG sudo deployuser

Step 3: Test Sudo Access

su - deployuser
sudo whoami

Should output: root

Exit back to root:

exit

Step 4: Generate SSH Key on Local Machine

On your local machine (NOT the VPS):

# Check if you already have SSH keys
ls -la ~/.ssh/id_ed25519.pub

# If not, generate a new key pair
ssh-keygen -t ed25519 -C "your-email@example.com"

When prompted:

  • File location: Press Enter (default)
  • Passphrase: Optional but recommended

View your public key:

cat ~/.ssh/id_ed25519.pub

Copy the entire output (starts with ssh-ed25519 ...)


Step 5: Add Public Key to VPS

Back on the VPS as root:

# Switch to your new user
su - deployuser

# Create .ssh directory
mkdir -p ~/.ssh
chmod 700 ~/.ssh

# Create authorized_keys file
nano ~/.ssh/authorized_keys

Paste your public key into this file.

Save and exit (Ctrl+X, Y, Enter)

Set correct permissions:

chmod 600 ~/.ssh/authorized_keys
exit  # Back to root

Step 6: Test SSH Key Login

From your local machine (open a NEW terminal, keep root session open!):

ssh deployuser@123.456.789.012

You should connect without entering a password!

⚠️ Keep your root session open until SSH key login is confirmed!


3. Harden SSH Security

Step 1: Edit SSH Configuration

On the VPS (as root or sudo):

sudo nano /etc/ssh/sshd_config

Step 2: Modify These Settings

Find and change these lines:

# Change SSH port from 22 to something non-standard (e.g., 2222, 3333, 509)
Port 2222

# Disable root login
PermitRootLogin no

# Disable password authentication (force SSH keys only)
PasswordAuthentication no

# Ensure public key authentication is enabled
PubkeyAuthentication yes

# Disable empty passwords
PermitEmptyPasswords no

# Disable challenge-response authentication
KbdInteractiveAuthentication no

Save and exit


Step 3: Test SSH Configuration

sudo sshd -t

Should output: no errors


Step 4: Restart SSH Service

sudo systemctl daemon-reload
sudo systemctl restart ssh
sudo systemctl restart ssh.socket

Step 5: Verify SSH is Listening on New Port

sudo ss -tlnp | grep sshd

Should show your new port (e.g., :2222) instead of :22


Step 6: Test New SSH Connection

From your local machine (keep current session open!):

ssh deployuser@123.456.789.012 -p 2222

If this works, you're good!


Step 7: Update Local SSH Config (Optional)

On your local machine:

nano ~/.ssh/config

Add:

Host myvps
    HostName 123.456.789.012
    User deployuser
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

Now you can connect with just:

ssh myvps

4. Setup Automatic Security Updates

Step 1: Install Unattended Upgrades

sudo apt update
sudo apt install unattended-upgrades -y

Step 2: Enable Automatic Updates

sudo dpkg-reconfigure --priority=low unattended-upgrades

Select "Yes" when prompted.


Step 3: Configure Update Behavior

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

Uncomment/modify these lines:

// Enable security updates
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
};

// Automatically remove unused dependencies
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";

// Automatically reboot if required (at 3 AM)
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";

Save and exit


Step 4: Configure Update Frequency

sudo nano /etc/apt/apt.conf.d/20auto-upgrades

Add:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";

Save and exit


Step 5: Verify Service is Running

sudo systemctl status unattended-upgrades

5. Configure Firewall (UFW)

Step 1: Allow SSH on Your Custom Port FIRST

⚠️ Critical: Do this BEFORE enabling UFW!

# Allow your custom SSH port (replace 2222 with your port)
sudo ufw allow 2222/tcp

Step 2: Allow HTTP and HTTPS

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Step 3: Enable UFW

sudo ufw enable

Type y when prompted.


Step 4: Verify Firewall Rules

sudo ufw status numbered

Should show:

Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 2222/tcp                   ALLOW IN    Anywhere
[ 2] 80/tcp                     ALLOW IN    Anywhere
[ 3] 443/tcp                    ALLOW IN    Anywhere

6. Install & Configure Nginx

Step 1: Install Nginx

sudo apt update
sudo apt install nginx -y

Step 2: Verify Nginx is Running

sudo systemctl status nginx

Should show active (running)


Step 3: Test Nginx

Visit in your browser: http://123.456.789.012

You should see the "Welcome to nginx" page.


7. Setup DNS Records

Before configuring SSL, you need to point your domain to your VPS.

Step 1: Log into Your DNS Provider

(e.g., Hostinger, Namecheap, Cloudflare, GoDaddy)


Step 2: Add A Records

Add these DNS records:

TypeName/HostValue/Points toTTL
A@123.456.789.0123600
Awww123.456.789.0123600

Explanation:


Step 3: Wait for DNS Propagation

DNS changes can take 5 minutes to 48 hours (usually 15-30 minutes).

Test DNS resolution:

nslookup yourdomain.com
dig yourdomain.com +short

Should return: 123.456.789.012


8. Install SSL Certificates (Certbot)

Step 1: Install Certbot

sudo apt update
sudo apt install certbot python3-certbot-nginx -y

Step 2: Obtain SSL Certificate

⚠️ Make sure DNS is propagating first!

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

You'll be asked:

  1. Email address: Enter your email (for renewal notifications)
  2. Terms of Service: Type Y
  3. Share email with EFF: Type N (optional)
  4. Redirect HTTP to HTTPS: Type 2 (recommended)

Step 3: Verify SSL is Working

Visit: https://yourdomain.com

Should show 🔒 lock icon in browser.


Step 4: Test Auto-Renewal

sudo certbot renew --dry-run

Should output: Congratulations, all simulated renewals succeeded


Step 5: Verify Certbot Timer

sudo systemctl status certbot.timer

Should show active (running) - this auto-renews certificates.


9. Install Node.js, NVM, and PM2

Step 1: Install NVM (Node Version Manager)

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash

Step 2: Reload Shell

source ~/.bashrc

Step 3: Install Node.js LTS

nvm install --lts
nvm use --lts
node --version
npm --version

Step 4: Install PM2 Globally

npm install -g pm2

Step 5: Setup PM2 Startup Script

pm2 startup systemd

Copy and run the command it outputs (starts with sudo env PATH=...)


10. Setup Git SSH Authentication

Step 1: Generate SSH Key for GitHub

ssh-keygen -t ed25519 -C "your-email@example.com"

Press Enter for all prompts (no passphrase needed for automation).


Step 2: Display Public Key

cat ~/.ssh/id_ed25519.pub

Copy the entire output.


Step 3: Add SSH Key to GitHub

  1. Go to GitHub.com → Profile → Settings
  2. Click SSH and GPG keys
  3. Click New SSH key
  4. Title: VPS - yourdomain.com
  5. Key: Paste your public key
  6. Click Add SSH key

Step 4: Test GitHub Connection

ssh -T git@github.com

Type yes when prompted.

Expected output:

Hi yourusername! You've successfully authenticated...

Step 5: Configure Git

git config --global user.name "Your Name"
git config --global user.email "your-email@example.com"

11. Install & Configure PostgreSQL

Step 1: Install PostgreSQL

sudo apt update
sudo apt install postgresql postgresql-contrib -y

Step 2: Verify PostgreSQL is Running

sudo systemctl status postgresql

Should show active (running)


Step 3: Create Database User

sudo -u postgres psql

In PostgreSQL shell, run:

-- Create user (replace 'dbuser' and 'strong_password')
CREATE USER dbuser WITH PASSWORD 'your_strong_password_here';

-- Grant permission to create databases (for migrations)
ALTER USER dbuser CREATEDB;

-- Create database (replace 'appdb')
CREATE DATABASE appdb OWNER dbuser;

-- Grant all privileges
GRANT ALL PRIVILEGES ON DATABASE appdb TO dbuser;

-- Exit PostgreSQL
\q

Step 4: Test Database Connection

psql -U dbuser -d appdb -h localhost

Enter password when prompted.

If successful, you'll see:

appdb=>

Type \q to exit.


12. Deploy Your Application

Step 1: Create Application Directory

# You can use either ~/apps or /var/www
sudo mkdir -p /var/www/yourdomain
sudo chown -R deployuser:deployuser /var/www/yourdomain

Step 2: Clone Your Repository

cd /var/www
git clone git@github.com:yourusername/yourrepo.git yourdomain
cd yourdomain

Step 3: Copy .env File from Local Machine

On your local machine:

scp -P 2222 .env deployuser@123.456.789.012:/var/www/yourdomain/

Step 4: Update .env with Production Values

On VPS:

nano /var/www/yourdomain/.env

Update with production values:

NODE_ENV=production
PORT=3000
DATABASE_URL="postgresql://dbuser:your_strong_password_here@localhost:5432/appdb?schema=public"

# Add other environment variables your app needs

Save and exit


Step 5: Install Dependencies

npm install

Step 6: Run Database Migrations (if using Prisma)

npx prisma migrate deploy
# Or generate Prisma client:
npx prisma generate

Step 7: Build Your Application (if needed)

# For Next.js:
npm run build

# For other frameworks, use their build command

Step 8: Create PM2 Ecosystem File

nano ecosystem.config.js

Add:

module.exports = {
  apps: [
    {
      name: "yourapp",
      script: "./server.js", // or './index.js' - adjust to your entry file
      instances: 1,
      exec_mode: "fork",
      autorestart: true,
      watch: false,
      max_memory_restart: "1G",
      env: {
        NODE_ENV: "production",
        PORT: 3000,
      },
      error_file: "./logs/err.log",
      out_file: "./logs/out.log",
      log_file: "./logs/combined.log",
      time: true,
    },
  ],
};

Adjust script to match your actual entry file!

Save and exit


Step 9: Create Logs Directory

mkdir -p logs

Step 10: Start Application with PM2

pm2 start ecosystem.config.js
pm2 save

Step 11: Verify Application is Running

pm2 status
pm2 logs yourapp --lines 50

Step 12: Test Application Locally

curl http://localhost:3000

Should return your app's HTML.


13. Configure Nginx Reverse Proxy

Step 1: Create Nginx Configuration

sudo nano /etc/nginx/sites-available/yourdomain

Add:

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

# HTTPS server with reverse proxy
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name yourdomain.com www.yourdomain.com;

    # SSL certificates (managed by Certbot)
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Reverse proxy to Node.js app on port 3000
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
        proxy_cache_bypass $http_upgrade;
    }

    # 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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json;
}

Save and exit


Step 2: Enable the Site

sudo ln -s /etc/nginx/sites-available/yourdomain /etc/nginx/sites-enabled/

# Remove default nginx page (optional)
sudo rm /etc/nginx/sites-enabled/default

Step 3: Test Nginx Configuration

sudo nginx -t

Should output: syntax is ok and test is successful


Step 4: Reload Nginx

sudo systemctl reload nginx

Step 5: Test Your Website

Visit: https://yourdomain.com

You should see your application!


14. Setup Automated Database Backups

Step 1: Create Backup Directory

sudo mkdir -p /var/backups/postgresql
sudo chown deployuser:deployuser /var/backups/postgresql

Step 2: Create Backup Script

nano ~/backup-db.sh

Add:

#!/bin/bash

# Configuration (replace with your values)
DB_NAME="appdb"
DB_USER="dbuser"
BACKUP_DIR="/var/backups/postgresql"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz"
RETENTION_DAYS=7

# Create backup
pg_dump -U $DB_USER -d $DB_NAME | gzip > $BACKUP_FILE

# Check if backup was successful
if [ $? -eq 0 ]; then
    echo "$(date): Backup successful - $BACKUP_FILE" >> /var/log/db-backup.log
else
    echo "$(date): Backup FAILED!" >> /var/log/db-backup.log
    exit 1
fi

# Delete backups older than RETENTION_DAYS
find $BACKUP_DIR -name "${DB_NAME}_*.sql.gz" -type f -mtime +$RETENTION_DAYS -delete

echo "$(date): Old backups cleaned (older than $RETENTION_DAYS days)" >> /var/log/db-backup.log

Save and exit


Step 3: Make Script Executable

chmod +x ~/backup-db.sh

Step 4: Create PostgreSQL Password File

nano ~/.pgpass

Add (replace with your actual credentials):

localhost:5432:appdb:dbuser:your_database_password

Save and exit

Set permissions:

chmod 600 ~/.pgpass

Step 5: Create Log File

sudo touch /var/log/db-backup.log
sudo chown deployuser:deployuser /var/log/db-backup.log

Step 6: Test Backup Script

~/backup-db.sh

Verify backup was created:

ls -lh /var/backups/postgresql/
cat /var/log/db-backup.log

Step 7: Schedule Daily Backups with Cron

crontab -e

Add this line (runs daily at 2 AM):

0 2 * * * /home/deployuser/backup-db.sh

Save and exit


Step 8: Verify Cron Job

crontab -l

Restore a Backup (if needed)

# List available backups
ls -lh /var/backups/postgresql/

# Restore a specific backup
gunzip < /var/backups/postgresql/appdb_2026-02-01_02-00-00.sql.gz | psql -U dbuser -d appdb

15. Configure Subdomains (Optional)

Want to host multiple apps or an API on subdomains like api.yourdomain.com?

Step 1: Add DNS Record

Add an A record for the subdomain:

TypeName/HostValue/Points toTTL
Aapi123.456.789.0123600

Or use a wildcard to allow all subdomains:

TypeName/HostValue/Points toTTL
A*123.456.789.0123600

Step 2: Create Subdomain Application

cd /var/www
git clone git@github.com:yourusername/api-repo.git api
cd api
npm install

Step 3: Start with PM2 on Different Port

# In ecosystem.config.js, use port 3001 (or any unused port)
pm2 start ecosystem.config.js
pm2 save

Step 4: Get SSL for Subdomain

sudo certbot --nginx -d api.yourdomain.com

Step 5: Create Nginx Config for Subdomain

sudo nano /etc/nginx/sites-available/api

Add:

server {
    listen 80;
    listen [::]:80;
    server_name api.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    server_name api.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
        proxy_cache_bypass $http_upgrade;
    }
}

Step 6: Enable and Test

sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Visit: https://api.yourdomain.com


16. Maintenance & Troubleshooting

Common Commands

Check PM2 apps:

pm2 status
pm2 logs yourapp
pm2 restart yourapp
pm2 stop yourapp

Check Nginx:

sudo nginx -t
sudo systemctl status nginx
sudo systemctl reload nginx
sudo tail -f /var/log/nginx/error.log

Check PostgreSQL:

sudo systemctl status postgresql
psql -U dbuser -d appdb

Check Firewall:

sudo ufw status numbered

View System Logs:

sudo journalctl -u nginx -n 50
sudo journalctl -u postgresql -n 50

Deployment Workflow

Create a simple deployment script:

nano ~/deploy.sh

Add:

#!/bin/bash
cd /var/www/yourdomain
git pull
npm install
npm run build  # if needed
pm2 restart yourapp
echo "Deployment completed at $(date)"

Make executable:

chmod +x ~/deploy.sh

Deploy updates:

~/deploy.sh

Security Checklist

  • ✅ SSH key authentication only (no passwords)
  • ✅ Custom SSH port (not 22)
  • ✅ Root login disabled
  • ✅ Firewall enabled (UFW)
  • ✅ Automatic security updates enabled
  • ✅ SSL certificates installed
  • ✅ Database backups automated
  • ✅ Strong database password
  • ✅ .env file secured (not in git)

Troubleshooting

App not starting:

pm2 logs yourapp --lines 100

502 Bad Gateway:

  • Check if app is running: pm2 status
  • Check app logs: pm2 logs
  • Verify port in nginx matches app port

Database connection errors:

  • Check DATABASE_URL in .env
  • Test connection: psql -U dbuser -d appdb
  • Check PostgreSQL is running: sudo systemctl status postgresql

SSL certificate errors:

  • Renew manually: sudo certbot renew
  • Check expiration: sudo certbot certificates

Summary

You now have a production-ready VPS with:

✅ Secure SSH access with key authentication
✅ Custom SSH port and disabled root login
✅ Automatic security updates
✅ UFW firewall protecting your server
✅ Nginx reverse proxy with SSL
✅ Node.js application running with PM2
✅ PostgreSQL database with backups
✅ Automated daily database backups
✅ Support for multiple domains/subdomains


🎉 Congratulations! Your application is now live and secure!


Guide created: February 2026
Stack: Ubuntu 24.04, Nginx, Node.js, PostgreSQL, PM2, Certbot