Back to Scaling Node.js Applications guides

Running Node.js Apps with PM2 (Complete Guide)

Ayooluwa Isaiah
Updated on April 29, 2024

PM2 is a renowned open-source process manager tailored for Node.js applications. It acts as a guardian, streamlining deployment, overseeing logs, monitoring resources, and ensuring minimal downtime for every application it manages.

This article will introduce you to the key features of PM2 and help you leverage it for deploying, overseeing, and scaling your Node.js applications effectively in a production environment.

Prerequisites

Before proceeding with this tutorial, ensure that you have a recent version of Node.js and npm installed on your machine.

Side note: Get alerted when your Node.js app goes down

Head over to Better Stack and start monitoring your endpoints in 2 minutes.

Better Uptime – Monitors (Dark Mode).png

Step 1 β€” Setting up the demo application (optional)

In this guide, we'll explore various functionalities of PM2 using a sample Node.js application. If you have your project, you're welcome to use it as you follow along. If not, let's get started by cloning the demo app repository:

 
git clone https://github.com/betterstack-community/dadjokes

Once cloned, navigate into the project directory and set up its required dependencies:

 
cd dadjokes && npm install

This application fetches a humorous dad joke from the ICanHazdadjokes API and displays it on a webpage. To see it in action, initiate the server using:

 
node server.js
Output
dadjokes server started on port: 3000

With the server up and running, open your browser and head to http://localhost:3000. Here's what you can expect to see:

Screenshot of dadjokes application in Brave

Having set up the demo app, we'll transition to installing PM2's npm package in the next section.

Step 2 β€” Installing PM2

To harness the power of PM2 for managing your Node.js applications, you'll first need to install its npm package as follows:

 
npm install -g pm2

Go ahead and confirm the version you have installed through the following command:

 
pm2 --version

Here's the expected output:

Output
5.2.0

Interestingly, your PM2 installation actually provides four distinct executables:

  • pm2: The primary PM2 binary.
  • pm2-dev: A development tool akin to nodemon that auto-restarts the Node.js process during development.
  • pm2-runtime: A seamless alternative to the node command, tailored for containerized environments.
  • pm2-docker: Essentially an alias for pm2-runtime.

In the sections ahead, we'll delve into the specific applications of each of these executables. But for now, let's focus on establishing a foundational development workflow using the pm2-dev binary in the next step.

Step 3 β€” Launching your application in development mode

While PM2 is primarily renowned for its usefulness in production, it can also serve you during the development phase through the pm2-dev binary. When you kickstart your application using pm2-dev, it monitors the current directory. Your application gets a swift restart if there are any changes in the directory or its subdirectories.

To set things in motion, first ensure that any previously running instance of your server is terminated with Ctrl-C. Then launch your server in development mode:

 
pm2-dev server.js

You should observe the following output:

Output
===============================================================================
--- PM2 development mode ------------------------------------------------------
Apps started         : server
Processes started    : 1
Watch and Restart    : Enabled
Ignored folder       : node_modules
===============================================================================
server-0  | dadjokes server started on port: 3000

At this point, changing any file in the dadjokes directory will cause the server to restart. Try it out by modifying your server.js file as shown below. Change the title property from 'Random Jokes' to 'Random Dad Jokes' and save the file:

server.js
. . .
app.get('/', async (req, res, next) => {
  try {
    const response = await getRandomJoke();

    res.render('home', {
title: 'Random Dad Jokes',
dadjokes: response, }); } catch (err) { next(err); } }); . . .

Now, shift your attention to the terminal where your development server is active. You'll notice an output indicating PM2's awareness of the change and its subsequent action to restart the server:

Output
. . .
[rundev] App server restarted
server-0  | dadjokes server started on port: 3000

For a deeper dive into pm2-dev and its capabilities, use the --help flag. It'll unveil the available options to tailor pm2-dev to your needs. Up next, we'll transition to the core features of the primary pm2 binary.

Step 4 β€” Starting your application in production

