RE:NODE

PM2 guide: ecosystem config, startup, logs and reload

By RE:NODE Team
15 min read


PM2 is a process manager for Node.js: a small background daemon that starts your application, restarts it when it dies, keeps its output in log files, and brings everything back after a reboot. Used properly it is four things - an ecosystem.config.js in the repository, pm2 start against that file, pm2-logrotate so the logs do not fill the disk, and pm2 save plus pm2 startup so the machine remembers what to run. Most PM2 problems come from skipping one of the four, or from running PM2 somewhere that already supervises your process.

That last point decides whether you need it at all. On a plain virtual or dedicated server, nothing restarts your app until you set something up, and PM2 is a reasonable choice. Inside a container on a hosting panel, the platform is already the supervisor, and a second one hides your crashes from it - PM2 vs a hosting panel covers that case in full and is not repeated here. This guide assumes a server you run yourself and goes through PM2 as it should be set up there.

What PM2 is and how it works#

PM2 has two parts. The daemon is a long-running Node process that owns your applications: it spawns them, watches them, restarts them, and writes their standard output and standard error to files. The CLI - the pm2 command - is a client that talks to the daemon over a socket. When you type pm2 start, the CLI starts the daemon if it is not already running, asks it to launch your app, prints a table, and exits. The app keeps running because the daemon keeps running.

Everything PM2 knows lives in PM2_HOME, which defaults to ~/.pm2 for the user who ran it. That directory holds the logs (~/.pm2/logs), the PID files, the daemon's own log (~/.pm2/pm2.log) and the saved process list (~/.pm2/dump.pm2). The important consequence: the daemon is per user. If you start your app as deploy and later type sudo pm2 ls, you are asking root's daemon, which has never heard of it, and you get an empty table. Pick one user, normally an unprivileged one that owns the application directory, and never mix.

Installing it is one global package:

bash
$ npm install -g pm2$ pm2 --version

PM2's commands and config keys have been stable for years, but defaults occasionally move between major versions, so when a number in this guide matters to you, check it against pm2 --version and the official documentation for that release.

Starting an app: pm2 start, names and interpreters#

The minimal form takes a script and a name. Always give a name; the default is the file name, and four apps all called index are unpleasant to manage.

bash
$ pm2 start dist/server.js --name api$ pm2 start dist/server.js --name api -- --port 3000   # args after -- go to your app$ pm2 start npm --name web -- start                    # runs "npm start"$ pm2 ls

pm2 ls (also pm2 list or pm2 status) prints one row per process: id, name, mode (fork or cluster), the restart counter, status, CPU and memory. The restart counter is the column to watch. A number that climbs while you are not deploying means the app is crashing and PM2 is quietly picking it up again.

Running through npm start works, but it puts npm between PM2 and your app: an extra process using memory, and an extra hop for signals on shutdown. Pointing PM2 at the real entry file is better whenever you can.

PM2 is not limited to JavaScript. It picks an interpreter from the file extension - .py gets python, .sh gets bash, .rb gets ruby, .php gets php - and you can set one explicitly with --interpreter. For a compiled binary or anything that is its own executable, the interpreter is none:

bash
$ pm2 start worker.py --name worker --interpreter python3$ pm2 start ./bin/server --name go-api --interpreter none$ pm2 start /srv/app/.venv/bin/gunicorn --name web --interpreter none \    -- app:app --bind 127.0.0.1:8000 --workers 2

Two things to know about Python under PM2. First, many current distributions ship python3 and no python, so the extension default fails with an interpreter-not-found error; set the interpreter, and preferably point it at the virtualenv's own binary so the right packages are on the path. Second, Python buffers standard output when it is not attached to a terminal, which under PM2 means your log lines appear in bursts or not at all until the process exits. Set PYTHONUNBUFFERED=1 in the environment, or run with -u. Python requirements and virtualenvs covers building the environment itself, and deploying FastAPI or Flask the server in front of it.

The ecosystem file#

Command-line flags live in somebody's shell history. An ecosystem file lives in the repository, gets reviewed, and starts the same way on every machine. pm2 init simple writes a skeleton; a realistic one looks like this:

