3 min read

How to Deploy a Node.js App with Nginx and PM2

A practical Node.js deployment guide for Ubuntu using Nginx as a reverse proxy and PM2 for process management, restarts, logs, and startup persistence.

The quickest production setup for a Node.js app in 2026 is simple: run the app on an internal port, put Nginx in front of it, and use PM2 to keep the process alive. That gives you restarts, logs, startup persistence, and SSL without overcomplicating the server.

What this setup looks like

  • Your app runs on 127.0.0.1:3000 or a similar local port
  • Nginx handles public traffic on ports 80 and 443
  • PM2 restarts the app if it crashes and starts it again after a reboot

Step 1: Update the server

sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx curl git

Step 2: Install Node.js

Use a current LTS release unless your project has a strong reason not to.

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

Step 3: Install PM2

sudo npm install -g pm2
pm2 -v

Step 4: Upload or clone the app

cd /var/www
sudo mkdir -p node-app
sudo chown -R $USER:$USER node-app
git clone your-repository-url node-app

Step 5: Install dependencies and build

cd /var/www/node-app
npm install
npm run build

If your app does not have a build step, skip that command.

Step 6: Start the app with PM2

Use the real start command for your framework. These are common examples:

pm2 start npm --name node-app -- start
pm2 save

If your app needs a custom port:

PORT=3000 pm2 start npm --name node-app -- start
pm2 save

Step 7: Enable PM2 on reboot

pm2 startup systemd

PM2 will print a command. Run that command exactly, then save again:

pm2 save

Step 8: Configure Nginx as a reverse proxy

sudo nano /etc/nginx/sites-available/node-app
server {
    listen 80;
    server_name example.com www.example.com;

    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;
    }
}
sudo ln -s /etc/nginx/sites-available/node-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Step 9: Add HTTPS

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

Step 10: Check logs and status

pm2 status
pm2 logs node-app
sudo tail -f /var/log/nginx/error.log

PM2 commands worth remembering

  • pm2 restart node-app
  • pm2 stop node-app
  • pm2 delete node-app
  • pm2 logs node-app
  • pm2 monit

Common mistakes

  • Binding the Node app to a public IP instead of localhost
  • Forgetting pm2 save
  • Skipping the startup command
  • Not forwarding the correct headers in Nginx
  • Using npm run dev in production

When to choose this over Docker

If you want a small, understandable deployment with fewer moving parts, this setup is excellent. It is especially good for a VPS, a small SaaS, an internal tool, or a content platform that needs to ship fast.

Useful next reads

If this is your first server, harden it first with How to Secure a Fresh Ubuntu Server Before Going Live. If you are still deciding on the web server, compare both options in Nginx vs Apache in 2026: What Developers Should Really Choose.

Quick FAQ

Do I need PM2 if I use systemd?

Not always, but PM2 is faster to set up and more convenient for many Node.js apps.

Can Nginx serve a Next.js app this way?

Yes. The reverse proxy pattern is the same. Only the start command may change.

Should I expose port 3000 publicly?

No. Let Nginx handle public traffic and keep the Node.js process behind it.

Linux Mar 28, 2026