+44 7575 472931[email protected]
HostAccentKnowledge BaseHosting, websites, SEO, and growth

Deploy a Node.js App on Linux VPS with PM2 + Nginx (Beginner to Production)

Complete Node.js VPS deployment guide: install Node.js LTS, manage processes with PM2, configure Nginx reverse proxy, set up a firewall, and add SSL — step by step.

Linux HostingVPSCloud Hosting
Deploy a Node.js App on Linux VPS with PM2 + Nginx (Beginner to Production) - Linux Hosting guide cover image

Getting a Node.js application working locally is the easy part. Deploying it to a Linux VPS and keeping it running reliably under real-world conditions — surviving crashes, server reboots, traffic spikes, and SSL requirements — is where most developers hit friction for the first time.

The most common failure patterns are:

  • The app runs manually but stops when you close the SSH session
  • The app runs but is not accessible from the browser because it is not proxied through port 80/443
  • The app restarts after a server reboot and everything breaks until someone SSH-es in and restarts it manually

This guide addresses all of these with a clean, production-ready setup using PM2 for process management and Nginx as a reverse proxy.

What you need before starting

  • A Linux VPS running Ubuntu 22.04 or 24.04 (the commands work on both)
  • Root or sudo access
  • A domain name pointed to your server's IP address (for SSL)
  • Your Node.js application code — either cloned via Git or transferred via SCP/SFTP

Step 1: Install Node.js LTS

Avoid installing Node.js from Ubuntu's default apt repository — it is often several major versions behind. Use the NodeSource repository for a current LTS release:

bash
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs

Verify the installation:

bash
node -v
npm -v

You should see something like v22.x.x and 10.x.x. If the output looks correct, proceed.

Step 2: Transfer and prepare your application

If your code is in a Git repository:

bash
cd /var/www
sudo git clone https://github.com/yourusername/yourapp.git
sudo chown -R $USER:$USER /var/www/yourapp
cd /var/www/yourapp
npm install --production

Using --production skips development dependencies (testing tools, build tools) that should not run on a production server.

If your application requires a build step (Next.js, TypeScript, etc.):

bash
npm run build

Create a .env file if your application requires environment variables:

bash
nano /var/www/yourapp/.env

Add your production environment variables. Never commit .env files to version control.

Step 3: Test the application directly

Before setting up PM2 and Nginx, confirm the application runs correctly:

bash
cd /var/www/yourapp
node server.js   # or: npm start

Open a second SSH session and test:

bash
curl http://localhost:3000

If you get a response, the application itself is working. Stop it with Ctrl+C and proceed to PM2 setup.

Step 4: Install and configure PM2

PM2 is a production process manager for Node.js. It keeps your application running after crashes, restarts it on server reboots, handles log management, and provides a monitoring dashboard.

Install globally:

bash
sudo npm install -g pm2

Start your application with PM2:

bash
cd /var/www/yourapp
pm2 start npm --name "myapp" -- start

Or if you want to start a specific file:

bash
pm2 start server.js --name "myapp"

Save the current process list so PM2 knows what to restart after a reboot:

bash
pm2 save

Generate and install the startup script that runs PM2 on boot:

bash
pm2 startup

This command outputs a sudo env PATH=... command — copy and run that command exactly as shown in your terminal. It configures systemd to start PM2 automatically.

Useful PM2 commands:

bash
pm2 status              # Show all running processes and their status
pm2 logs myapp          # View application logs in real time
pm2 logs myapp --lines 100  # View last 100 lines of logs
pm2 restart myapp       # Restart the application
pm2 stop myapp          # Stop the application
pm2 delete myapp        # Remove from PM2 process list
pm2 monit               # Interactive CPU and memory monitor

After running pm2 status, confirm your application shows as online. If it shows errored, check logs with pm2 logs myapp to diagnose the problem.

Step 5: Configure Nginx as a reverse proxy

Your Node.js application runs on a local port (typically 3000). Nginx sits in front of it, accepting traffic on ports 80 and 443, then forwarding requests to your app.

Create the Nginx configuration file:

