Skip to content
Express.js ex deployment 4 min read

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:

OptionPurpose
nameLabel shown in pm2 list and used in commands
scriptEntry file to run
instancesNumber of workers; "max" uses all CPU cores
exec_modefork (single) or cluster (load-balanced)
max_memory_restartRestart a worker if it exceeds this memory
env / env_productionEnvironment variables per environment
watchRestart 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 reload for rolling, zero-downtime deploys and reserve pm2 restart for cases where you must hard-cycle the process (e.g. after changing native dependencies).

Best practices

  • Keep your start configuration in an ecosystem.config.js checked into source control instead of typing flags by hand.
  • Run exec_mode: "cluster" with instances: "max" to use every CPU core, and keep your app stateless so workers are interchangeable.
  • Always pair pm2 startup with pm2 save so apps survive a reboot.
  • Use pm2 reload (not restart) for deploys and implement graceful shutdown so no request is dropped.
  • Install pm2-logrotate and cap max_size/retain so logs never fill the disk.
  • Set max_memory_restart to recover from slow memory leaks automatically.
  • In containerized environments, prefer pm2-runtime start ecosystem.config.js so PM2 stays in the foreground as PID 1.
Last updated June 14, 2026
Was this helpful?