RE:NODE
Browse hosting

App hosting12 min read

PM2 vs a hosting panel: what a process manager is for

What PM2 actually does, what a container-based panel already does, and why running a process manager inside one hides the crashes you most need to see.

0 readers

PM2 answers a question that a hosting panel has usually already answered: what restarts my application when it exits, and where do its logs go. Run both and you get two supervisors stacked on top of each other, and the outer one - the one with the memory graph, the restart history and the alerting - loses sight of the process it is supposed to be watching. An app crash-looping under PM2 looks, from outside the container, like an app that has been up for nine days.

That is the short answer: on a container-based panel, start your process directly and let the platform supervise it. On a plain virtual server with no supervision of its own, a process manager is exactly right - though systemd is already installed and does most of the same job. The rest of this post is the detail: what each layer actually provides, where they collide, and how to move off PM2 without losing the things it was genuinely doing for you.

What PM2 actually does#

PM2 is a daemon plus a CLI. pm2 start hands your script to a background daemon which spawns it, watches it, restarts it when it exits, and writes its output to files. The CLI then exits, which is the first thing that matters in a container.

bash
$ pm2 start dist/server.js --name api -i 2$ pm2 ls$ pm2 logs api --lines 100$ pm2 restart api$ pm2 save && pm2 startup

Its feature list is genuinely useful on a bare machine:

  • Restart on exit, with min_uptime, max_restarts and an optional exponential backoff so a process that dies instantly is not restarted in a tight loop.
  • Cluster mode, which uses Node's cluster module to run several worker processes sharing one listening socket.
  • `pm2 reload`, which in cluster mode restarts workers one at a time, so there is always a worker accepting connections.
  • Log capture to ~/.pm2/logs/<name>-out.log and -error.log, with rotation if you install the pm2-logrotate module.
  • Start at boot, via pm2 startup, which generates and installs a systemd unit that runs pm2 resurrect to bring back whatever pm2 save recorded.
  • `max_memory_restart`, which restarts a worker whose resident memory crosses a threshold.
  • An ecosystem file, so all of the above is in the repository rather than in somebody's shell history.
ecosystem.config.js
module.exports = {  apps: [    {      name: "api",      script: "dist/server.js",      instances: 2,      exec_mode: "cluster",      max_memory_restart: "300M",      env: { NODE_ENV: "production" },    },  ],};

None of that is bad software. The question is only whether anything underneath it is already doing the same work.

What a panel already does#

A panel-based host runs your application in its own container, and the container is itself supervised. On RE:NODE that means:

  • Start, stop, restart and kill from the console, and the container is restarted when the process exits.
  • A memory limit enforced by the kernel. At the limit the container is stopped and restarted clean rather than being allowed to swap, which is the opposite of upstream Pterodactyl's behaviour and much easier to reason about: you never have a server that is technically alive and functionally frozen.
  • A hard CPU share. The plan's figure is a percentage of one core, so 250 is 2.5 vCPU, and it is a ceiling rather than a target. A process at 100% is slow, not broken, and is never suspended for it.
  • Crash detection. A watcher polls every two minutes for a server that went offline or whose uptime went backwards. Restarts you asked for are not counted. Three unexpected restarts in an hour puts a warning on the server page and opens a ticket automatically; six suspends the server so a crash loop cannot run all weekend.
  • Live console output, unfiltered, with a command line, plus graphs of memory, CPU and disk against the plan's limits.
  • Schedules on a cron expression, running ordered tasks with delays: a console command, a backup, a power action.
  • Git deploys from GitHub, with two switches - pull on every start, and redeploy on push, which restarts only a server that was already running - and one record per deploy.

Read that list next to the PM2 one and the overlap is most of both.

The overlap, line by line#

JobPM2Panel
Restart after a crashYes, inside the containerYes, restarts the container
Start after a rebootpm2 startup plus systemdYes, by default
Memory ceilingmax_memory_restart, advisoryKernel limit on the container
CPU ceilingNoHard throttle to the plan's share
LogsFiles in ~/.pm2/logsLive console, unfiltered
Several processesCluster modeOne server per process, or &
Zero-downtime reloadpm2 reload in cluster modeNo
Scheduled restartscron_restartSchedules tab
Alert on a crash loopNoWarning and an automatic ticket
Deploy from gitpm2 deploy, rarely usedGitHub app, pull or push

Two rows are worth pausing on. PM2 is the only column with a zero-downtime reload, and that is a real thing it offers. And the panel is the only column that tells somebody when the app is crash-looping, which is the thing that matters at four in the morning.