bash
sudo nano /etc/nginx/sites-available/myapp

Paste:

nginx
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";

    location / {
        proxy_pass http://127.0.0.1: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;
        proxy_read_timeout 90;
    }

    # Serve static files directly from Nginx (faster than proxying through Node)
    location /static/ {
        root /var/www/yourapp/public;
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }
}

Replace yourdomain.com with your actual domain and adjust the port (3000) and static files path as needed.

Enable the site and test:

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

The nginx -t step is critical — never reload without testing first.

Step 6: Configure the firewall

bash
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Nginx Full opens both port 80 (HTTP) and port 443 (HTTPS). OpenSSH ensures you do not lock yourself out of SSH access.

Do not open port 3000 (or whatever port your Node app runs on) publicly — it should only be accessible internally via Nginx. Verify:

bash
sudo ufw status

Confirm port 3000 is not listed as allowed.

Step 7: Add SSL with Let's Encrypt

Once your domain DNS is pointing to your server's IP and propagation is complete (verify with dig yourdomain.com or a DNS checker tool):

bash
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot will modify your Nginx configuration automatically to add SSL and set up HTTP-to-HTTPS redirects. It also configures auto-renewal via a systemd timer.

Verify auto-renewal works:

bash
sudo certbot renew --dry-run

If this passes without errors, your SSL will renew automatically before expiry.

Step 8: Deploy updates without downtime

When your code changes, you need a deployment process that updates the application without extended downtime.

Zero-downtime update flow:

bash
cd /var/www/yourapp
git pull origin main
npm install --production
npm run build          # if applicable
pm2 reload myapp       # graceful reload: starts new workers before killing old ones

pm2 reload (not pm2 restart) performs a graceful reload — it starts the new version of your app in a new worker, waits for it to be ready, then stops the old worker. This results in zero-downtime deployments for most applications.

For applications that cannot do graceful reloads (stateful connections, etc.), pm2 restart is safer:

bash
pm2 restart myapp

Post-deployment checklist

Before considering a deployment complete:

  1. pm2 status shows the application as online
  2. sudo nginx -t passes
  3. curl https://yourdomain.com returns a valid response
  4. HTTPS is active (check for padlock in browser)
  5. SSL auto-renewal is confirmed (certbot renew --dry-run passes)
  6. Application logs show no errors (pm2 logs myapp --lines 50)
  7. Firewall allows only necessary ports (sudo ufw status)

Handling multiple applications on one server

If you need to run multiple Node.js applications on the same VPS, each gets its own:

  • Internal port (3000, 3001, 3002...)
  • PM2 process name
  • Nginx server block with a different server_name

They all share the server's resources. Monitor total RAM and CPU usage with pm2 monit to ensure you are not overprovisioning the server.

Final recommendation

This setup handles the vast majority of Node.js production deployments reliably. PM2 covers process management and recovery. Nginx covers SSL, proxy, and static file serving. UFW covers network exposure.

The main risk to avoid after setup is SSH-ing in to make changes "just quickly" without going through the deployment process. Ad-hoc changes that are not tracked break predictably and are hard to debug. Treat your production server as something you deploy to, not something you edit directly.

Reviewed by

HostAccent Editorial Team · Editorial Team

Last updated

Apr 13, 2026

HostAccent Editorial Team publishes practical hosting guides, operations checklists, and SEO-focused tutorials for businesses building international web presence.

Discussion

Have a question or tip about this topic? Share it below — your comment will appear after review.

Your email stays private and is only used for moderation.

How do I choose the right VPS location for my audience?

Pick the datacenter closest to your primary users, then test latency, page speed, and checkout flow from that region before scaling.

When should I move from shared hosting to VPS?

Move when you need guaranteed resources, root-level control, custom server tuning, or when traffic spikes cause unstable performance.

What baseline security should a new VPS have?

Use strong SSH practices, firewall rules, auto security updates, regular backups, and active monitoring for uptime and suspicious activity.

Start typing to find the right article.

Write for the Community

Have a tutorial, tip, or insight to share? Get published on the HostAccent Blog with your name, bio, and website link.

Become a Contributor