PM2 & systemd
Supervise Node.js 24 processes on VMs with PM2 or systemd when you are not on Kubernetes or Fargate.
Recipe
Quick-reference recipe card - copy-paste ready.
# /etc/systemd/system/api.service
[Unit]
Description=Node API
After=network.target
[Service]
Type=simple
User=nodeapp
WorkingDirectory=/opt/api
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node dist/main.js
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=30
[Install]
WantedBy=multi-user.targetWhen to reach for this: Bare EC2, on-prem VMs, or legacy deploy paths. Prefer containers + orchestrator for new services.
Working Example
systemd Unit
[Unit]
Description=Acme API (Node 24)
After=network-online.target
Wants=network-online.target
[Service]
User=nodeapp
Group=nodeapp
WorkingDirectory=/opt/api/current
EnvironmentFile=/etc/acme/api.env
ExecStart=/usr/bin/node --max-old-space-size=512 dist/main.js
Restart=on-failure
RestartSec=5
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=45
LimitNOFILE=65535
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable api
sudo systemctl start api
sudo journalctl -u api -f# Deploy new release
sudo -u nodeapp ln -sfn /opt/api/releases/20260709-abc123 /opt/api/current
sudo systemctl restart apiPM2 Ecosystem
// ecosystem.config.cjs
module.exports = {
apps: [
{
name: "api",
cwd: "/opt/api/current",
script: "dist/main.js",
instances: 2,
exec_mode: "cluster",
env: {
NODE_ENV: "production",
PORT: 3000,
},
max_memory_restart: "600M",
kill_timeout: 30_000,
listen_timeout: 10_000,
merge_logs: true,
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
},
],
};pm2 start ecosystem.config.cjs
pm2 save
pm2 startup systemd -u nodeapp --hp /home/nodeappWhat this demonstrates:
- systemd: single process, journald logs, env file for secrets
- PM2: cluster mode on one VM (use when not containerized)
- Symlink
currentrelease directory for atomic deploy switch KillSignal=SIGTERMandkill_timeoutalign with graceful shutdown
Deep Dive
systemd vs PM2
| Feature | systemd | PM2 |
|---|---|---|
| Cluster on VM | One unit per instance or manual | Built-in instances |
| Log aggregation | journald | PM2 logs + optional forward |
| Zero-downtime reload | systemd restart blip | pm2 reload cluster |
| OS integration | Native on Linux | Node npm package |
| Best for | Single process, simple ops | Multi-process on one VM |
PM2 Reload (Cluster)
pm2 reload apiStarts new workers before killing old (similar to rolling update). Requires app to handle SIGINT/SIGTERM - Graceful Shutdown.
Environment Files
# /etc/acme/api.env (chmod 600, root:nodeapp)
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://...Never world-readable secret files. Prefer SSM agent pulling secrets at boot on AWS VMs.
Reverse Proxy
Internet -> nginx (TLS) -> localhost:3000 (Node)
nginx handles TLS and request buffering; Node binds localhost only if desired.
Gotchas
- PM2 inside Docker/K8s - double supervision fights platform signals. Fix: one
nodeprocess per container. - Running as root - security risk. Fix: dedicated
nodeappuser with filesystem ownership. - No
TimeoutStopSec- systemd sends SIGKILL at default 90s inconsistently. Fix: explicit 30-45s matching app shutdown. pm2 startupforgotten after reboot - service down after instance recycle. Fix:pm2 saveafterstartupsetup.- Cluster mode + in-memory state - broken sessions across workers. Fix: sticky sessions at nginx or external session store.
- Deploy without symlink atomicity - half-written
dist/. Fix: release dirs +ln -sfnswap.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| systemd | Simple single-process VM | Need built-in cluster on VM |
| PM2 cluster | Multi-core VM without containers | Already on K8s |
| Docker on VM | Container parity without K8s | Minimal container ops appetite |
| ECS/Fargate | Leave VM ops entirely | Hard on-prem requirement |
FAQs
Should new projects use PM2?
Default to containers on ECS/Fargate or K8s. PM2 remains valid for legacy VMs and quick EC2 setups.
How many PM2 instances?
Match CPU cores (often 2-4 on a small API VM). Measure RSS; avoid OOM from too many heaps.
systemd socket activation?
Rare for Node HTTP servers. Bind port in app with PORT env; nginx in front is simpler.
Log rotation?
journald rotates automatically. PM2: pm2 install pm2-logrotate or forward to Loki/Datadog agent.
Node binary path?
Use absolute path /usr/bin/node or nvm shim in systemd ExecStart. Document in runbook.
Health checks on VM?
ALB health check to /health or local cron curl alerting. Same endpoints as containers.
Related
- Graceful Shutdown - SIGTERM handling
- Runtime Ops Basics - supervision model
- Zero-Downtime Deploys - PM2 reload pattern
- Platform Deploy Basics - why orchestrators replace PM2
- Runtime Ops Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.