When it's time to shift from development to production, the pm2 start command is one to use. First, ensure you've terminated any running development server with Ctrl-C. Then, to launch your application in a production setting, use:

 
pm2 start --name 'dadjokes' server.js

You should see the following output:

Output
[PM2] Starting /home/ayo/dev/demo/dadjokes/server.js in fork_mode (1 instance)
[PM2] Done.
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name        β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0   β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ fork    β”‚ 1658     β”‚ 0s     β”‚ 0    β”‚ online    β”‚ 0%       β”‚ 18.3mb   β”‚ ayo      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This table confirms the successful background launch of the dadjokes application. If your terminal window is compact, you might see fewer columns, so consider expanding it for a comprehensive view.

Here's a breakdown of the table columns:

  • id: A unique identifier assigned by pm2 to the application.
  • name: The application's name. If the --name flag isn't specified, it defaults to the entry file name (in this case, server.js).
  • namespace: Allows for batch operations (like start/stop/restart) on a group of apps.
  • version: The version number sourced from the app's package.json.
  • mode: Can be either fork or cluster. It directs pm2 to utilize the child_process.fork or the cluster API.
  • pid: The process's unique identifier.
  • uptime: Duration since the application's launch. β†Ί: Counts the number of application restarts. status: Current application status. cpu: Percentage of CPU usage. mem: Memory consumption. user: The username of the individual who initiated the application. watching: Indicates if pm2 is set to auto-restart the app upon detecting file changes in the directory or its subdirectories. This is controlled by the --watch option.

At this point, you can visit http://localhost:3000 in your browser and everything should work just fine as before.

Ensuring a graceful startup

If you need your application to connect to various services (such as databases, caches, etc) on start up, you can use the --wait-ready flag which instructs PM2 to await a ready signal from your application before marking it as "online". Here's how to implement this:

server.js
. . .

const server = app.listen(process.env.PORT || 3000, () => {
  console.log(`dadjokes server started on port: ${server.address().port}`);
// simulate a ready application after 1 second
setTimeout(function () {
process.send('ready');
}, 1000);
});

After integrating the highlighted lines into your application, you'll need to delete the current instance and restart it, this time using the --wait-ready flag:

 
pm2 delete dadjokes
 
pm2 start --name 'dadjokes' server.js --wait-ready
Output
[PM2] Starting /home/ayo/dev/demo/dadjokes/server.js in fork_mode (1 instance)
[PM2] Done.
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name        β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0   β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ fork    β”‚ 18529    β”‚ 1s     β”‚ 0    β”‚ online    β”‚ 0%       β”‚ 57.4mb   β”‚ ayo      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

By default, if the ready signal isn't received, PM2 will wait for three seconds before designating your application as online. If you wish to adjust this duration, you can use the --listen-timeout flag followed by the desired timeout in milliseconds:

 
pm2 start --name 'dadjokes' server.js --wait-ready --listen-timeout 10000

Up next, we'll delve into how you can monitor your application's resource consumption and other pertinent metrics using PM2.

Step 5 β€” Keeping tabs on application metrics

With your application in motion, PM2 offers a suite of subcommandsβ€”list, show, and monitβ€”to help you monitor its performance. To get an overview of all active applications on your server, use:

 
pm2 list

You should observe the following output:

Output
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name        β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0   β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ fork    β”‚ 18529    β”‚ 3m     β”‚ 0    β”‚ online    β”‚ 0%       β”‚ 53.3mb   β”‚ ayo      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This command provides a snapshot of each application's status, uptime, memory usage, and more. If you have multiple applications running, you can sort them based on specific metrics:

 
pm2 list --sort [name|id|pid|memory|cpu|status|uptime][:asc|desc]

As in:

 
pm2 list --sort memory:desc

For a more detailed look at a specific application, use the pm2 show command followed by the application's name or id:

 
pm2 show dadjokes

[s/t]

For a live dashboard displaying metrics, metadata, and application logs, use:

 
pm2 monit

