Why Deploy Node.js on a VPS Rather Than on a Managed Platform?
Serverless and PaaS platforms are convenient at first, but their cost rises with traffic and they impose constraints (timeouts, cold starts, limits on WebSocket connections). A VPS gives you a persistent Node.js process, long-lived connections (WebSocket, SSE) without artificial limits, dedicated resources and a fixed, predictable cost. You choose the Node version, the process manager, the reverse proxy and the update strategy, without depending on a provider's conventions.
Node.js's asynchronous, non-blocking model lets a single process hold a large number of simultaneous connections. That is what makes it a good fit for REST APIs, real-time features and microservices, as long as the process stays alive. This is exactly what most shared hosting refuses, by forbidding persistent processes or WebSockets. On a VPS, your npm dependencies, environment variables and PM2 configuration are not shared with any other account.
What You Gain with a VPS for Node.js
- A persistent Node.js process, ideal for WebSocket, SSE and long-running tasks with no cold start
- Automatic restart after a crash and when the server reboots, thanks to PM2
- Cluster mode to use every CPU core of the VPS without changing your code
- An Nginx reverse proxy in front of Node for HTTPS, compression and static files
- Queue workers (BullMQ, Agenda) and scheduled jobs running next to the API on the same machine
- Several applications on one VPS, each on its own internal port (3000, 3001…) and subdomain behind Nginx
- Server-side rendering for frameworks such as Nuxt or Next.js, served by the same Node.js and PM2 foundation
- A fixed, predictable cost, with no per-request billing and no scaling surprises
- Full control over the Node.js version and system dependencies
Prerequisites
A standard Node.js application (Express, Nest or Fastify API) runs comfortably on a VPS with 1 to 2 vCPUs and 1 to 2 GB of RAM; plan for more RAM if you compile a heavy front end at build time or run workers. Aim for 2 vCPUs and 2 GB as soon as the API shares the machine with SSR rendering or a PostgreSQL database.
Pick an LTS line that is still maintained: Node.js 20 has received no fixes since 30 April 2026, Node.js 24 moved to maintenance on 20 October 2026, and Node.js 26 has been the active LTS since 28 October 2026. Install it through nvm or the NodeSource repository to pin it, along with PM2 as the process manager and Nginx as the reverse proxy. You need a domain pointing to the VPS IP and ports 80/443 open.
Starting from the Node.js Stack Template
The Node.js Stack template in the ServOrbit Marketplace prepares an Ubuntu 24.04 VPS with Node.js on the current LTS line of the NodeSource repository, npm, PM2 installed globally, Nginx and PostgreSQL. It saves you the first step below; the rest of the guide applies as is.
It is not an application with a login screen but a runtime environment: you get in over SSH (ssh root@<vps-ip>) to upload your code. On first access, Nginx serves its default welcome page on port 80, because no virtual host has been written for your domain yet, and Certbot is not installed: the Nginx and HTTPS steps are still yours to do. PostgreSQL listens only locally and authenticates the postgres system user with the peer method: sudo -u postgres psql opens the console without a password.
Deploy a Node.js Application to Production
Prepare the VPS and Install Node.js
On an Ubuntu 24.04 VPS, update the system and install an LTS version of Node through nvm (
nvm install --lts) or the NodeSource repository. Check withnode -vandnpm -v. Create a non-root user dedicated to the application instead of running everything as root: that user will own the code, the.envfile and the PM2 processes. With the Node.js Stack template, Node.js, npm and PM2 are already installed, so only the user creation remains.Fetch the Code and Build
Clone your repository on the VPS, install the production dependencies (
npm ci --omit=dev) and run the build if needed (npm run build).Put sensitive variables in an uncommitted
.envfile that only the application user can read (chmod 600 .env). No library is needed to read it: since Node.js 20.6, the--env-fileoption loads that file intoprocess.envat startup.Prepare the PostgreSQL Database
If the application needs a database, create a dedicated role and database instead of working with the superuser:
sudo -u postgres createuser --pwprompt monapp sudo -u postgres createdb --owner=monapp monappThen set
DATABASE_URL=postgres://monapp:<password>@127.0.0.1:5432/monappin the.env. Since PostgreSQL listens only locally, no database port has to be opened in the firewall.Start the Application with PM2
Install PM2 globally (
npm install -g pm2, already done on the template), then start the app from the project folder, asking Node to load the.env:pm2 start app.js --name monapp -i max --node-args="--env-file=.env"The
-i maxflag enables cluster mode across all cores. PM2 automatically restarts the process if it crashes.Enable Automatic Startup at Boot
Generate the systemd script with
pm2 startup, run as the application user, execute the command it prints with sudo, then freeze the current state withpm2 save. On the next VPS reboot, your application will be restarted automatically, with no intervention.Configure Nginx as a Reverse Proxy
Install Nginx if it is not already there and create
/etc/nginx/sites-available/monapp, a virtual host that proxies to Node's local port. TheUpgradeandConnectionheaders are for WebSockets, andX-Forwarded-Prototells the application that the client is on HTTPS:server { listen 80; server_name yourdomain.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-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }Enable the site, remove the default page, test the configuration and reload:
ln -s /etc/nginx/sites-available/monapp /etc/nginx/sites-enabled/ rm /etc/nginx/sites-enabled/default nginx -t && systemctl reload nginxSecure with HTTPS via Let's Encrypt
Install Certbot and its Nginx plugin, then request the certificate:
apt install certbot python3-certbot-nginx certbot --nginx -d yourdomain.comCertbot obtains the certificate, sets up the HTTP→HTTPS redirect and schedules automatic renewal. Your Node application is now served over HTTPS.
Centralize the Configuration in ecosystem.config.js
As soon as the VPS hosts more than one application, or an API comes with a worker, options passed on the command line become hard to keep track of. PM2 reads an ecosystem.config.js file that describes each process: script, folder, number of instances, memory limit and non-sensitive variables.
module.exports = {
apps: [
{
name: 'api',
cwd: '/home/monapp/app',
script: 'app.js',
instances: 'max',
exec_mode: 'cluster',
node_args: '--env-file=.env',
max_memory_restart: '500M',
env: { NODE_ENV: 'production', PORT: 3000 }
},
{
name: 'worker',
cwd: '/home/monapp/app',
script: 'worker.js',
node_args: '--env-file=.env'
}
]
}pm2 start ecosystem.config.js starts everything, pm2 reload ecosystem.config.js updates it, and the --only api option targets a single application. Here the API runs in cluster mode on every core and the worker as a single process. Version this file with the code; the secrets stay in the .env, outside the repository.
Logs: Reading Them, Then Rotating Them
PM2 captures the standard output and error output of every process in ~/.pm2/logs/, with one -out.log file and one -error.log file per application. pm2 logs monapp follows them live, and pm2 logs monapp --lines 100 shows the latest lines.
Without rotation, these files grow until the disk is full. The pm2-logrotate module takes care of it; by default it rotates a file past 10 MB and keeps 30 of them, thresholds that are easy to adjust:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7On the proxy side, a 502 error shows up in /var/log/nginx/error.log: it almost always means the Node.js process is stopped or listening on a different port from the one in proxy_pass.
For zero-downtime deployments, combine PM2 cluster mode with pm2 reload monapp rather than restart: PM2 restarts the workers one at a time, always keeping at least one instance available during the update. Then watch memory with pm2 monit and set an automatic restart limit (--max-memory-restart 500M) to guard against memory leaks in production.
The Official Documentation
For each tool's own options and version changes, refer to the official Node.js documentation, the Node.js LTS release schedule and the PM2 documentation. This guide covers putting an application into production on a VPS; the vendor documentation remains the reference for fine-tuning.