Managing Node.js Apps on a Server With PM2: From Launch to Monitoring

Running Node.js with a bare node command means your app dies on the first crash or reboot. PM2 restarts it automatically, spreads it across every core in cluster mode, recycles it past a memory ceiling, rotates logs and starts with the server through systemd.
Running a Node.js app on a server with node server.js inside an SSH window only works on day one. The first unhandled exception kills the process, closing the window kills it, and a server reboot leaves it stopped until someone notices. PM2 is a process manager built for exactly this problem. It restarts your app the moment it crashes, runs one instance per core in cluster mode so you use the whole CPU instead of a single core, recycles a process when it passes a memory ceiling you set, collects and rotates logs so they never fill the disk, and starts automatically with the server through systemd. The full setup boils down to four commands: npm install -g pm2, then pm2 start ecosystem.config.js --env production, then pm2 startup systemd, then pm2 save. On a 4-core server, cluster mode means your app handles close to 4 times as many concurrent requests as a single process, because Node runs one main execution thread per process. This article walks through each step, and covers when PM2 is the right choice versus systemd alone or Docker.
Why you should not run node directly
The problems a process manager solves are not theoretical. They show up in the first week of any real application:
- Crashes: an unhandled promise rejection on a rare code path ends the process, and the site stays down until you restart it by hand.
- Reboots: a kernel security update reboots the server at 4 a.m. and it comes back without your app.
- Wasted cores: one Node process uses one core for JavaScript execution, so on an 8-core server 7 cores sit mostly idle.
- Memory leaks: a cache in a global variable grows slowly to 1.5 GB over a few days, then the OOM killer takes it down along with other processes.
- Logs:
console.loginto a file with no rotation reaches tens of gigabytes within weeks.
Cluster mode and using every core
In cluster mode PM2 runs several copies of your app sharing the same port and spreads connections between them round robin. The app needs no changes as long as it keeps no state in process memory:
pm2 start server.js -i max --name api
# or a fixed number that leaves a core for the database
pm2 start server.js -i 3 --name api
The important condition is that sessions, caches and queues live outside the process, in Redis for example, because any request might land on a different instance. If you keep user sessions in an in-memory Map, users get logged out at random whenever a request reaches another instance. On a Business plan server (4 vCPU and 8 GB), running 3 instances and leaving one core for the database is the split we usually recommend.
Cluster mode has another advantage. pm2 reload api restarts instances one after another, so some are always serving requests, which gives you zero-downtime deploys without an external load balancer.
An ecosystem file for environments
Rather than remembering flags every time, keep everything in one file inside the repository:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'api',
script: './dist/server.js',
instances: 3,
exec_mode: 'cluster',
max_memory_restart: '600M',
exp_backoff_restart_delay: 200,
kill_timeout: 5000,
wait_ready: true,
listen_timeout: 10000,
env: { NODE_ENV: 'development', PORT: 3000 },
env_production: { NODE_ENV: 'production', PORT: 3000 }
}]
};
Start it with pm2 start ecosystem.config.js --env production. The exp_backoff_restart_delay option prevents a frantic restart loop when the app crashes right after boot, growing the wait gradually from 200 milliseconds up to 15 seconds. wait_ready makes PM2 wait for a process.send('ready') message from your app after it connects to the database, so an instance is not considered ready until it truly is, and that is what makes reload fully safe. kill_timeout gives the process 5 seconds to finish in-flight requests after the stop signal before it is killed outright.
Restarting past a memory ceiling
max_memory_restart: '600M' is a safety net, not a cure. If any instance passes 600 MB, PM2 restarts it, and in cluster mode users notice nothing because the other instances keep serving. Pick the number after measuring: watch the app with pm2 monit for a few days, then set the ceiling at roughly twice normal usage. If the app settles around 250 MB, a 500 to 600 MB ceiling fits. If you see memory restarts more than once a day, you have a real leak that needs analysis with --inspect and heap snapshots, not a higher ceiling.
Do the total math as well. Three instances with a 600 MB ceiling means 1.8 GB in the worst case, which is about the most you should allow on a 4 GB server that also hosts a database. If you need more, adding 2 GB of RAM to your cloud server costs 600 SAR per year.
Log rotation
PM2 writes each app's output to ~/.pm2/logs/ with no default limit. The fix is an official module that rotates them:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 14
pm2 set pm2-logrotate:compress true
With this setup no log file exceeds 50 MB and the last 14 rotations are kept compressed, so log usage stays under roughly 300 MB per app instead of growing forever. To follow live logs use pm2 logs api --lines 200, and if you want structured JSON logs to ship to a central system, add log_type: 'json' to the ecosystem file.
Starting on boot with systemd
PM2 is itself a process that needs something to launch it after a reboot, and that is where systemd comes in:
pm2 startup systemd -u deploy --hp /home/deploy
# copy the command it prints and run it as root
pm2 save
The first command creates a pm2-deploy.service unit that launches PM2 at boot, and the second saves the current app list so it can be restored. The most common mistake here is adding a new app and forgetting pm2 save, so it runs until the first reboot and then vanishes. Make pm2 save the last line of your deploy script, and reboot once after setup to confirm it works rather than finding out during an incident.
Monitoring: what to check every day
pm2 ls shows the two numbers that matter most on one line per instance: the ↺ column, which counts restarts, and the memory column. The restart count is your first health signal. If it reads 0 or 1 after a week, things are fine; if it jumps to 40, something keeps crashing and PM2 is hiding it from you with quick restarts while users see intermittent errors you never notice. pm2 describe api shows the reason for the last stop, log paths and uptime. For automated alerts, add a simple cron check every 5 minutes that reads pm2 jlist as JSON and sends an alert when restarts climb by more than 5 within an hour, which catches problems before a customer reports them.
PM2, plain systemd or Docker
PM2 is not the only option, and the right pick depends on how your infrastructure looks:
| Criterion | PM2 | systemd alone | Docker |
|---|---|---|---|
| Restart on crash | Yes | Yes (Restart=always) | Yes (restart: unless-stopped) |
| Multi-core cluster mode | Built in | Manual with several units or node:cluster | Several containers behind a balancer |
| Zero-downtime deploys | pm2 reload | Needs manual orchestration | Needs an orchestration tool |
| Memory ceiling | Graceful restart | MemoryMax kills the process | mem_limit |
| Isolation between apps | Weak | Medium | Strong |
| Best fit | One or more Node apps on one server | A single simple service | Apps with different languages and versions |
Our working rule: if the server runs only Node apps, PM2 gives you cluster mode, reload and monitoring with the least effort. For one small single-instance service, a 10-line systemd unit is enough and saves you a layer. If the server mixes Node 18, Node 22, Python and a database, Docker prevents version conflicts, and you can even run PM2 inside the container with pm2-runtime. AI hosting templates such as n8n and Flowise are Node-based and ship with process management already set up.
Steps to ship it
- Install PM2 globally and create
ecosystem.config.jswith cluster mode and a memory ceiling. - Move sessions and caches to Redis before running more than one instance.
- Add
process.send('ready')after the database connects and enablewait_ready. - Install
pm2-logrotatewith a 50 MB limit and 14 rotations. - Run
pm2 startupthenpm2 save, and test one reboot. - Use
pm2 reloadin your deploy script instead ofrestart.
The Growth plan (2 vCPU and 4 GB) at 6,900 SAR per year fits a Node app with two instances plus Redis, while the Business plan at 11,500 SAR per year gives you 4 cores for three instances and a database. The full comparison is on the pricing page.