Why a supervisor inside a supervisor hides problems#

start, stoprestartexit 1Panelwatches the containerContainermemory and CPU limitPM2 daemonrestarts on exitYour appcrashing every 20s
Two supervisors, one of which cannot see the crash

Follow a crash up that diagram. Your app throws on an unhandled promise rejection and exits. PM2 notices and starts it again. The container never exited, so from the platform's point of view nothing happened: uptime is unbroken, no restart is recorded, the crash watcher counts zero, and the ticket that would have told you never opens. Your memory graph is a flat line made of dozens of short-lived processes.

This is the whole argument, and the rest are corollaries:

  • The exit code is swallowed. A process that exits 1 because a required environment variable is missing is indistinguishable, from outside, from one that is running fine.
  • Memory accounting gets confusing. The container's limit applies to the daemon plus every worker together. Set max_memory_restart to 512M on a 1 GB plan with two workers and the kernel stops the whole container before PM2's own threshold is ever reached - one policy you configured, one policy that actually fires, and the one that fires is not yours.
  • Signals get an extra hop. A panel stop signals the container's main process. If that is a shell, or a PM2 CLI invocation that has already returned, your app may never receive the signal and gets killed after the grace period instead of shutting down cleanly. Graceful shutdown and health checks explains why that costs you in-flight requests.
  • The logs move somewhere awkward. PM2 writes to files under its home directory, not to standard output, so the live console shows the daemon's own chatter rather than your application's. You end up reading logs over SFTP, which is a strange way to watch a deploy.
  • The daemon costs memory. Tens of megabytes, which is nothing on 8 GB and noticeable on 1 GB.

If you do run PM2 in a container, use pm2-runtime#

Sometimes PM2 stays for a reason - an ecosystem file that encodes real configuration, or a team that knows its commands. In that case there is exactly one correct entry point:

bash
$ pm2-runtime start ecosystem.config.js --env production

pm2-runtime is the container-shaped version of the CLI. It stays in the foreground, streams application logs to standard output so the console can show them, and exits when the apps do, which lets the platform see failures. Starting with plain pm2 start in a container is the classic mistake: the CLI hands the app to a daemon and returns, the main process exits, and the container stops with your app apparently running.

Two settings to check while you are there. PM2 sends SIGINT to your app by default, not SIGTERM, so an app whose shutdown handler only listens for SIGTERM will never run it - set kill_signal or handle both. And kill_timeout is how long PM2 waits before sending SIGKILL; it needs to be longer than your longest in-flight request, not shorter.

Cluster mode: when more processes help, and when they do not#

Cluster mode is PM2's strongest feature and the most frequently misapplied. Node runs your JavaScript on one thread, so a second process can use a second core. The question is whether you bought a second core.

Plan CPU shareUseful Node processes
0.5 core1
1 core1
1.5-2 cores2
3 cores2-3

instances: "max" asks for one worker per logical CPU the machine reports, which on a shared node is the host's core count, not your share. On a container throttled to half a core that can mean a dozen workers dividing the same throttled slice and each holding its own copy of your dependency tree in memory. The result is less throughput and much more memory, which is the exact shape of the "we added workers and it got slower" report.

Two more consequences of cluster mode worth knowing before you enable it:

  • Sticky sessions. Anything stateful in the process - a WebSocket connection tracked in a Map, an in-memory session store, Socket.IO's default transport upgrade - breaks when consecutive requests land on different workers. See websockets behind a reverse proxy for the routing side.
  • Anything that must run once. A cron job, a queue consumer or a migration inside your app now runs once per worker. That is how a nightly email gets sent four times.

If what you actually want is to use the CPU you have, size the plan for it rather than multiplying processes inside a plan that cannot feed them. Node memory limits explained covers the other half of that sizing question.

Doing without it: the start command and the logs#

Replacing PM2 on a panel is mostly deletion. The start command runs your process in the foreground, in the container, as the main process:

bash
$ npm ci --omit=dev && node dist/server.js

Use npm ci rather than npm install so the lockfile decides what gets installed - npm ci vs npm install has the difference and why it matters more on a server than on a laptop. Logging goes to standard output and standard error, which is where the console reads from; there is no log file to rotate and nothing to purge when the disk fills. Reading the console covers what the panel does with that stream.

