PM2 is a production process manager for Node.js applications. On Ubuntu 24.04, you can install PM2 with npm, use it to keep Node.js processes running after SSH disconnects or crashes, restore applications after reboot with systemd, and run stateless applications across multiple workers with cluster mode.
Raff users can run a Node.js application as a long-lived service with PM2 while keeping the application port private behind Nginx. This tutorial uses Node.js 24 LTS, a small Express application, PM2 cluster mode on the tested 2 vCPU VM, systemd startup persistence, graceful reload handling, and an Nginx reverse proxy. You will also verify that port 3000 listens only on 127.0.0.1, so the Node.js process is not exposed directly to the internet.
PM2 cluster mode can run multiple instances of a networked Node.js application on the same port, but PM2's current documentation explicitly recommends keeping clustered applications stateless. In-memory sessions, local worker state, and similar data should be moved to a shared service before scaling workers. PM2 also documents reload as a rolling reload path for clustered HTTP applications, with a fallback to a normal restart if the reload cannot complete. For that reason, this tutorial adds graceful shutdown handling instead of promising zero downtime under every failure condition.
Node.js 24 is an LTS release as of September 2026. If Node.js is not installed yet, use the current Node.js on Ubuntu 24.04 tutorial before continuing.
Step 1 — Verify Node.js 24 LTS and the Server User
Run the application as a normal user with sudo privileges rather than as root.
Check your user and Node.js installation:
whoami node --version npm --version command -v node
The Node.js project currently lists Node.js 24 as an LTS line. Your exact patch version can be newer than the version available when this tutorial was updated.
If node --version does not report v24.x.x, follow How to Install Node.js on Ubuntu 24.04 and return here after the runtime is working.
Verify: whoami should show your non-root deployment user, node --version should report v24.x.x, and npm --version should run without an error.
Step 2 — Create a Production Application Directory
Create a directory under /srv and give your deployment user ownership:
sudo mkdir -p /srv/myapp sudo chown "$USER:$USER" /srv/myapp cd /srv/myapp
Initialize npm and install Express:
npm init -y npm install express
Confirm the project files exist:
ls -la npm ls express
For your own application, deploy the repository into /srv/myapp instead of creating this sample project. Keep secrets such as API keys and database passwords out of version-controlled files.
Verify: /srv/myapp/package.json should exist and npm ls express should show Express installed without dependency errors.
Step 3 — Create a Localhost-Only Express App with Graceful Shutdown
Create app.js:
cat > /srv/myapp/app.js <<'EOF' const express = require('express'); const app = express(); const host = '127.0.0.1'; const port = Number(process.env.PORT || 3000); app.get('/', (req, res) => { res.json({ message: 'Node.js is running under PM2', pid: process.pid, }); }); app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', pid: process.pid }); }); const server = app.listen(port, host, () => { console.log(`Listening on http://${host}:${port}`); if (process.send) process.send('ready'); }); function shutdown(signal) { console.log(`${signal} received; closing HTTP server`); const forceExit = setTimeout(() => { console.error('Graceful shutdown timed out'); process.exit(1); }, 8000); forceExit.unref(); server.close((error) => { clearTimeout(forceExit); if (error) { console.error(error); process.exit(1); } process.exit(0); }); } process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); EOF
PM2's graceful-shutdown guidance recommends handling the stop signal, stopping new connections, finishing active work, closing external resources, and then exiting. In a real application, add database, queue, or cache cleanup before process.exit(0).
Verify:
node --check /srv/myapp/app.js
The command should exit with no syntax error.
Step 4 — Test the Node.js App Before Adding PM2
Start the application in the foreground:
cd /srv/myapp NODE_ENV=production PORT=3000 node app.js
In a second SSH session, test both routes:
curl -fsS http://127.0.0.1:3000/ curl -fsS http://127.0.0.1:3000/health
Check the listening address:
ss -lntp | grep ':3000'
The listener should be on 127.0.0.1:3000, not 0.0.0.0:3000 or [::]:3000.
Return to the first session and press Ctrl+C. The app should log that it received SIGINT and exit.
Verify: Both local requests should succeed, the health route should return "status":"ok", and port 3000 should be loopback-only.
Step 5 — Install PM2 on Ubuntu 24.04
If Node.js was installed system-wide with the NodeSource method from the Raff Node.js tutorial, install PM2 globally with:
sudo npm install -g pm2
If your Node.js runtime is managed by nvm under your user account, run the same npm command without sudo:
npm install -g pm2
Do not mix a system-wide Node.js runtime and an nvm-managed PM2 installation unless you deliberately manage both paths.
Verify PM2:
pm2 --version command -v pm2
If an existing application already has a start script in package.json, PM2 can run that npm script directly:
pm2 start npm --name myapp -- start
For this tutorial we continue with an ecosystem file because it keeps the worker count, readiness, restart, and environment settings together.
Verify: pm2 --version should return a version number and command -v pm2 should resolve to the installation associated with your intended Node.js runtime.
Step 6 — Create a PM2 Ecosystem File for the Tested 2 vCPU Setup
Create the PM2 configuration:
cat > /srv/myapp/ecosystem.config.js <<'EOF' module.exports = { apps: [ { name: 'myapp', script: './app.js', cwd: '/srv/myapp', instances: 2, exec_mode: 'cluster', wait_ready: true, listen_timeout: 10000, kill_timeout: 10000, restart_delay: 1000, watch: false, merge_logs: true, env: { NODE_ENV: 'production', PORT: 3000, }, }, ], }; EOF
The tested Raff VM has 2 vCPU, so this example uses two workers explicitly instead of instances: 'max'. On a 1 vCPU VM, set instances: 1. On larger machines, increase workers only after measuring your application and confirming that it is stateless.
Do not put secrets in this ecosystem file if it is committed to source control. Use your deployment's secret-management method or a restrictive runtime configuration file that is excluded from Git.
PM2's ecosystem-file reference documents instances, exec_mode, wait_ready, listen_timeout, kill_timeout, and restart controls. The combination here lets PM2 wait for the app's explicit ready message and gives the app time to finish requests during a reload.
Verify:
node --check /srv/myapp/ecosystem.config.js
The configuration should pass Node.js syntax validation.
Step 7 — Run Node.js with PM2 Cluster Mode
Start the ecosystem configuration:
cd /srv/myapp pm2 start ecosystem.config.js
Check status:
pm2 status pm2 describe myapp
Make several local requests:
for i in 1 2 3 4 5 6; do curl -fsS http://127.0.0.1:3000/health echo done
On the tested 2-worker setup, the responses should normally show more than one process ID over repeated requests. Do not depend on a strict alternating order.
If your application stores sessions or other state in process memory, do not enable multiple workers until that state has been moved to a shared store.
Verify: pm2 status should show two myapp instances online on the tested 2 vCPU setup, and repeated health requests should succeed.
Step 8 — Configure PM2 to Restore the App After Reboot
Generate the startup hook as your normal deployment user:
pm2 startup systemd
PM2 prints a sudo command containing the correct user, home directory, PM2 path, and Node.js path. Copy and run the exact command PM2 prints rather than copying a path from another server.
Save the current process list:
pm2 save
PM2's startup documentation uses the saved process list to resurrect applications after boot.
Check the generated service:
sudo systemctl is-enabled "pm2-$USER" sudo systemctl status "pm2-$USER" --no-pager
If you later change an nvm-managed Node.js version, PM2 recommends regenerating the startup hook because the Node.js path embedded in the service can change.
Verify: The pm2-$USER service should be enabled, and pm2 save should complete successfully.
Step 9 — Put Nginx in Front of the Private Node.js Port
Install Nginx if it is not already present:
sudo apt update sudo apt install -y nginx
Create a server block. Replace your-domain.com with your domain or the server address you use for testing:
sudo tee /etc/nginx/sites-available/myapp > /dev/null <<'EOF' server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; 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_read_timeout 60s; } } EOF
PM2 and Nginx solve different problems. PM2 keeps the Node.js process running and manages its workers; Nginx owns the public HTTP/HTTPS edge and proxies requests to the private application port. Using both keeps process management separate from public web traffic.
Enable the site and validate Nginx before reloading:
sudo ln -sfn /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp sudo nginx -t sudo systemctl reload nginx
For HTTPS, continue with Secure Nginx with Let's Encrypt on Ubuntu 24.04 after the HTTP proxy works.
Verify:
curl -fsS http://your-domain.com/health
The response should contain "status":"ok".
Step 10 — Confirm the Node.js Port Is Not Publicly Exposed
Check the local listener again:
ss -lntp | grep ':3000'
Expected listening address:
127.0.0.1:3000
Do not add a firewall rule that exposes TCP 3000 to the internet when Nginx is the intended public entry point. Only Nginx needs to accept public HTTP/HTTPS traffic for this architecture.
You can also test locally that Nginx and the app are separate layers:
curl -fsS http://127.0.0.1:3000/health curl -fsS http://your-domain.com/health
Verify: Both requests should succeed, while ss should still show the application itself bound only to 127.0.0.1:3000.
Step 11 — Test a Rolling Reload and Graceful Shutdown
PM2 cluster mode supports rolling reloads for HTTP applications:
pm2 reload myapp
Watch the application state:
pm2 status pm2 logs myapp --lines 30
The app's signal handler should allow active HTTP connections to finish before the worker exits. PM2's current cluster documentation states that reload keeps workers available one by one, but also notes that a failed reload can fall back to a normal restart. Treat reloads as a safer deployment mechanism, not as an unconditional availability guarantee.
For applications with database or queue connections, close those resources inside the shutdown handler before exiting.
Verify: The app should remain online after pm2 reload myapp, and a new request should succeed:
curl -fsS http://127.0.0.1:3000/health
Step 12 — Check Logs, Restarts, and Runtime State
Inspect the PM2 process details:
pm2 status pm2 describe myapp
View recent logs without leaving a long-running terminal session:
pm2 logs myapp --lines 50 --nostream
For interactive CPU and memory observation:
pm2 monit
Exit the monitor when finished. Use the measurements from your own workload to decide worker count, memory thresholds, and VM sizing; this tutorial does not assume a universal requests-per-second or latency figure for Node.js applications.
Verify: Both workers should remain online on the tested setup, restart counts should be understandable from your deployment actions, and recent logs should not show crash loops or binding errors.