You SSH into your VPS, run node server.js, and everything works. Close the terminal to grab a coffee, come back, and the site's down. Or worse — it runs fine for three days, then a bad request crashes it at 2 a.m. and it never comes back up on its own. Both problems have the same root cause, and the same fix.
Symptom: the app dies when you're not watching it
This shows up in a few different ways depending on what killed it:
- The site works while you're SSH'd in, but goes down the second you disconnect (even with the process seemingly still "running" in a screen you forgot about).
- A memory leak or an unhandled promise rejection crashes the process, and nothing restarts it. Visitors get
ERR_CONNECTION_REFUSEDuntil you notice and manually restart. - You reboot the VPS for a kernel update, and the app just doesn't come back — because it was never actually configured to start on boot.
Cause: node.js has no built-in process supervision
Running node app.js directly starts a single foreground process attached to your shell. When your SSH session ends, the shell sends a SIGHUP to its child processes, and Node exits with it — there's no daemon behavior, no auto-restart, no boot persistence built in. Node was never designed to babysit itself; that job belongs to a process manager sitting on top of it.
This is different from a typical cPanel "Setup Node.js App" account, where WHM/cPanel wires the app into Apache's mod_passenger for you. On a bare VPS you're managing the whole stack yourself, so you need your own supervisor. PM2 is the most common choice because it's purpose-built for Node, but the same problem could be solved with systemd or Docker — more on that trade-off in the FAQ below.
Fix: install PM2 and hand it your app
1. Install PM2 globally
sudo npm install -g pm2Check it installed correctly:
pm2 --version2. Start your app under PM2 instead of node
cd /var/www/myapp
pm2 start server.js --name myappFor apps started via npm start:
pm2 start npm --name myapp -- startConfirm it's running:
pm2 listYou'll see a table with the app name, status (online), CPU, memory, and restart count. That restart count matters — if it's climbing on its own, something's crashing repeatedly and you've got a separate bug to chase.
3. Make it survive a reboot
This is the step most people skip, and it's the one that actually fixes the "didn't come back after a server restart" problem:
pm2 startupPM2 prints a command tailored to your OS and user — copy and run that exact line (it registers a systemd service). Then save the current process list so it knows what to relaunch:
pm2 saveFrom now on, a reboot brings your app back automatically, with no manual SSH-and-restart required.
4. Use an ecosystem file for anything beyond one simple app
Once you've got more than one app, or need environment variables, create ecosystem.config.js in your project root:
module.exports = {
apps: [
{
name: "myapp",
script: "server.js",
instances: 2,
exec_mode: "cluster",
env: {
NODE_ENV: "production",
PORT: 3000
},
max_memory_restart: "300M"
}
]
}Start everything defined in the file with:
pm2 start ecosystem.config.jsexec_mode: "cluster" with instances: 2 runs two copies of your app across CPU cores and load-balances between them — useful once a single process can't keep up, and it also means one crashed worker doesn't take the whole site down. max_memory_restart auto-restarts a worker if it balloons past a memory ceiling, which is a cheap safety net against slow leaks.
5. Put Nginx in front of it
PM2 keeps the Node process alive; it doesn't serve HTTPS or handle your domain. A minimal reverse proxy block:
server {
listen 80;
server_name 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_cache_bypass $http_upgrade;
}
}Then issue a certificate with Certbot and let it handle the TLS termination — Node itself should stay on plain HTTP behind the proxy.
6. Deploy updates without downtime
Once you're in cluster mode, ship code changes with a reload instead of a restart:
pm2 reload myappreload replaces workers one at a time, so there's always at least one instance serving traffic. Plain pm2 restart kills everything first, which means a brief gap — fine for a single-instance app, not what you want for anything customer-facing.
| Command | What it does | When to use it |
|---|---|---|
pm2 restart myapp | Stops then starts the process | Single instance, brief downtime is fine |
pm2 reload myapp | Rolling restart, zero downtime | Cluster mode, production deploys |
pm2 logs myapp | Tails stdout/stderr | Debugging a crash right after it happens |
pm2 monit | Live CPU/memory dashboard | Checking if a leak is building up |
pm2 flush | Clears log files | Logs are eating disk space |
Prevention: don't wait for the next crash to notice
- Set up log rotation. PM2 logs grow forever by default. Install the rotate module:
pm2 install pm2-logrotate, then it'll cap file size and keep a rolling number of days automatically. - Run PM2 as a non-root user. Create a dedicated app user, run
pm2 startupunder that account, and keep your Node process off root privileges entirely. - Watch the restart count, not just uptime. A process showing "online" with 40 restarts in the last hour is not healthy — it's crash-looping and PM2 is just quietly catching it every time.
- Re-run
pm2 saveafter any change to the process list — adding an app, changing its name, or switching to cluster mode. If you forget, a reboot restores the old, outdated list.