Running with PM2
Running node server.js in a terminal is fine for development, but in production you need a process that survives crashes, restarts on boot, scales across CPU cores, and rotates its logs. PM2 is a battle-tested process manager for Node.js that handles all of this with a single command. It keeps your Express app alive, runs it in cluster mode to use every core, and lets you reload code without dropping a single request.
Installing and starting an app
Install PM2 globally (or as a project dependency so the exact version is pinned in CI):
npm install -g pm2
# or, pinned per-project:
npm install pm2 --save-dev
The simplest way to launch an Express app is pm2 start. Point it at your entry file and give it a name so you can manage it later:
pm2 start server.js --name api
PM2 daemonizes the process, captures its stdout/stderr, and restarts it automatically if it exits with a non-zero code. List what’s running with pm2 list:
pm2 list
Output:
┌────┬──────┬─────────┬─────────┬─────────┬──────────┬────────┐
│ id │ name │ mode │ status │ cpu │ mem │ uptime │
├────┼──────┼─────────┼─────────┼─────────┼──────────┼────────┤
│ 0 │ api │ fork │ online │ 0.2% │ 58.4mb │ 12s │
└────┴──────┴─────────┴─────────┴─────────┴──────────┴────────┘
A minimal Express server PM2 can manage:
const express = require("express");
const app = express();
app.get("/health", (req, res) => {
res.json({ status: "ok", pid: process.pid });
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Worker ${process.pid} listening on ${port}`);
});
Using an ecosystem config file
Passing flags on the command line gets unwieldy fast. Define an ecosystem file so your start configuration lives in version control. Generate a template with pm2 ecosystem, then edit it:
// ecosystem.config.js
module.exports = {
apps: [
{
name: "api",
script: "./server.js",
instances: "max",
exec_mode: "cluster",
max_memory_restart: "300M",
env: {
NODE_ENV: "development",
PORT: 3000,
},
env_production: {
NODE_ENV: "production",
PORT: 8080,
},
},
],
};
Start it, selecting the production environment block:
pm2 start ecosystem.config.js --env production
The most common options:
| Option | Purpose |
|---|---|
name | Label shown in pm2 list and used in commands |
script | Entry file to run |
instances | Number of workers; "max" uses all CPU cores |
exec_mode | fork (single) or cluster (load-balanced) |
max_memory_restart | Restart a worker if it exceeds this memory |
env / env_production | Environment variables per environment |
watch | Restart on file changes (dev only — never in production) |
Cluster mode
Node.js is single-threaded, so one process uses one core. Cluster mode forks multiple workers that share the same port via the OS, and PM2 round-robins incoming connections across them. Set exec_mode: "cluster" and instances: "max" (or a specific number) as above — no changes to your Express code are required, because PM2 uses Node’s built-in cluster module under the hood.
pm2 start ecosystem.config.js -i max
Cluster mode requires your app to be stateless. In-memory sessions, caches, or rate-limit counters won’t be shared across workers — push that state into Redis or another shared store, or you’ll get inconsistent behavior depending on which worker handles a request.
Logs and monitoring
PM2 aggregates the stdout and stderr of every worker. Tail them live with pm2 logs:
pm2 logs api --lines 100
Output:
0|api | Worker 18421 listening on 8080
1|api | Worker 18422 listening on 8080
0|api | GET /health 200 1.2 ms
For an interactive dashboard of CPU, memory, and request throughput, run pm2 monit. To prevent log files from filling the disk, install the rotation module:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
Auto-restart and surviving reboots
PM2 restarts a crashed process immediately by default. If a process crashes repeatedly in a tight loop, PM2 backs off and marks it errored after max_restarts (default 15) to avoid burning CPU. You can tune this in the ecosystem file with max_restarts and restart_delay.
To bring your apps back after a server reboot, save the current process list and generate a startup script for your init system (systemd, etc.):
pm2 startup
pm2 save
pm2 startup prints a one-line command to run with sudo; pm2 save snapshots the running apps so they’re resurrected on boot.
Zero-downtime reloads
A plain restart kills and re-spawns workers, causing a brief outage. In cluster mode, pm2 reload instead restarts workers one at a time, only routing traffic to a new worker once it’s listening — so requests are never dropped:
pm2 reload api
For this to be truly graceful, your Express app should handle SIGINT/SIGTERM by finishing in-flight requests before exiting:
const server = app.listen(port);
process.on("SIGINT", () => {
console.log("Draining connections before shutdown...");
server.close(() => {
console.log("Closed remaining connections. Exiting.");
process.exit(0);
});
});
Use
pm2 reloadfor rolling, zero-downtime deploys and reservepm2 restartfor cases where you must hard-cycle the process (e.g. after changing native dependencies).
Best practices
- Keep your start configuration in an
ecosystem.config.jschecked into source control instead of typing flags by hand. - Run
exec_mode: "cluster"withinstances: "max"to use every CPU core, and keep your app stateless so workers are interchangeable. - Always pair
pm2 startupwithpm2 saveso apps survive a reboot. - Use
pm2 reload(notrestart) for deploys and implement graceful shutdown so no request is dropped. - Install
pm2-logrotateand capmax_size/retainso logs never fill the disk. - Set
max_memory_restartto recover from slow memory leaks automatically. - In containerized environments, prefer
pm2-runtime start ecosystem.config.jsso PM2 stays in the foreground as PID 1.