ecosystem.config.js
module.exports = {  apps: [    {      name: "api",      script: "dist/server.js",      cwd: "/srv/api",      instances: 2,      exec_mode: "cluster",      max_memory_restart: "400M",      node_args: "--max-old-space-size=320",      watch: false,      time: true,      kill_timeout: 10000,      env: { NODE_ENV: "development", PORT: 3000 },      env_production: { NODE_ENV: "production", PORT: 3000 },    },    {      name: "worker",      script: "worker.py",      cwd: "/srv/api",      interpreter: "/srv/api/.venv/bin/python",      exec_mode: "fork",      env: { PYTHONUNBUFFERED: "1" },    },  ],};
bash
$ pm2 start ecosystem.config.js --env production$ pm2 reload ecosystem.config.js --env production   # after a deploy$ pm2 start ecosystem.config.js --only worker       # one app from the file

The keys that matter, and what they actually do:

KeyWhat it does
nameThe handle for every other command
scriptEntry file or binary, relative to cwd
argsArguments passed to your app
cwdWorking directory; set it explicitly
interpreternode by default; a path, or none
node_argsFlags for Node itself, such as heap size
instancesHow many processes; max means one per CPU
exec_modefork (default) or cluster
env / env_<name>Environment, picked with --env <name>
KeyWhat it does
max_memory_restartRestart a process above this memory, e.g. 400M
autorestarttrue by default; false for one-off jobs
max_restartsConsecutive unstable restarts before giving up
min_uptimeHow long a process must live to count as started
restart_delayFixed pause before each restart, in ms
exp_backoff_restart_delayGrowing pause between restarts, in ms
kill_timeoutWait after the stop signal before SIGKILL, in ms
cron_restartRestart on a cron expression
timePrefix every log line with a timestamp

A few of those deserve more than a table cell.

`env` and `env_production`. The plain env block is always applied; --env production merges env_production over it. That is useful, but it is not a secrets store - the ecosystem file is in the repository. Keep credentials in a file outside the repo with restrictive permissions and load it at the top of the ecosystem file (for example with require("dotenv").config({ path: "/etc/api/api.env" })), or have the app read it itself. Environment variables and secrets explains why the distinction matters.

`max_memory_restart`. PM2 checks memory on an interval, not on every allocation, so it is a tidy-up for slow leaks rather than a hard ceiling. It also restarts the process abruptly mid-request. Keep it above normal peak usage and pair it with a V8 heap limit below it, so Node fails in a way you can read before PM2 or the kernel steps in - Node memory limits explained has the arithmetic, including why every cluster worker gets its own heap.

`max_restarts` and `min_uptime`. A process that exits before min_uptime is counted as unstable; after max_restarts unstable starts in a row, PM2 stops trying and marks it errored. The documented defaults are 16 restarts and 1000 ms. If your app takes several seconds to fail - waiting on a database connection, for instance - it never counts as unstable and PM2 restarts it forever. exp_backoff_restart_delay: 100 is the better tool: the pause grows with each crash and resets once the app stays up, so a dead database does not turn into a busy loop.

`kill_timeout`. On stop, PM2 sends SIGINT and waits this long before SIGKILL. The default is 1600 ms, which is shorter than a slow request. If your app drains connections on shutdown, raise it to cover your longest request, and make sure the handler listens for SIGINT as well as SIGTERM. Graceful shutdown and health checks covers the application side.

Cluster mode and instances#

exec_mode: "cluster" uses Node's cluster module: PM2 starts several copies of your app and they share one listening port, with incoming connections spread across them. It is the only way one Node app uses more than one CPU core without you writing the forking yourself, and it is what makes zero-downtime reloads possible.

Three facts to set expectations:

  • Cluster mode is Node-only. A Python, Go or Bun process cannot share a port this way. For those, instances above 1 in fork mode starts several copies that all try to bind the same port, and all but the first fail. Use the app server's own workers - Gunicorn's --workers, Uvicorn's --workers - and leave PM2 at one instance.
  • `instances: "max"` counts the machine's logical CPUs. On a VDS that is your vCPU count, which is fine. 0 means the same thing, and -1 means all but one, leaving a core for the database on a single box.
  • Each instance is a full copy of your app. Memory per instance multiplies, in-memory state is not shared, and anything scheduled inside the app runs once per instance. The trade-offs - sticky sessions, jobs running four times, sizing workers to the cores you actually have - are laid out in the PM2 vs a hosting panel cluster section.