For the features you lose, the replacements are:

  1. Restart on crash - the container does it. Make sure the process really exits on a fatal error rather than hanging, or nothing will restart it.
  2. Restart on a schedule - a power action on the Schedules tab with a cron expression, which also means the restart is recorded as deliberate and not counted by the crash watcher.
  3. Memory limit - the plan's limit, enforced by the kernel. If you want Node itself to fail earlier and more predictably, set --max-old-space-size below the container limit.
  4. Several processes - a second server, which is cleaner, or & in the start command, which is cheaper and gives you no supervision of the backgrounded process. Background jobs on a small server weighs those up.
  5. Zero-downtime reload - honestly, you lose this. A single-container deploy has a gap of a few seconds while the process restarts, and pretending otherwise is how people end up with two supervisors again. Zero-downtime deploys on a small server is clear about what is and is not possible on one box.

The deploy itself comes from GitHub: pull on start, or redeploy on push, with each deploy recorded so you can tell one that shipped from one that fell over. Deploy a Node app from GitHub is the five-minute version of that setup.

Where a process manager is still right#

On a VDS or a dedicated machine, nothing supervises anything until you make it. There you need something, and the two candidates are PM2 and systemd.

systemd is already installed, starts at boot without a helper, captures logs to the journal, and restarts on failure with a backoff you configure:

/etc/systemd/system/api.service
[Service]User=appWorkingDirectory=/srv/apiExecStart=/usr/bin/node dist/server.jsRestart=alwaysRestartSec=5Environment=NODE_ENV=production[Install]WantedBy=multi-user.target

journalctl -u api -f replaces pm2 logs, and systemctl restart api replaces pm2 restart. No extra daemon, no pm2 save to forget, and the same mechanism supervises everything else on the box. Systemd services for your apps covers unit files properly.

PM2 earns its place on a VDS when you want cluster mode with rolling reloads, when you are running several small Node apps and prefer one CLI for all of them, or when pm2 monit is genuinely how your team works. Those are real reasons. "It is what the tutorial said" is not.

Migrating off PM2 in ten minutes#

  1. Read ecosystem.config.js and write down anything that is real configuration: environment variables, the script path, the instance count, max_memory_restart.
  2. Move the environment variables to the panel's Startup tab, or to your own environment file if you are on a VDS.
  3. Set the start command to the script directly, in the foreground: node dist/server.js, not pm2 start.
  4. Confirm the app writes to standard output. If it was configured to log to a file because PM2 captured stdout anyway, change it back.
  5. Add a SIGTERM handler that closes the server and finishes in-flight requests, and check the panel's stop is clean.
  6. If you used cron_restart, recreate it as a scheduled power action.
  7. Delete PM2 from package.json and check nothing else shells out to it.
  8. Deploy, then deliberately crash the app and watch what happens - the container should restart and the restart should be visible. If it is not, something is still swallowing the exit.

Step eight is the one people skip, and it is the only one that proves the rest worked. Monitoring that tells you something makes the same argument about alerts nobody has ever seen fire.

FAQ#

Does PM2 work on a hosting panel at all?

Yes, if you start it with pm2-runtime so it stays in the foreground and streams logs to standard output. It works; it just duplicates supervision the container already has, and it hides crashes from the platform's crash detection. Plain pm2 start as a start command does not work, because the CLI returns and the container stops.

Do I need cluster mode?

Only if you have more than one core of CPU share and your app is CPU-bound in JavaScript. Most small Node services are waiting on a database or an upstream API, where a second process adds memory and no throughput. Check the CPU graph before adding workers.

What replaces pm2 logs?

The console, if the app writes to standard output. That is also the only place a panel can show you output in real time, which is a good reason to stop writing application logs to files on a single-container host.

Is PM2 a bad tool?

No. It is a well-built process manager solving a problem that exists on unmanaged machines. The mistake is not PM2, it is running two supervisors and expecting the outer one to know what the inner one is doing.

Does the panel give me zero-downtime deploys?

No, and neither does anything else running one copy of your app on one server. A restart is a gap of a few seconds. Real zero-downtime needs two instances and something in front of them, which is a bigger architecture than a single application plan.

What about forever, nodemon or supervisor?

nodemon is a development tool that restarts on file changes and should never be in a production start command. forever is largely superseded. The reasoning above applies to all of them: if the platform restarts your container, a second restarter adds nothing except a place for failures to hide.


Comments

Completely anonymous: no account, no email, no cookie. We store the name you type, the text and the time - nothing else. Links are limited and markup is not rendered.

0/2000