Every Node.js app on a VPS needs something to start it, restart it when it crashes and bring it back after a reboot. The two common answers are PM2 and systemd. Both work. They're good at different things.
Quick answer
-
PM2 if you want cluster mode,
pm2 reloadwith no dropped requests, and friendly per-app commands. - systemd if you want zero extra dependencies, strict sandboxing and the same tool you use for every other service.
What each one is
systemd is the init system on almost every modern Linux distro. It already runs Nginx, SSH and cron. A Node app becomes one more unit file.
PM2 is a Node.js process manager installed via npm. It runs its own daemon, which manages your apps and is itself started by systemd through pm2 startup. ShipOwn's guide What Is PM2? covers it in depth.
Side by side
| PM2 | systemd | |
|---|---|---|
| Install | npm i -g pm2 |
Built in |
| Auto-restart on crash | Yes | Restart=always |
| Start on boot |
pm2 startup + pm2 save
|
systemctl enable |
| Cluster mode across CPU cores | Built in | You run multiple units or use Node's cluster module |
| Zero-downtime reload | pm2 reload |
Not out of the box |
| Logs |
~/.pm2/logs, pm2 logs
|
journald, journalctl -u
|
| Log rotation |
pm2-logrotate module |
journald handles it |
| Memory limit restart | max_memory_restart |
MemoryMax= (kills), plus Restart=
|
| Sandboxing | Minimal | Strong (ProtectSystem, NoNewPrivileges...) |
| Monitoring view | pm2 monit |
systemctl status, external tools |
A minimal systemd unit
[Unit]
Description=My Node API
After=network.target
[Service]
User=deploy
WorkingDirectory=/srv/api/current
EnvironmentFile=/srv/api/shared/.env
ExecStart=/usr/bin/node dist/main.js
Restart=always
RestartSec=3
NoNewPrivileges=true
ProtectSystem=full
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now api
ShipOwn's free systemd service file generator builds one of these in your browser.
A minimal PM2 setup
module.exports = { apps: [{
name: 'api', script: 'dist/main.js', cwd: '/srv/api/current',
instances: 'max', exec_mode: 'cluster', max_memory_restart: '400M'
}]};
pm2 start ecosystem.config.js && pm2 save && pm2 startup
When PM2 is the better choice
- You run several Node apps on one server and want one consistent set of commands.
- You want to use all CPU cores without writing cluster code.
- You deploy often and want
pm2 reloadto roll workers without dropping requests.
When systemd is the better choice
- You run a single service and don't want another daemon or global npm package.
- Security hardening matters and you want systemd's sandbox options.
- Your team already manages everything with
systemctland journald.
Mistakes with either
- Running as root. Use a deploy user in both cases.
-
Forgetting reboot persistence.
pm2 saveafter changes;systemctl enablefor units. Test with a real reboot. -
Unbounded logs. Install
pm2-logrotate, or check journald'sSystemMaxUse. -
Env changes not applied.
pm2 reload --update-env, orsystemctl restartafter editing theEnvironmentFile.
My choice
For Node apps I deploy often, I use PM2, mainly for reloads and cluster mode. It's also what ShipOwn, the desktop app I built for deploying to servers you already own, uses for Node apps: Next.js, Nuxt, Remix, SvelteKit, NestJS, Fastify and Express run under PM2, and you can see PM2 processes next to CPU, RAM, disk and a live terminal in the health view. For a single small internal service, a systemd unit is hard to beat.
Written by Vishvajeet Shukla, full stack developer and founder of ShipOwn.
Top comments (0)