Scaling a running cluster up or down needs no restart:

bash
$ pm2 scale api 4$ pm2 scale api +1

The change lasts until the next start from the ecosystem file, which still says instances: 2. Edit the file if you mean it.

Restart, reload, stop and delete#

These four are the most confused part of PM2, and the difference is not cosmetic.

CommandWhat happens
pm2 restart apiKills every instance, then starts them again
pm2 reload apiCluster mode: replaces instances one at a time
pm2 stop apiStops it; it stays in the list
pm2 delete apiStops it and removes it from the list

restart has a gap: for a moment there is nothing listening, and requests in that moment fail. reload in cluster mode starts a new instance, waits for it to be ready, then stops an old one, and repeats until every instance is new - there is always something accepting connections. In fork mode there is nothing to roll across, and reload simply falls back to a restart.

"Ready" by default means the new instance has started listening. If your app needs to warm a cache or open a database pool before it can serve, tell PM2 to wait for an explicit signal:

ecosystem.config.js (excerpt)
wait_ready: true,listen_timeout: 10000,
server.js
server.listen(process.env.PORT, () => {  if (process.send) process.send("ready");});

If the signal never arrives within listen_timeout, PM2 carries on anyway, so a broken release still replaces a working one. Reload is a convenience, not a health check; zero-downtime deploys on a small server covers what else has to be true for a deploy to be truly gapless.

One more trap. PM2 captures the environment when a process is first started and reuses it. Changing an environment variable and running pm2 restart api keeps the old value. Add --update-env, or restart from the ecosystem file:

bash
$ pm2 reload ecosystem.config.js --env production --update-env

And stop versus delete matters because of the next section: a stopped process is still in the list and will be saved as stopped; a deleted one is gone only until you resurrect an old dump that still contains it.

Logs and pm2-logrotate#

Every process gets two files: ~/.pm2/logs/<name>-out.log for standard output and ~/.pm2/logs/<name>-error.log for standard error. In cluster mode each instance writes its own pair with the id in the name unless you set merge_logs: true.

bash
$ pm2 logs                    # all apps, streaming$ pm2 logs api --lines 200    # last 200 lines, then stream$ pm2 logs api --err          # error stream only$ pm2 flush api               # empty this app's log files

Set time: true in the ecosystem file unless your logger already writes timestamps. Without them, a log file is a list of events with no idea when any of them happened.

Out of the box, those files grow forever. A chatty app writing a few hundred kilobytes a minute fills a small disk in weeks, and a full disk takes the database down with it. The fix is a PM2 module:

bash
$ pm2 install pm2-logrotate$ pm2 set pm2-logrotate:max_size 20M$ pm2 set pm2-logrotate:retain 14$ pm2 set pm2-logrotate:compress true$ pm2 conf pm2-logrotate
SettingDefaultWhat it does
max_size10MRotate a file once it passes this size
retain30Rotated files to keep per log
compressfalseGzip rotated files
rotateInterval0 0 * * *Also rotate on this cron, daily by default

The module runs as its own process inside the daemon and appears in pm2 ls, which is normal. The settings are per user, like everything else in PM2. If you would rather keep logs in the system journal, systemd does that without a module; logs worth keeping is about what should be in them at all.

pm2 save and pm2 startup: surviving a reboot#

PM2 does not start at boot by itself. Two commands fix that, and they do different jobs.

pm2 startup generates a systemd unit that starts the PM2 daemon for your user at boot. Run as a normal user, it does not install anything; it prints the command that would, because installing a unit needs root:

bash
$ pm2 startup[PM2] To setup the Startup Script, copy/paste the following command:sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u deploy --hp /home/deploy

Copy that line exactly and run it. It writes and enables a unit named pm2-deploy.service (for the user deploy). The paths in it depend on where Node is installed on your machine, so do not copy one from a tutorial.

pm2 save writes the current process list to ~/.pm2/dump.pm2. At boot, the unit starts the daemon and runs pm2 resurrect, which restarts whatever was in that file. The sequence is therefore:

  1. Start everything you want running: pm2 start ecosystem.config.js --env production.
  2. Check it: pm2 ls, all online, restart counters at zero.
  3. Save it: pm2 save.
  4. Install the boot unit once: pm2 startup, then run the printed sudo line.
  5. Test it: sudo reboot, log back in, pm2 ls.

