Deploy Node.js on Linux with Nginx and PM2 — a practical beginner’s guide

Published: (December 20, 2025 at 08:53 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Hook: why this stack matters

If you’ve built a Node.js app and want it to run reliably in production, you need more than node app.js and crossed fingers. Using Linux + Nginx + PM2 is a pragmatic stack that gives you:

  • stability (Linux)
  • performance and SSL termination (Nginx)
  • automatic process management with monitoring (PM2)

This article walks you through the rationale and a concise, practical path to deploy a real app quickly.

Context: the problem most developers face

New apps fail in production for simple reasons: processes crash, ports are exposed, SSL is missing, or no one set up restarts for server reboots. You need a predictable deployment pattern that:

  • Keeps the app running after crashes and reboots
  • Hides internal ports from the public internet
  • Handles HTTPS, static assets, and load spikes

Nginx + PM2 on a Linux VPS solves all of these without heavy orchestration.

Solution overview

The high‑level flow looks like this:

  1. Nginx listens on ports 80/443 and proxies to your Node process on an internal port (e.g., 3000).
  2. PM2 runs and monitors the Node process, restarts on failure, and can resurrect processes after reboot.
  3. A PM2 ecosystem file versions your process configuration.

These are standard, well‑supported tools and work on common distros such as Ubuntu and CentOS.

Quick checklist before you start

  • Provision a Linux server (1–2 GB RAM is fine for small apps).
  • SSH into the server and update packages.
  • Install Node.js (NodeSource or nvm), Nginx, and PM2.
  • Ensure DNS points your domain to the server before issuing SSL.
  • Keep your code and ecosystem config in Git.

Implementation: essential steps (short and actionable)

  1. Install Node.js (NodeSource or nvm) and verify versions:

    node -v
    npm -v
  2. Upload or clone your app to a dedicated folder (e.g., /home/youruser/my-app) and install production dependencies:

    cd /home/youruser/my-app
    npm install --production
  3. Install PM2 globally and start your app with a friendly name:

    npm install -g pm2
    pm2 start app.js --name my-app
    pm2 startup               # generates init script
    pm2 save                  # saves the process list
  4. Install Nginx and create a server block that proxies to the Node port (e.g., 3000). Example /etc/nginx/sites-available/my-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;
        }
    
        # Optional: serve static assets directly
        # location /static/ {
        #     alias /home/youruser/my-app/public/;
        # }
    }

    Test and reload:

    sudo nginx -t
    sudo systemctl reload nginx
  5. Secure the site with Let’s Encrypt:

    sudo apt-get install certbot python3-certbot-nginx   # Ubuntu example
    sudo certbot --nginx -d example.com -d www.example.com

    The certbot plugin will obtain certificates and configure HTTPS automatically.

Note: Do not expose your Node port to the public internet. Let Nginx handle incoming traffic and SSL.

PM2: make it repeatable

Create an ecosystem.config.js (or .json) to describe your app(s) and environment variables:

module.exports = {
  apps: [
    {
      name: "my-app",
      script: "./app.js",
      instances: "max",          // enable clustering
      exec_mode: "cluster",
      env: {
        NODE_ENV: "development",
        PORT: 3000
      },
      env_production: {
        NODE_ENV: "production",
        PORT: 3000
      }
    }
  ]
};

Start everything with:

pm2 start ecosystem.config.js --env production
pm2 save

Best practice: avoid watch: true in production; trigger restarts via CI/CD instead.

Nginx: reverse proxy basics and tips

  • Proxy target: http://127.0.0.1:3000 (adjust to your app’s port).

  • Common headers: X-Forwarded-For, Host, X-Forwarded-Proto.

  • Test config with nginx -t before reloading.

  • Enable gzip and appropriate cache headers for static assets.

  • Add basic security headers:

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";
  • For multiple apps, create separate server blocks per domain, each pointing to its own internal port.

Security, monitoring and validation

  • Firewall: enable UFW (Ubuntu) or firewalld (CentOS) and allow only SSH and Nginx Full (ports 22, 80, 443).

    sudo ufw allow OpenSSH
    sudo ufw allow 'Nginx Full'
    sudo ufw enable
  • HTTPS: use Certbot with the Nginx plugin for automated issuance and renewal.

  • Logs & monitoring:

    pm2 logs
    pm2 monit

    Install log rotation to prevent uncontrolled growth:

    pm2 install pm2-logrotate
  • Load testing: run a quick test with ab or similar tools and watch pm2 monit, top/htop, and Nginx logs.

Quick best practices

  • Run Node under a non‑root user.
  • Never commit secrets; use environment variables or PM2 env blocks.
  • Regularly run npm audit and keep dependencies updated.

Where to learn more and get help

For a ready reference and expanded explanations, see the full guide:

Company and additional resources:

Conclusion

Deploying Node.js on Linux with Nginx and PM2 gives you a reliable, maintainable base without the complexity of Docker or full orchestration. Start small:

  1. Get Node + PM2 running.
  2. Put Nginx in front.
  3. Add HTTPS.
  4. Automate with an ecosystem file and CI later.

That pattern covers most production needs for indie projects, MVPs, and early‑stage SaaS.

Back to Blog

Related posts

Read more »