VPS Deployment Made Easy: Your Complete Guide to Node.js/Next.js Setup
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
- Prerequisites
- Initial VPS Setup & SSH Access
- Create Sudo User & SSH Key Authentication
- Harden SSH Security
- Setup Automatic Security Updates
- Configure Firewall (UFW)
- Install & Configure Nginx
- Setup DNS Records
- Install SSL Certificates (Certbot)
- Install Node.js, NVM, and PM2
- Setup Git SSH Authentication
- Install & Configure PostgreSQL
- Deploy Your Application
- Configure Nginx Reverse Proxy
- Setup Automated Database Backups
- Configure Subdomains (Optional)
- 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:
| Type | Name/Host | Value/Points to | TTL |
|---|---|---|---|
| A | @ | 123.456.789.012 | 3600 |
| A | www | 123.456.789.012 | 3600 |
Explanation:
@= root domain (yourdomain.com)www= www subdomain (www.yourdomain.com)
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:
- Email address: Enter your email (for renewal notifications)
- Terms of Service: Type
Y - Share email with EFF: Type
N(optional) - 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
- Go to GitHub.com → Profile → Settings
- Click SSH and GPG keys
- Click New SSH key
- Title:
VPS - yourdomain.com - Key: Paste your public key
- 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:
| Type | Name/Host | Value/Points to | TTL |
|---|---|---|---|
| A | api | 123.456.789.012 | 3600 |
Or use a wildcard to allow all subdomains:
| Type | Name/Host | Value/Points to | TTL |
|---|---|---|---|
| A | * | 123.456.789.012 | 3600 |
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