Deploying a Node.js app on an Ubuntu VPS is still one of the most practical ways to keep costs predictable while retaining full control over runtime versions, reverse proxy behavior, SSL, and process management. This guide gives you a reusable deployment checklist built around a simple, durable stack: Ubuntu, Nginx, PM2, and Let's Encrypt. It is written for developers who want a clean baseline they can revisit whenever Ubuntu releases, Node LTS versions, or server hardening practices change.
Overview
If your goal is to deploy Node.js app on Ubuntu VPS infrastructure without adding unnecessary platform complexity, this stack remains a strong default. Ubuntu gives you a familiar Linux base, Nginx handles public HTTP traffic and reverse proxy duties, PM2 keeps your app running, and SSL closes the gap between a development setup and a production-ready service.
The value of this approach is not that it is the only way to host Node app on VPS servers. It is that it is understandable, portable, and easy to maintain for small teams. You can move the same pattern across many forms of Node.js VPS hosting, whether you choose a low-cost virtual server for a side project or a larger instance for a SaaS MVP. If you are still deciding where to run it, our Best VPS Hosting for Developers guide and DigitalOcean vs Linode vs Vultr vs Hetzner comparison can help narrow down the infrastructure side first.
At a high level, the deployment flow looks like this:
- Create and harden an Ubuntu VPS.
- Point your domain or subdomain to the server with DNS.
- Install the current Node LTS release and your application dependencies.
- Run the app behind PM2 on an internal port.
- Configure Nginx as the public reverse proxy.
- Issue an SSL certificate and force HTTPS.
- Validate logs, restarts, firewall rules, and persistence after reboot.
For this tutorial, assume your app listens on a local port such as 3000, your domain already exists, and you have SSH access to a fresh Ubuntu VPS. Also assume you are deploying a traditional Node app rather than a containerized workload. If you prefer containers, see Best Hosting for Docker Projects for the tradeoffs before you choose a Docker-first workflow.
Checklist by scenario
Use the scenario below that matches your project. The core steps are similar, but the details you prioritize will differ depending on whether this is a quick internal app, a public production service, or a deployment you expect to scale later.
Scenario 1: Fast baseline deployment for a personal or internal app
This is the shortest useful path when you want a stable Ubuntu Node deployment without overengineering it.
- Provision the VPS and update packages.
Log in over SSH, update the package index, and install security updates. This gives you a cleaner starting point before you add Node, Nginx, or PM2. - Create a non-root deploy user.
Use sudo for administrative work and avoid running your application as root. Add your SSH key to this user and disable password-based access if your workflow allows it. - Enable a basic firewall.
Allow SSH, HTTP, and HTTPS. Keep anything else closed unless you have a specific reason to expose it. - Install Node LTS.
Prefer a maintained Node LTS version rather than whichever package happens to be bundled in the default Ubuntu repositories. Your exact method may vary, but consistency matters more than the installation path. - Transfer or clone your app.
Pull from Git, sync files from CI, or upload a release artifact. Keep secrets out of the repository and place environment variables in a local configuration file or service environment. - Install dependencies and build if needed.
Run your package manager install command, then build assets for frameworks that require it. - Test the app directly first.
Start it on a local port and confirm it runs before introducing PM2 or Nginx. This isolates application issues from server configuration issues. - Install PM2 and run the app under a named process.
Use a clear process name. Save the PM2 process list and configure startup so the app returns automatically after reboot. - Install and configure Nginx.
Create a server block that listens on port 80 and proxies requests to your local Node port. - Point DNS to the server.
Create an A record for the domain or subdomain you want to use. Wait for propagation before requesting SSL if the record is new. - Issue SSL with Let's Encrypt.
Once Nginx is serving the site over HTTP and DNS resolves correctly, request a certificate and enable HTTPS redirection. - Reboot and validate.
Confirm the app, PM2, and Nginx all return properly after a restart.
This baseline works well for utility apps, prototypes, internal dashboards, and lightweight APIs.
Scenario 2: Public production deployment for a customer-facing service
If the app will be exposed to real users, add a few controls beyond the baseline.
- Separate environments clearly.
Do not use the same VPS for both testing and production if avoidable. Even a small project benefits from a distinct production server and a safer promotion path. - Set explicit environment variables.
DefineNODE_ENV=productionand review any runtime secrets, API keys, session secrets, and database credentials. Store them outside source control. - Configure Nginx carefully.
Forward the correct headers, including host and client IP-related headers where your app expects them. If your framework uses secure cookies or trust proxy settings, test those with HTTPS enabled. - Add health checks.
A simple application route such as/healthmakes debugging and uptime monitoring easier. It also helps later if you move to a load-balanced setup. - Review log locations.
Know where your Nginx access logs, Nginx error logs, PM2 logs, and application logs live. During deployment problems, this often matters more than anything else. - Back up both code and configuration.
At minimum, preserve your Nginx configuration, PM2 startup setup, environment files, and deployment steps. A server rebuild is much easier when those are documented. - Harden SSH and package maintenance.
Use key-based access, remove unused users, and have a plan for security updates. Even a simple monthly maintenance window is better than none. - Test renewals and persistence.
Make sure SSL renewals are scheduled and that your app survives reboots and package upgrades.
This is the scenario most readers mean when they search for an Nginx PM2 SSL setup. The core stack stays small, but the operating discipline gets tighter.
Scenario 3: Small-team deployment you may scale later
For a service that may outgrow a single VPS, keep the first deployment simple while avoiding choices that make migration painful.
- Use a predictable directory structure.
Store the app in a path that makes future automation easier. Keep release, shared config, and logs organized. - Keep application state out of the filesystem where possible.
User uploads, session storage, and generated artifacts are common migration pain points. Move them to object storage, a database, or another external service if growth is likely. - Externalize the database early.
You can keep the Node app on one VPS and use a managed database or separate database server later. That is often easier than untangling both app and database from one box at once. - Document the exact Node version and deployment steps.
If another developer or future you needs to rebuild the server, version drift becomes a common source of avoidable outages. - Prepare for horizontal scaling, even if you do not need it yet.
Use PM2 sensibly, but do not confuse process clustering on one machine with true scaling. A clean single-node deployment is often the better foundation.
If your roadmap includes other self-hosted services, it also helps to compare operational patterns across stacks. For example, our guides on self-hosting n8n, hosting Plausible Analytics, and hosting Nextcloud show how storage, backups, and service-specific requirements change the checklist even when the VPS basics stay similar.
What to double-check
Most deployment issues come from a short list of mismatches between application settings, proxy configuration, and DNS. Before you call the server done, review these items carefully.
- DNS is pointing to the correct IP.
If the domain resolves elsewhere, SSL issuance and public access will fail no matter how good the server configuration is. - The app binds to localhost or an internal port.
In most cases, the Node process should not be exposed directly to the public internet. Let Nginx handle inbound traffic. - Nginx upstream port matches your app port.
A one-line mismatch here can look like a full outage. - Your firewall allows 80 and 443.
It is common to install Nginx correctly and still block public traffic at the firewall layer. - PM2 startup has been saved.
Starting the app manually is not enough. Make sure the process list is saved and startup is enabled for reboots. - Environment variables are loaded in the production context.
An app that works in your shell may fail under PM2 if the runtime environment differs. - SSL renewal is in place.
Certificate issuance is only the first step. Renewal automation matters just as much. - Reverse proxy headers are correct.
Applications that generate redirects, secure cookies, or callback URLs often fail in subtle ways when proxy headers are incomplete. - Static file and upload paths are writable where needed.
This matters for apps that write logs, cache files, or user content. - System time is correct.
Time drift can cause SSL and authentication issues that are surprisingly hard to diagnose.
It is also worth testing from outside your network: open the site over HTTPS, submit a form or API request, restart the server, and inspect both Nginx and PM2 logs. A deployment is not truly finished until those checks pass.
Common mistakes
The same deployment errors show up repeatedly, especially when someone is moving from local development to a first production VPS.
Running the app as root. This is rarely necessary and creates needless risk. Use a normal deploy user and keep privileges narrow.
Skipping the direct app test before adding Nginx. If you never confirm that the Node app works on its own port, you can waste time debugging the wrong layer.
Using an outdated Node release by accident. Ubuntu package defaults, old setup scripts, or copied commands from old blog posts can leave you on an unexpected runtime version.
Forgetting production-specific settings. Apps often behave differently with production mode enabled. Asset handling, error output, cookies, sessions, and caching may all change.
Trusting only the browser test. The homepage may load even when API routes, WebSocket connections, redirects, or file uploads are broken. Check real request paths.
Leaving no notes behind. A successful manual deploy is not the same thing as an operationally healthy deploy. Write down the app path, service names, environment file location, Nginx file name, and restart commands.
Putting too many services on one VPS too early. It is possible to combine your Node app with databases, queues, and analytics tools on one small instance, but it increases failure scope quickly. If you later decide to split responsibilities, the transition is easier when you kept the original stack clean. Readers planning a broader self-hosted setup may also find it useful to compare this with our Ghost CMS VPS guide and Laravel hosting guide, since each application family introduces different operational expectations.
When to revisit
This setup is not something you configure once and forget. The stack is stable, but the details change enough that a recurring review is worthwhile.
Revisit your Node.js VPS hosting setup when any of the following happens:
- A new Node LTS cycle begins.
Check compatibility, package manager behavior, and build tooling before upgrading production. - You move to a new Ubuntu release.
Review package names, repository instructions, firewall assumptions, and Nginx defaults. - Your app adds background jobs, WebSockets, or uploads.
These often require proxy, timeout, storage, or process changes. - You adopt CI/CD or automated deploys.
Manual PM2 and Nginx workflows may need refinement once deployment frequency increases. - You change DNS providers or certificate flow.
Any DNS change is a reason to confirm records, SSL issuance, redirects, and renewal behavior again. - Traffic patterns shift.
Slow responses, memory pressure, or restart frequency are signs to revisit instance sizing, process counts, and log handling. - You are heading into a busy launch period.
Before a release or seasonal planning cycle, confirm backups, update paths, and rollback steps.
A practical maintenance routine looks like this: once per quarter, log in to the server, review package updates, confirm the Node version, inspect PM2 status, test SSL renewal, verify disk usage, and reload the site over HTTPS from an external network. Once per major application update, recheck your Nginx config and environment variables. Once per infrastructure change, repeat the full deployment checklist from DNS through reboot validation.
If you want to keep this stack reliable over time, the most useful next step is simple: save your exact deployment commands, configuration file paths, and verification steps in your project repository or internal docs. That turns a one-off Ubuntu server setup into a repeatable operating procedure.