Screenshot of pm2 monit in action

πŸ”­ Want to get alerted when your Node.js app stops working?

Head over to Better Uptime start monitoring your endpoints in 2 minutes

Step 6 β€” Ensuring application uptime with PM2

PM2's auto-restart capability is a boon for ensuring application uptime. You can also restart your application manually through the pm2 restart command as follows:

 
pm2 restart dadjokes
Output
[PM2] Applying action restartProcessId on app [dadjokes](ids: [ 0 ])
[PM2] [dadjokes](0) βœ“
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name        β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0   β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ fork    β”‚ 4873     β”‚ 1s     β”‚ 1    β”‚ online    β”‚ 0%       β”‚ 52.6mb   β”‚ ayo      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Notice that the restart column (β†Ί) has been incremented to 1, indicating that the application has been restarted once.

PM2's default behavior is to restart your application even if it exits intentionally. For instance, triggering the /graceful-shutdown route in the dadjokes application will gracefully shut it down by calling process.exit(0):

 
curl http://localhost:3000/graceful-shutdown
Output
Graceful shutdown!

On the other hand, accessing the /crashme route simulates an application crash:

 
curl http://localhost:3000/crashme
Output
Crashing server!

After these actions, executing the pm2 list command will show the restart count has risen to 3, yet the application status remains online:

 
pm2 list
Output
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name        β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0   β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ fork    β”‚ 10008    β”‚ 2s     β”‚ 3    β”‚ online    β”‚ 0%       β”‚ 53.5mb   β”‚ ayo      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Currently, PM2 doesn't offer the flexibility to adjust restart behavior based on an application's exit code. Although the --stop-exit-codes flag is documented, it seems non-functional in the recent versions. This limitation is being tracked in an open GitHub issue.

 
pm2 start --stop-exit-codes 0 --name 'dadjokes' server.js
Output
error: unknown option `--stop-exit-codes'

In the next step, you will configure other auto-restart strategies through the PM2 configuration file.

Step 7 β€” Fine-tuning auto-restart strategies

PM2 employs an ecosystem.config.js file to consolidate configuration settings for one or more applications. To generate this file in your project directory, use:

 
pm2 init simple

Upon execution, you should see:

Output
File /home/ayo/dev/demo/dadjokes/ecosystem.config.js generated

Edit the ecosystem.config.js file and update the name and script fields to match your application:

ecosystem.config.js
module.exports = {
  apps: [{
name: 'dadjokes',
script: './server.js',
}] }

With this configuration, you can manage all declared applications:

 
pm2 start ecosystem.config.js
 
pm2 restart ecosystem.config.js
 
pm2 stop ecosystem.config.js

Let's now delve into advanced restart strategies:

Restarting based on memory usage

PM2 can restart an application upon reaching a memory limit, preventing potential "heap out of memory errors". Configure this using the max_memory_restart option:

ecosystem.config.js
module.exports = {
  apps: [{
    name: 'dadjokes',
    script: './server.js',
max_memory_restart: '1G',
}] }

This restarts the dadjokes application if memory usage surpasses 1 Gigabyte. You can also configure the max_memory_restart option in Kilobyte (K), and Megabyte (M).

Restarting based on a CRON schedule

PM2 can restart applications based on a cron schedule. For daily restarts:

ecosystem.config.js
module.exports = {
  apps: [{
    name: 'dadjokes',
    script: './server.js',
cron_restart: '0 */24 * * *',
}] }

To learn and test the cron syntax, consider using the crontab guru editor.

Delayed restarts

You can introduce a delay before PM2 restarts an application using the restart_delay option:

ecosystem.config.js
module.exports = {
  apps: [{
    name: 'dadjokes',
    script: './server.js',
restart_delay: 5000 // wait for five seconds before restarting
}] }

Exponential backoff restart delay

Instead of setting a fixed delay before restarting the application, you can use the exp_backoff_restart_delay option to to raise the time between restarts up to 15 seconds incrementally. The initial delay time set through this option will be multiplied by 1.5 after each restart attempt.

ecosystem.config.js
module.exports = {
  apps: [{
    name: 'dadjokes',
    script: './server.js',
exp_backoff_restart_delay: 100 // 100ms
}] }

With the above in place, the first restart attempt will be delayed by 100ms, second restart 150ms, then 225ms, 337.5ms, 506.25ms, and so on. This delay is reset to 0ms if the application remains online for over 30 seconds.

Setting a maximum restart limit

PM2 provides a max_restarts option for configuring the maximum number of unstable restarts before the application is considered to have encountered an unrecoverable error. This lets you prevent your application from constantly dying and restarting, which may waste resources.

You can also specify an unstable restart through the min_uptime option. This allows you to specify the amount of time before your application is considered "online":

ecosystem.config.js
module.exports = {
  apps: [{
    name: 'dadjokes',
    script: './server.js',
max_restarts: 16,
min_uptime: 5000, // 5 seconds
}] }

Once an application has reached the maximum number of restarts, it will display an errored status. Your intervention will be required before the app can be up and running once again.

 
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name           β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0   β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ fork    β”‚ 0        β”‚ 0      β”‚ 15   β”‚ errored   β”‚ 0%       β”‚ 0b       β”‚ ayo      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Disabling automatic restarts

To turn off automatic restarts entirely, set the autorestart option to false:

ecosystem.config.js
module.exports = {
  apps: [{
    name: 'dadjokes',
    script: './server.js',
autorestart: false,
}] }

Now, go ahead and integrate these strategies into your ecosystem.config.js:

ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'dadjokes',
      script: './server.js',
exp_backoff_restart_delay: 100,
max_memory_restart: '1G',
max_restarts: 10,
min_uptime: 2000,
}, ], };

Remove the existing dadjokes instance:

 
pm2 delete dadjokes

Then, relaunch it using the configuration file:

 
pm2 start ecosystem.config.js

In the next section, you will discover how to start your Node.js application automatically even after a system reboot.

Step 8 β€” Launching applications on system startup

In the last section, we explored methods to keep your application resilient against unexpected crashes. In this segment, we'll delve into how to set up a Node.js application to autostart after system reboots or crashes using PM2 and Systemd on Linux. Although PM2 is compatible with various init systems, including upstart, launchd, openrc, rcd, and systemv, this guide focuses on Systemd.

Begin by generating an init script for your system. This script will initiate the pm2 daemon during system startup, which in turn will launch any saved applications:

 
pm2 startup systemd

The output should resemble:

Output
[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/home/<username>/.local/share/nvm/v20.5.0/bin /home/<username>/.local/share/nvm/v20.5.0/lib/node_modules/pm2/bin/pm2 startup systemd -u <username> --hp /home/<username>

Go ahead and run the generated command shown above to configure the Systemd startup script:

 
sudo env PATH=$PATH:/home/<username>/.local/share/nvm/v16.14.0/bin /home/<username>/.local/share/nvm/v16.14.0/lib/node_modules/pm2/bin/pm2 startup systemd -u <username> --hp /home/<username>

A successful command will yield:

Output
[PM2] Init System found: systemd

. . .

[PM2] [v] Command successfully executed.
+---------------------------------------+
[PM2] Freeze a process list on reboot via:
$ pm2 save

[PM2] Remove init script via:
$ pm2 unstartup systemd

If you encounter an error, it might indicate that the pm2 service file was created but couldn't be enabled. In such cases, manually enable the service:

 
sudo systemctl enable pm2-<username>

To ensure that PM2 automatically starts any saved processes upon system boot, save your PM2 process list:

 
pm2 save
Output
pm2 save
[PM2] Saving current process list...
[PM2] Successfully saved in /home/<username>/.pm2/dump.pm2

Now, PM2 is set to launch saved processes on system boot automatically, and you can update this list anytime with pm2 save. Test this by rebooting your system and running pm2 list afterward. If any issues arise, use the following command to launch all applications:

 
pm2 resurrect

To deactivate the startup script, employ the pm2 unstartup command. It's a good practice to do this whenever you upgrade Node.js, ensuring that the subsequent pm2 startup uses the updated Node.js binary.

If you need to disable the startup script, use the pm2 unstartup command. You should always do this after upgrading your Node.js version so that when you run pm2 startup again, it'll generate a startup configuration that uses the latest Node.js binary.

At this point, you've learned all the ways PM2 ensures your application's resilience against external disruptions like system reboots. For continuous monitoring of your application's online status, consider implementing uptime monitoring checks for timely notifications on server availability and other related issues.

Step 9 β€” Stopping and deleting your application

PM2 offers the stop and delete commands to halt an application and expunge it from the process list, respectively. These commands can target specific applications by their names, ids, namespaces, configuration files, or you can use the all keyword to target every application.

To halt all applications defined in the ecosystem.config.js file, use:

 
pm2 stop ecosystem.config.js
Output
[PM2] [dadjokes](0) βœ“
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name        β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0   β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ fork    β”‚ 0        β”‚ 0      β”‚ 3    β”‚ stopped   β”‚ 0%       β”‚ 0b       β”‚ ayo      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€οΏ½οΏ½οΏ½β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When you pass the all keyword, it causes the delete subcommand to act on all the applications in the list:

 
pm2 delete all
Output
[PM2] Applying action deleteProcessId on app [all](ids: [ 0 ])
[PM2] [dadjokes](0) βœ“
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name      β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

After deleting your application, you may need to run pm2 save to update process list dump file.

Ensuring a graceful shutdown

To guarantee a smooth shutdown when employing the restart, stop, reload, or delete commands, it's crucial to catch the SIGINT or SIGTERM signals. This ensures that any essential cleanup tasks are executed before the application terminates, such as processing all current requests and releasing resources like files and database connections.

Below is a sample that stops the server from accepting new connections when the SIGINT and SIGTERM signals are detected, and then terminates the process:

server.js
. . .

function cleanupAndExit() {
  server.close(() => {
    console.log('dadjokes server closed');
    process.exit(0);
  });
}

process.on('SIGTERM', cleanupAndExit);
process.on('SIGINT', cleanupAndExit);

Step 10 β€” Managing your application logs

PM2 automatically archives logs generated by your application in the $HOME/.pm2/logs directory. Logs directed to the standard output are stored in the <app_name>-out.log file, while logs directed to the standard error are stored in the <app_name>-error.log file.

 
ls ~/.pm2/logs
Output
dadjokes-error.log  dadjokes-out.log

Within the ecosystem.config.js file, you can define the error_file and out_file options to specify custom locations for the application's error and output log files:

ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'dadjokes',
      script: './server.js',
      exp_backoff_restart_delay: 100,
      max_memory_restart: '1G',
      max_restarts: 10,
      min_uptime: 2000,
out_file: '<custom_path>', // use /dev/null to disable
error_file: '<custom_path>', // use /dev/null to disable
}, ], };

Enabling log rotation

To ensure that your application log files are rotated before they get too large, install the pm2-logrotate module as shown below:

 
pm2 install pm2-logrotate

Afterward, when you run pm2 list, you should see a new "Module" section like so:

Output
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  β”‚ name             β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0   β”‚ dadjokes         β”‚ default     β”‚ 1.0.0   β”‚ fork    β”‚ 9408     β”‚ 3m     β”‚ 0    β”‚ online    β”‚ 0%       β”‚ 50.2mb   β”‚ ayo      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Module
β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id β”‚ module                       β”‚ version       β”‚ pid      β”‚ status   β”‚ β†Ί    β”‚ cpu      β”‚ mem      β”‚ user     β”‚
β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 1  β”‚ pm2-logrotate                β”‚ 2.7.0         β”‚ 12068    β”‚ online   β”‚ 0    β”‚ 0%       β”‚ 21.4mb   β”‚ ayo      β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

By default, the pm2-logrotate module rotates log files once they exceed 10 megabytes. It retains up to 30 rotated files and deletes older ones. Backup files are not compressed by default. You can adjust these settings and more by referring to the module's documentation, or you use logrotate to handle log file rotation instead.

The pm2 logs command may come in handy in development for displaying incoming application log entries in real time:

 
pm2 logs dadjokes
Output
PM2        | 2022-03-14T09:18:30: PM2 log: App [dadjokes:0] starting in -fork mode-
PM2        | 2022-03-14T09:18:30: PM2 log: App [dadjokes:0] online
PM2        | 2022-03-14T09:23:18: PM2 log: Stopping app:dadjokes id:0
PM2        | 2022-03-14T09:23:18: PM2 log: App [dadjokes:0] exited with code [0] via signal [SIGINT]
PM2        | 2022-03-14T09:23:18: PM2 log: App [dadjokes:0] will restart in 100ms
PM2        | 2022-03-14T09:23:18: PM2 log: pid=21869 msg=process killed
PM2        | 2022-03-14T09:23:38: PM2 log: [PM2][WORKER] Reset the restart delay, as app dadjokes has been up for more than 30000ms

PM2      | App [dadjokes:0] starting in -fork mode-
PM2      | App [dadjokes:0] online
0|dadjokes  | Random Joke server started on port: 3000
0|dadjokes  | GET /joke 200 97 - 760.323 ms
0|dadjokes  | GET /joke 200 92 - 750.802 ms
0|dadjokes  | GET /joke 200 117 - 715.876 ms

To explore all available options for the logs command and customize its output, use:

 
pm2 logs --help

Step 11 β€” Clustering with PM2

PM2 offers a cluster mode, enabling networked Node.js applications to harness the full power of the host's CPUs without any code changes. This results in launching multiple application instances (based on your machine's CPU count) on the same port, effectively distributing incoming workloads among them and enhancing performance.

Internally, PM2 relies on Node.js's cluster module to spawn child processes that share the server port. Your application should be stateless to maximize the benefits of PM2's cluster mode. This ensures that any reference to past transactions is managed through a shared, stateful medium like a database or cache.

To initiate your application in cluster mode, first remove it from the process list. Then, specify the number of workers via the instances option and set the exec_mode option to cluster:

ecosystem.config.js
module.exports = {
  apps: [
    {
      . . .
instances: 'max',
exec_mode : 'cluster',
}, ], };

Then, use the start command with the -i flag:

 
pm2 start ecosystem.config.js -i max

Here, max instructs PM2 to spawn as many workers as there are CPU cores. While you can specify a particular number, it's not recommended to exceed the number of CPU cores, as it can lead to resource contention and performance issues.

A common practice is to spawn one worker less than the total CPU cores. If your server has four cores, you'd typically spawn three workers. Achieve this in PM2 by setting instances to -1:

ecosystem.config.js
module.exports = {
  apps: [
    {
      . . .
instances: -1,
exec_mode : 'cluster', }, ], };

Starting the application will yield:

 
[PM2] App [dadjokes] launched (3 instances)
β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id β”‚ name        β”‚ namespace   β”‚ version β”‚ mode    β”‚ pid      β”‚ uptime β”‚ β†Ί    β”‚ status    β”‚ cpu      β”‚ mem      β”‚ user     β”‚ watching β”‚
β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 0  β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ cluster β”‚ 1000013  β”‚ 0s     β”‚ 0    β”‚ online    β”‚ 0%       β”‚ 51.9mb   β”‚ ayo      β”‚ disabled β”‚
β”‚ 1  β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ cluster β”‚ 1000023  β”‚ 0s     β”‚ 0    β”‚ online    β”‚ 0%       β”‚ 48.1mb   β”‚ ayo      β”‚ disabled β”‚
β”‚ 2  β”‚ dadjokes    β”‚ default     β”‚ 1.0.0   β”‚ cluster β”‚ 1000035  β”‚ 0s     β”‚ 0    β”‚ online    β”‚ 0%       β”‚ 40.3mb   β”‚ ayo      β”‚ disabled β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

You'll notice that the application launches in cluster mode, not fork as before. The number of workers corresponds to your machine's CPU cores.

Using cluster mode ensures balanced request handling across workers. The NODE_APP_INSTANCE environment variable can distinguish between processes, handy for operations exclusive to one process.

Modify the /joke endpoint in your server.js file as shown below:

server.js
app.get('/joke', async (req, res, next) => {
  console.log('Request handled by process:', process.env.NODE_APP_INSTANCE);

  if (process.env.NODE_APP_INSTANCE === '0') {
    console.log('Executing some operation on process 0 only...');
  }

  try {
    const response = await getRandomJoke();
    res.json(response);
  } catch (err) {
    next(err);
  }
});

Restart the application through the command below:

 
pm2 restart ecosystem.server.js

Afterward, send a handful of requests to the /joke endpoint and view the logs:

 
pm2 logs dadjokes
Output
0|dadjokes  | Request handled by process: 0
0|dadjokes  | Executing some operation on process 0 only...
1|dadjokes  | Request handled by process: 1
2|dadjokes  | Request handled by process: 2
0|dadjokes  | Request handled by process: 0
0|dadjokes  | Executing some operation on process 0 only...
1|dadjokes  | Request handled by process: 1
0|dadjokes  | GET /joke 200 106 - 1935.223 ms
2|dadjokes  | Request handled by process: 2
0|dadjokes  | GET /joke 200 160 - 1137.809 ms
0|dadjokes  | Request handled by process: 0
0|dadjokes  | Executing some operation on process 0 only...
1|dadjokes  | Request handled by process: 1
2|dadjokes  | GET /joke 200 171 - 1083.629 ms
2|dadjokes  | GET /joke 200 100 - 2079.859 ms
2|dadjokes  | Request handled by process: 2
0|dadjokes  | Request handled by process: 0
0|dadjokes  | Executing some operation on process 0 only...
0|dadjokes  | GET /joke 200 119 - 1070.117 ms
2|dadjokes  | GET /joke 200 130 - 1073.907 ms
1|dadjokes  | GET /joke 200 142 - 6049.548 ms
1|dadjokes  | GET /joke 200 111 - 3412.896 ms
1|dadjokes  | GET /joke 200 148 - 5938.372 ms
0|dadjokes  | GET /joke 200 111 - 5827.303 ms

You'll observe that a number prefixes each log entry. This number is the id for each worker process, and it can be accessed in the application code via process.env.NODE_APP_INSTANCE as demonstrated earlier. Notice how we can execute some code on process 0 only even though requests were sent to all three worker instances (on our test machine, could be more or less on yours).

Screenshot of pm2 load balancing

An advantage of cluster mode is zero-downtime reloads in production using pm2 reload. This restarts processes sequentially, ensuring continuous availability. Conversely, pm2 restart halts and restarts all processes simultaneously, causing brief downtime.

PM2 also offers dynamic scaling with the scale command. For instance, to add two workers:

 
pm2 scale dadjokes +2

Or, to set a specific number of workers:

 
pm2 scale dadjokes 2

This flexibility ensures your application can adapt to varying workloads quickly and efficiently.

Step 12 β€” Deploying your application to production

PM2 provides an integrated deployment system to facilitate deploying your application to one or multiple remote servers. To set this up, modify your ecosystem.config.js file as follows:

ecosystem.config.js
module.exports = {
  apps: [],
deploy: {
production: {
user: '<your_remote_server_username>',
host: [<your_remote_server_ip>],
ref: 'origin/master',
repo: '<your_git_repo_url>',
path: '/home/<your_server_username>/dadjokes',
'post-setup': 'npm install',
'post-deploy': 'pm2 startOrRestart ecosystem.config.js --env production',
},
},
};

Here's a breakdown of the production object properties:

  • user: The username for authentication on the remote server.
  • host: An array of IP addresses of the remote servers.
  • ref: The git branch and remote to deploy, e.g., origin/master.
  • repo: The git repository's remote URL (HTTPS or SSH).
  • path: The directory on the remote server where the repository will be cloned.
  • post-setup: Commands or scripts to run post cloning.
  • post-deploy: Commands or scripts to run after deployment.

Before deploying to the specified host servers, ensure each has PM2 installed and the necessary permissions to clone the Git repository (e.g., the correct SSH key setup). Once set up, initiate the server provisioning with:

 
pm2 deploy production setup

Here's the output to expect if everything goes well:

Output
--> Deploying to production environment
--> on host <your_remote_server_ip>
  β—‹ hook pre-setup
  β—‹ running setup
  β—‹ cloning https://github.com/betterstack-community/dadjokes
  β—‹ full fetch
Cloning into '/home/ayo/dadjokes/source'...
  β—‹ hook post-setup
  β—‹ setup complete
--> Success

If you encounter an error, it's likely SSH-related, preventing PM2 from accessing the remote server or Git repository. Start your investigation by ensuring that the git clone <your_git_repo_url> command works on the remote server. For troubleshooting PM2 deployments, refer to this guide.

After successful provisioning, deploy the application:

 
pm2 deploy production
 
pm2 deploy production --force # if there are local changes

A successful deployment will show:

Output
--> Deploying to production environment
--> on host <your_git_repo_url>

  β—‹ deploying origin/prod
  β—‹ executing pre-deploy-local
  β—‹ hook pre-deploy
  β—‹ fetching updates
  β—‹ full fetch
Fetching origin
  β—‹ resetting HEAD to origin/prod
HEAD is now at 4c31583 Add pm2 config file
  β—‹ executing post-deploy `pm2 startOrRestart ecosystem.config.js --env production`
[PM2][WARN] Applications dadjokes not running, starting...
[PM2][WARN] Environment [production] is not defined in process file
[PM2] App [dadjokes] launched (1 instances)
. . .
--> Success

Logging into your remote server and executing pm2 list will confirm the application's deployment and operational status. Additionally, you can run commands on each remote server without logging in using the pm2 deploy command. For instance, to reload the dadjokes application on all remote servers, run:

 
pm2 deploy production exec "pm2 reload dadjokes"

A successful reload will display:

Output
--> Deploying to production environment
--> on host <your_remote_server_ip>
Use --update-env to update environment variables
[PM2] Applying action reloadProcessId on app [dadjokes](ids: [ 0 ])
[PM2] [dadjokes](0) βœ“
--> Success

For a deeper dive into deployment commands and hooks, consult the relevant PM2 documentation.

Note that while PM2 provides a Docker integration through its pm2-runtime command, the general recommendation is to forgo using PM2 if you're deploying your Node.js application with Docker.

Final thoughts

PM2 is a powerful tool for managing and maintaining Node.js applications, offering several features to streamline development and production workflows. While we've covered many of its capabilities in this guide, a vast array of features is still waiting to be explored.

Whether you're looking to integrate PM2 with Docker, run it without a daemon, or set it up with NGINX as a reverse proxy, the possibilities are vast. The PM2 documentation should be your next stop to gain insights into those topics and many others.

Note that the complete source code from this tutorial is available on the prod branch of the dadjokes GitHub repository. It is a practical reference and a starting point for your projects.

Thanks for reading, and happy coding!

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is the Head of Content at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including LWN.net, Digital Ocean, and CSS-Tricks. When he’s not writing or coding, he loves to travel, bike, and play tennis.
Got an article suggestion? Let us know
Next article
A Comprehensive Guide to Dockerizing Node.js Applications
This guide covers building optimized Node.js Docker images, using Docker Compose for multi-container apps, and essential Dockerfile best practices
β†’
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github