Every application you deploy needs three things from the server: to keep running, to restart by itself after a crash, and to start again after a reboot. A process manager provides all three. This guide compares the three common choices on a VPS — PM2, systemd and Supervisor — with working configurations for each and a clear recommendation.
Quick Comparison
| systemd | PM2 | Supervisor | |
|---|---|---|---|
| Installed already? | Yes, on every modern distribution | No (npm package) | No (apt / dnf package) |
| Languages | Any | Built for Node.js, runs any command | Any; popular with Python |
| Starts on boot | Native | Via a systemd unit it generates | Runs as a systemd service |
| Logs | journald (journalctl) | Own log files, pm2 logs | Own log files |
| Resource limits and sandboxing | Yes — CPU, memory, file-system protection | Memory-based restart only | No |
| Multi-core for Node.js | Run several instances | Built-in cluster mode | Run several instances |
systemd: The Default Choice
systemd is already managing everything else on the server, so your application gets the same tooling: systemctl to control it, journalctl for logs, boot ordering, and resource limits — with nothing extra to install or keep updated.
sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network-online.target
Wants=network-online.target
[Service]
User=deploy
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/.env
ExecStart=/usr/bin/node /opt/myapp/index.js
Restart=on-failure
RestartSec=3
# limits and sandboxing
MemoryMax=512M
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
journalctl -u myapp -f
The same file works for Python (ExecStart=/opt/myapp/venv/bin/gunicorn app:app), Go (the compiled binary) or anything else. More in How to Manage Services with systemd.
PM2: Convenient for Node.js
npm install -g pm2
pm2 start index.js --name myapp -i max # cluster mode: one process per core
pm2 save
pm2 startup systemd # prints a command to run with sudo
PM2’s strengths are the developer-friendly CLI (pm2 ls, pm2 logs, pm2 monit), zero-downtime reloads with pm2 reload myapp, and cluster mode that spreads a Node.js app across all cores without code changes. An ecosystem.config.js file keeps the settings in your repository.
Two things to remember: run pm2 save after changes, or a reboot restores the old process list; and rotate the logs (pm2 install pm2-logrotate), or they grow until the disk is full. Full walkthrough: How to Deploy a Node.js Application with PM2 and Nginx.
Supervisor: Simple INI Files
sudo apt install supervisor -y # dnf install supervisor on AlmaLinux / Rocky (EPEL)
sudo nano /etc/supervisor/conf.d/myapp.conf
[program:myapp]
command=/opt/myapp/venv/bin/gunicorn myproject.wsgi:application --bind 127.0.0.1:8000
directory=/opt/myapp
user=deploy
autostart=true
autorestart=true
stopasgroup=true
stdout_logfile=/var/log/myapp/out.log
stderr_logfile=/var/log/myapp/err.log
sudo mkdir -p /var/log/myapp
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status
Supervisor is still common in Python and Django deployments and handles queue workers well (numprocs=4 runs four copies). On a new server, systemd does the same job without the extra service.
Which Should You Use?
- Most servers: systemd. Nothing to install, works for every language, and gives you limits and sandboxing.
- Node.js teams who want cluster mode and a friendly CLI: PM2, started by systemd at boot.
- An existing setup built on Supervisor: keep it; there is no urgent reason to migrate.
Whatever you choose, use one per server — mixing them makes it hard to know what is managing what.
Same Rules for All Three
- Run applications as an unprivileged user, never as root.
- Bind the application to
127.0.0.1and put Nginx in front for HTTPS — see How to Build and Secure a REST API. - Keep secrets in an environment file with
600permissions, not in the unit file or repository. - Make sure logs are rotated.
Common Problems
The app runs by hand but fails as a service — usually a missing environment variable or a relative path. Set WorkingDirectory and the environment explicitly, then read journalctl -u myapp -n 50.
Restarts in a loop — systemd stops trying after repeated failures (“start request repeated too quickly”). Fix the crash, then systemctl reset-failed myapp.
PM2 apps gone after reboot — pm2 save was not run, or pm2 startup was run as a different user.
“node: command not found” under systemd — Node installed with NVM lives in the user’s home directory; use its full path in ExecStart (which node as that user).
Continue Reading
- How to Manage Services with systemd and systemctl
- How to Deploy a Django/Flask Application with Gunicorn and Nginx
- Which Programming Language and Runtime Should You Deploy On?
- VPS for Developers
Browse more articles in Programming Languages & Runtimes.