Step 5 is the one people skip, and a reboot is not the time to discover that the list was never saved.

If Node's path changes - a new version through nvm, for example - the unit still points at the old binary. Re-run the pair: pm2 unstartup systemd, then pm2 startup and its printed command, then pm2 save. After upgrading the PM2 package itself with npm install -g pm2, run pm2 update so the running daemon is replaced by the new version rather than left on the old one.

You can check the unit like any other: systemctl status pm2-deploy. If you are going to read systemd output anyway, it is worth knowing you could run the app as its own unit with no PM2 at all - systemd services for your apps covers that, and it is the leaner option for a single process.

Watching it: pm2 monit, describe and the restart counter#

bash
$ pm2 monit          # terminal dashboard: CPU, memory, logs per process$ pm2 describe api   # full detail: paths, restarts, uptime, env$ pm2 reset api      # zero the restart counter

pm2 monit is a live terminal view with a process list, per-process CPU and memory, and a log pane. It is handy during a deploy or a load test and useless at four in the morning, because it only shows anything while someone is looking at it. pm2 describe is the command for a post-mortem: it prints the script path, interpreter, log file locations, uptime, restart count, and unstable restarts.

The restart counter is the cheapest alert you have. Reset it after a deliberate deploy (pm2 reset api), and any non-zero number afterwards is a crash you did not cause. If nobody is going to look at it, a small cron job that runs pm2 jlist, which prints the process list as JSON, and complains when restart_time or status changes is a dozen lines of script and catches crash loops long before users do. PM2 also has a paid hosted dashboard; nothing in this guide needs it.

Common mistakes#

Watch mode in production. watch: true restarts the app whenever a file in its directory changes. In production that means every log write inside the project, every upload, every cache file triggers a restart, and the restart writes a log line, which triggers another. Keep watch: false outside development. If you do use it locally, add ignore_watch: ["node_modules", "logs", "uploads"].

Two daemons. One started as root, one as your user, each with half the apps. Run pm2 ls and sudo pm2 ls once and make sure only one of them has anything in it. Then delete from the other and pm2 kill it.

Forgetting `pm2 save`. Everything works until the first reboot. Make the save part of your deploy script.

Two supervisors. PM2 inside a container whose platform already restarts it, or PM2 started from a systemd unit you wrote yourself while pm2 startup also installed one. Pick one supervisor per process.

`instances: "max"` with stateful code. Sessions held in memory, WebSockets tracked in a Map, a scheduler inside the app. All break or multiply the moment there is a second instance.

Python logs that never appear. Unbuffered output is off by default. PYTHONUNBUFFERED=1.

Arguments in the wrong place. In pm2 start server.js --name api -- --port 3000, everything after -- goes to your app. Node flags such as --max-old-space-size belong in node_args or --node-args, not after the double dash, or they are silently ignored.

Treating `max_memory_restart` as a fix. It hides a leak by restarting the process on a schedule you did not choose. Find the leak.

FAQ#

Is PM2 free?

Yes. The process manager is open source and everything in this guide uses only the free CLI and the free pm2-logrotate module. The paid part is an optional hosted monitoring dashboard, which you do not need to run PM2.

Should I use PM2 or systemd?

For one process on a server, systemd is already installed and does the job with no extra daemon. PM2 earns its place when you want Node cluster mode with rolling reloads, or several small apps managed with one command. Both are fine; running both for the same app is not.

Can PM2 run Python, Go or a shell script?

Yes. Set interpreter to the Python binary (ideally inside the virtualenv), to none for a compiled binary, or let .sh files default to bash. Cluster mode and pm2 reload without downtime work only for Node, so other runtimes get one instance and their own worker model.

Where does PM2 keep my logs?

In ~/.pm2/logs of the user who started the app, as <name>-out.log and <name>-error.log. pm2 describe <name> prints the exact paths. Install pm2-logrotate before they become a disk problem.

Why did my app not come back after a reboot?

Either pm2 startup was never installed, or pm2 save was not run after the app was started, or it was saved under a different user. Check systemctl status pm2-<user>, then pm2 ls as that user, then start, save and reboot again.

Should I use PM2 on a hosting panel?

Usually not. A panel already restarts the process, keeps its output and tracks crashes, and PM2 in between hides those crashes from it. Start the app directly in the foreground instead.