How to Deploy a Node.js App on a VPS With Nginx, PM2, and SSL
nodejsvpsnginxpm2sslubuntudeployment

How to Deploy a Node.js App on a VPS With Nginx, PM2, and SSL

PPlkdt Labs Editorial
2026-06-13
9 min read

A practical checklist for deploying a Node.js app on a VPS with Nginx, PM2, and SSL, including setup steps and common fixes.

Deploying a Node.js app on a VPS is still one of the most flexible ways to run a production service, but the stack only feels simple until something breaks. This guide gives you a reusable checklist for deploying a Node.js app on Ubuntu with Nginx, PM2, and SSL, with enough detail to help you set it up cleanly the first time and enough structure to revisit it before future releases, server moves, package upgrades, or DNS changes.

Overview

This article walks through a durable production pattern: a Node.js app listens on a local port, PM2 keeps the process alive, Nginx handles incoming HTTP and HTTPS traffic as a reverse proxy, and a TLS certificate secures the public domain. It is a practical middle ground between a fully managed platform and a container-heavy setup.

The core flow looks like this:

  • Create and harden an Ubuntu VPS.
  • Point your domain to the server.
  • Install Node.js, Nginx, and PM2.
  • Run your app on a local port such as 3000.
  • Configure Nginx to proxy requests from the public domain to the app.
  • Issue an SSL certificate and redirect HTTP to HTTPS.
  • Make sure the app restarts on reboot and survives deploys.

This stack works well for small production apps, internal tools, APIs, and side projects that need predictable hosting without too much platform abstraction. If you are still preparing a fresh machine, it helps to review a broader baseline server checklist first: Linux Server Setup Checklist for New App Deployments.

Before you start, assume you have the following:

  • An Ubuntu VPS with SSH access.
  • A non-root user with sudo privileges.
  • A domain or subdomain you can point at the server.
  • A Node.js app that starts reliably with a command such as npm start or node server.js.
  • A firewall policy that allows SSH, HTTP, and HTTPS.

If your domain is not yet pointed correctly, review How to Point a Domain to a Server: A Record, CNAME, Nameservers, and TTL Explained. If DNS behavior becomes confusing later, keep How to Use Dig, Nslookup, and Whois to Troubleshoot Domain Problems nearby.

Checklist by scenario

Use the scenario that matches your deployment. The exact commands may vary by Node version and package source, but the underlying checks stay mostly the same.

Scenario 1: First-time deployment on a fresh VPS

  1. Update the server.
    Run your normal system update steps so you are not building on stale packages.
  2. Create or confirm a deploy user.
    Avoid running your app as root. Use a regular user with sudo access for package installation and app management.
  3. Install Node.js.
    Choose a supported Node.js version that matches your app requirements. Confirm with node -v and npm -v.
  4. Install Nginx.
    Start the service and verify that the default web page loads when you visit the server IP over HTTP.
  5. Install PM2 globally.
    This will manage the Node process and help with restarts after crashes or reboots.
  6. Transfer or clone your app.
    Place it in a stable directory such as /var/www/app-name or a home directory owned by your deploy user.
  7. Install dependencies.
    Run your package manager in production-friendly mode. Also verify any environment-specific build steps.
  8. Create environment variables.
    Set values for NODE_ENV, database URLs, API secrets, ports, and any third-party credentials. Keep these out of your Git repo.
  9. Start the app locally first.
    Run it directly on a local port such as 3000 and confirm with curl http://127.0.0.1:3000 that it responds.
  10. Start the app with PM2.
    Use a named PM2 process so logs and restart behavior are easier to manage.
  11. Save the PM2 process list and configure startup.
    This step is often missed. Without it, your app may not come back after a reboot.
  12. Point DNS to the VPS.
    Add an A record for the domain or subdomain and allow time for propagation. If you automate DNS, your future self may prefer an infrastructure-as-code workflow such as the one described in Terraform DNS Records Guide: Manage Cloudflare and Route 53 as Code.
  13. Create an Nginx server block.
    Set server_name to your domain and proxy requests to http://127.0.0.1:3000 or whichever internal port your app uses.
  14. Test the Nginx config.
    Validate before reload. This catches syntax errors before they take down working traffic.
  15. Enable the site and reload Nginx.
    After reloading, confirm that the domain reaches the app over HTTP.
  16. Issue an SSL certificate.
    Use your preferred ACME client workflow to obtain a certificate and set up HTTPS. Then verify automatic certificate renewal.
  17. Force HTTPS.
    Redirect plain HTTP traffic to HTTPS once the certificate is working.
  18. Run a post-deploy smoke test.
    Check the homepage, health route, API endpoint, redirects, static assets, and logs.

