Process Management with PM2
A plain node app.js dies when the process crashes, uses only one CPU core, and produces no daemon — you'd have to keep the terminal open. PM2 is the production process manager that fills all three gaps: it restarts crashed processes, runs one instance per CPU core (or any number you choose), daemonizes the app so it keeps running after logout, and survives server reboots. It's the go-to solution for running Node on a VM or bare metal, providing an ecosystem.config.js for declarative configuration, a handy CLI for runtime management, and built-in log aggregation. This page covers the core commands, cluster mode, the ecosystem file, log management, and startup on boot.
Running an app with PM2
Bash
npm install -g pm2 pm2 start app.js --name api # start and daemonize pm2 start dist/index.js --name api # compiled TypeScript pm2 list # status of all managed processes pm2 logs api # tail logs (stdout + stderr, merged) pm2 restart api # restart gracefully pm2 stop api # stop without removing pm2 delete api # stop and remove from PM2's list
┌────┬────────────────────┬──────────┬───────┬──────────┬──────┬──────────┐ │ id │ name │ mode │ ↺ │ status │ cpu │ memory │ ├────┼────────────────────┼──────────┼───────┼──────────┼──────┼──────────┤ │ 0 │ api │ fork │ 0 │ online │ 0% │ 52.0mb │ └────┴────────────────────┴──────────┴───────┴──────────┴──────┴──────────┘
PM2 daemonizes your app and restarts it on crash — `pm2 list` shows the runtime status of every managed process
`pm2 start` launches your app as a **daemon** (runs in the background, survives terminal logout) and registers it with PM2's process list. On any crash — uncaught exception, OOM, segfault — PM2 **automatically restarts** it, so brief failures self-heal without human intervention. `pm2 list` gives a live dashboard of every process: name, mode, restart count, status, CPU, and memory. The restart count (`↺`) is a useful signal — a rising number means the app is crash-looping and needs attention. `pm2 logs` merges and tails stdout/stderr from all instances, while `pm2 logs --lines 200` shows recent history. This baseline already solves the "process dies and stays down" problem; cluster mode solves the "one core" problem.
Cluster mode — use all CPU cores
Bash
# -i max spawns one worker per logical CPU — fully uses the machine: pm2 start dist/index.js --name api -i max # Zero-downtime reload: new workers start BEFORE old ones stop: pm2 reload api # Scale up or down without stopping: pm2 scale api 4 # run exactly 4 instances
Cluster mode requires stateless app instances — session/cache state must live in Redis, not in process memory
Cluster mode spawns **multiple Node processes** sharing the same port (via the built-in [cluster module](/nodejs/cluster-module) under the hood), so all CPU cores handle real traffic. But it *requires* that your app is **stateless**: each instance is independent, and a user's requests can land on any of them. Any state kept in process memory — cached values, session data, active WebSocket rooms — **is invisible to the other workers**. This is the same rule from [scaling strategies](/nodejs/scaling-strategies): put shared state in **[Redis](/nodejs/redis)** or your database, not in `Map`s or module-level variables. If your app *is* stateless already (pure request/response, state lives in the DB), cluster mode is a free horizontal-scaling win that requires zero code changes. Validate with `pm2 reload` — if requests fail during the reload, your app isn't truly stateless.
ecosystem.config.js — declarative config
ecosystem.config.js
JS
export default {
apps: [
{
name: 'api',
script: 'dist/index.js',
instances: 'max', // one per CPU core
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
max_memory_restart: '512M', // restart if a worker exceeds memory limit
out_file: '/var/log/api/out.log',
error_file: '/var/log/api/err.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
},
],
}Bash
pm2 start ecosystem.config.js --env production # apply the env_production block pm2 save # persist current process list
Commit `ecosystem.config.js` — it's the declarative spec for how PM2 runs your app, replacing long command-line flags
Rather than memorizing command-line flags, **`ecosystem.config.js`** defines your app's PM2 config declaratively and **commits it** to the repo — so anyone deploying gets the same settings, and CI can use `pm2 start ecosystem.config.js`. It supports per-environment variable blocks (`env_production`), memory limits (PM2 restarts workers that exceed them, guarding against slow memory leaks), log file paths, instance count, and more. `pm2 save` serializes the current running process list to `~/.pm2/dump.pm2`; combined with startup hooks (below), it ensures the same processes restart after a reboot. Treat the ecosystem file as code: review it, version it, and use it as the single source of truth for your PM2 configuration.
Survive server reboots
Bash
# Generate and install a startup script for the current OS (systemd, upstart, etc.): pm2 startup # prints a command — run it with sudo as instructed pm2 save # save the current list so it restores after reboot pm2 unstartup # remove the startup hook
`pm2 startup` + `pm2 save` ensures your processes restart automatically after a server reboot
By default PM2's process list is in memory — a server reboot wipes it. Two commands make it persistent: **`pm2 startup`** generates and installs a system-level startup hook (systemd unit, init script, etc.) that relaunches PM2 on boot, and **`pm2 save`** persists the current process list to a file that PM2 reads at startup. Together, a reboot → PM2 starts → restores all saved processes → your app is live again, automatically. Run `pm2 startup` once when provisioning a server and `pm2 save` after every change to the process list. In container/Kubernetes environments you typically don't use PM2 startup (the orchestrator manages restart), but on a plain VM or bare-metal server these two commands are essential for reliability across reboots.
Next
Put Nginx in front to handle TLS and routing: [Reverse Proxy with Nginx](/nodejs/reverse-proxy-nginx).