Scenario 2: Updating an app already running with PM2 and Nginx

  1. Review environment changes first.
    If the new release needs new variables, ports, build output, or OS packages, update those before restart.
  2. Pull code or upload the new release.
    Keep deploy steps deterministic. If possible, use a simple script rather than typing from memory.
  3. Install dependencies and build.
    Be careful when lockfiles or Node versions changed between releases.
  4. Reload or restart the PM2 process.
    Use reload where appropriate to reduce downtime. Then confirm the process is healthy.
  5. Check PM2 logs.
    Look for startup errors, missing modules, permission issues, or environment variable problems.
  6. Confirm Nginx still points to the correct internal port.
    Many failed deploys are just port mismatches after app changes.
  7. Retest HTTPS and redirects.
    Do not assume TLS stayed healthy after hostname or config changes.

If you want a broader release triage list for failed updates, see Deployment Troubleshooting Checklist: What to Check When a Release Fails.

Scenario 3: Moving from IP-only access to a real domain with SSL

  1. Make sure the app already works through Nginx on the server IP.
  2. Add the correct DNS record.
    For most VPS setups this is an A record pointing to the public IPv4 address.
  3. Wait for the domain to resolve from your location.
    Use dig or nslookup rather than guessing.
  4. Add or update the Nginx server_name.
  5. Reload Nginx and test over HTTP.
  6. Issue the certificate only after DNS resolves correctly.
  7. Enable HTTPS redirect after the certificate succeeds.

If DNS does not resolve as expected, review How to Fix DNS_PROBE_FINISHED_NXDOMAIN for Websites, APIs, and Local Development. If HTTPS fails after a domain change, How to Fix ERR_SSL_PROTOCOL_ERROR After DNS or Hosting Changes is a useful companion.

Scenario 4: Preparing for repeatable deploys

Once the manual flow works, document and automate the parts you repeat:

  • Store runtime config in a consistent location.
  • Use a deploy script for pulling code, installing dependencies, building, and reloading PM2.
  • Add health checks after reload.
  • Use GitHub Actions or another CI tool if you want a push-to-deploy workflow.
  • Track DNS changes as code where possible.

For teams comparing patterns, you may also want to decide whether a VPS is still the right fit or whether containers would simplify handoff. If that becomes relevant, see Docker Deployment Tutorial for Small Production Apps.

What to double-check

This is the part to revisit before you call a deploy complete. Most production issues in this stack come from a short list of mismatches rather than exotic bugs.

  • App bind address and port: Your Node app should usually listen on a local interface or on the port expected by Nginx. If Nginx proxies to 127.0.0.1:3000, confirm the app is really there.
  • Nginx proxy headers: Forward common headers so the app can detect the original host, protocol, and client IP when needed.
  • server_name values: The Nginx hostname must match the real domain users visit. A typo here can route requests to the wrong site or the default server block.
  • Firewall rules: Confirm ports 80 and 443 are open publicly, while your app port stays internal unless you intentionally expose it.
  • PM2 startup persistence: Saving the current process list is not enough unless startup is configured correctly for reboots.
  • File ownership and permissions: The deploy user, app directory, build output, and any writable directories should line up cleanly. Permissions problems often appear only after restarts.
  • Environment variables: Compare what your app expects with what the server actually provides. Missing secrets are a common cause of loops or silent failures.
  • Certificate renewal: Do not stop at the first successful certificate issuance. Make sure your renewal method exists and can run without manual intervention.
  • DNS TTL expectations: Changes may not appear instantly. If you are rotating servers or records, plan for propagation delays and stale caches.
  • Logs in both layers: Check PM2 logs and Nginx access and error logs. One without the other can hide the real failure point.

If you need a deeper reverse proxy reference, Nginx Reverse Proxy Setup Guide for Node.js, Docker, and SSL expands on the Nginx side of the setup.

Common mistakes

The fastest way to make this deployment stable is to avoid a handful of recurring mistakes.

  • Running the app as root.
    It may feel convenient early on, but it creates unnecessary risk and makes permissions harder to reason about later.
  • Skipping the local app test before Nginx.
    If the app does not respond on 127.0.0.1:3000, Nginx cannot fix it. Test the app in isolation first.
  • Assuming DNS is done the moment a record is saved.
    Always verify actual resolution from the command line. If you are evaluating providers for future projects, Best DNS Providers for Developers: Cloudflare vs Route 53 vs Namecheap vs Others can help frame tradeoffs.
  • Forgetting to open ports 80 and 443.
    Everything may look correct on the box while traffic is blocked one layer up.
  • Pointing Nginx to the wrong port.
    This is especially common after changing frameworks, start commands, or environment settings.
  • Not persisting PM2 across reboots.
    A server restart should not become a service outage.
  • Editing Nginx without testing the config first.
    One syntax error can break multiple sites on the same machine.
  • Mixing deploy artifacts with source inconsistently.
    If your build writes to a different directory than expected, your process manager may launch old code.
  • Leaving secrets in repo files.
    Use environment variables or a secure secret store appropriate to your environment.
  • Stopping after the site loads once.
    A real post-deploy check should include restart behavior, log review, TLS, redirects, and at least one dynamic route or API request.

A stable deployment is less about finding a perfect stack and more about making each layer observable and predictable.

When to revisit

This setup is not something you configure once and forget. Revisit your Node.js VPS deployment whenever any of the underlying assumptions change.

Recheck the full stack before these moments:

  • Before a major Node.js version upgrade.
  • Before moving the app to a new VPS or changing IP addresses.
  • Before changing domains, subdomains, or DNS providers.
  • When switching package managers or build tooling.
  • When adding SSL for a new hostname.
  • When changing the app port, process name, or startup command.
  • When introducing CI/CD or automated deploy scripts.
  • Before seasonal traffic spikes, launches, or internal planning cycles.

Use this quick refresh checklist each time:

  1. Confirm the server still has the expected Node.js version.
  2. Confirm environment variables are present and current.
  3. Verify PM2 startup behavior and process names.
  4. Review Nginx server blocks for correct domains and ports.
  5. Check DNS records and expected TTL behavior.
  6. Confirm certificate validity and renewal path.
  7. Run a smoke test covering HTTP, HTTPS, redirects, and one application endpoint.
  8. Review logs after the first live requests.

If you document those eight checks in your project repo or runbook, future deploys become much less fragile. That is the real value of this stack: not just that it works, but that it can be understood, debugged, and refreshed without starting from scratch.

For most small Node production deployments, Ubuntu plus Nginx, PM2, and SSL remains a practical pattern. Keep the layers simple, keep your checks explicit, and revisit the setup whenever infrastructure, DNS, TLS, or release workflow changes. That discipline prevents many of the outages that get blamed on the tools.

Related Topics

#nodejs#vps#nginx#pm2#ssl#ubuntu#deployment
P

Plkdt Labs Editorial

Senior SEO Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

2026-06-15T09:57:27.374Z