Guides
Process Management with PM2

How to Deploy, Manage and Scale Node.js Apps with PM2

Better Stack Team
Updated on May 4, 2022

PM2 is an open-source process manager for Node.js applications that allows you to monitor and manage your application's lifecycle in production. It's like an administrator that simplifies deployment, log management, and resource monitoring, and helps to minimize downtime for each application under its control.

In this article, you'll learn the most important PM2 features and how to use them to deploy, manage, and scale your Node.js applications in production.

By following through with this tutorial, you will become familiar with the following aspects of managing Node.js applications with PM2:

  • Managing your application processes (start, stop, restart, delete).
  • Configuring a multitude of automatic restart strategies.
  • Ensuring that your application starts and exits gracefully.
  • Managing and monitoring your application logs.
  • Scaling up your application through PM2's clustering feature.
  • Deploying your application to one or more servers.

Prerequisites

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

Step 1 — Downloading the demo application

In this article, we'll demonstrate several operations that can be performed with PM2 through a demo Node.js application. Nothing we'll do here is specific to this application, so feel free to use your personal project to follow along if you wish. That said, if you don't have a project lying around, you can clone the demo app repository to your server through the command below:

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

Afterward, change into the project directory and install its dependencies:

cd dadjokes && npm install
Copied!

This project generates a random dad joke through the ICanHazdadjokes API and presents it on a web page. You can try it out by starting the server through the command below:

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

Once the server is running, visit http://localhost:3000 in your browser. You should see the following results:

Screenshot of dadjokes application in Brave

Now that we've set up our demo application, let's go ahead and install PM2's npm package in the next section.

Step 2 — Installing PM2

Before you can utilize PM2 to manage your Node.js application, you need to install its npm package first. Execute the command below to install pm2 globally on your computer:

npm install -g pm2
Copied!

You can also install pm2 locally (npm install pm2), especially when deploying to a cloud provider where you do not have access to the CLI to start your Node.js application. In such situations, PM2 should be added to the project's dependencies and accessed via an NPM script.

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

pm2 --version
Copied!

Here's the expected output:

Output
5.2.0

Each installation of PM2 actually provides four executables:

  1. pm2: the main PM2 binary.
  2. pm2-dev: a nodemon-like tool for auto restarting the Node.js process in development.
  3. pm2-runtime: a drop-in replacement for the node command intended to be used with containers.
  4. pm2-docker: an alias for pm2-runtime.

We'll explore the use case for each of these executables in subsequent sections. So let's go ahead and set up a basic development workflow with the pm2-dev binary in the next step.

Step 3 — Starting your application in development

Although PM2 primarily provides production-grade features, it can also be used in development through the pm2-dev binary. When you start your application with pm2-dev, it watches the current directory and restarts your application whenever a file changes in the directory or any subdirectories.

Go ahead and start the server for your application in development mode using the command below. Ensure to kill the previous instance with Ctrl-C before proceeding.

pm2-dev server.js
Copied!

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, making changes to 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 value of the title property highlighted below 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); } }); . . .
Copied!

Return to the terminal instance where your development server is running. You should observe the following output indicating that PM2 detected the change and restarted the server accordingly:

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

Ensure to use the --help flag to discover other options that can be used to customize the behavior of pm2-dev. In the next section, we will start exploring the features of the main pm2 binary.

Step 4 — Starting your application in production

When you're ready to deploy your application to production, you can use the start subcommand provided with the pm2 binary. Go ahead and kill your existing development server with Ctrl-C, then use the command below to start it in production mode:

pm2 start --name 'dadjokes' server.js
Copied!

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 output indicates that the dadjokes application was launched in the background successfully. If your terminal window is small, you may see less columns than what is displayed above so ensure to expand your terminal to get the complete view.

Here's an explanation of each column above:

  • id: the application's id as assigned by pm2.
  • name: the name of the application. The entry file name (server.js) is used by default if the --name flag is not provided.
  • namespace: provides an easy way to start/stop/restart a group of apps in one step.
  • version: the app's version number (taken from package.json).
  • mode: this can be fork or cluster. Instructs pm2 to use either the child_process.fork API or the cluster API.
  • pid: a unique number that identifies the process.
  • uptime: the length of time since the application started running.
  • : indicates how many times the application was restarted.
  • status: the current status of the application.
  • cpu: CPU usage.
  • mem: memory usage.
  • user: name of the user who started the application.
  • watching: whether pm2 will automatically restart your application when a file is modified in the current directory or its subdirectories. This is configured through 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) before it is considered "online", you can use the --wait-ready flag to cause PM2 to wait for a "ready" event. Here's how to send the event in your application:

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);
});
Copied!

After adding the highlighted lines above to your application, delete the existing instance and start it once again with the --wait-ready flag:

pm2 delete dadjokes
Copied!
pm2 start --name 'dadjokes' server.js --wait-ready
Copied!
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, pm2 will wait for three seconds before marking your application as online regardless of whether the ready event is received or not. If you want to customize this timeout, pass a millisecond value to the --listen-timeout flag as shown below:

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

In the next section, we'll discuss how to examine your application's resource usage and other relevant metrics with PM2.

Step 5 — Monitoring application metrics

Once your application is up and running, you can use PM2's list, show, and monit subcommands to keep tabs on how well your application is performing. First off, list all the running applications on your server with pm2 list as shown below:

pm2 list
Copied!

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 output tells us that we have just the one application running at the moment. It has been up for three minutes, and is currently using about 53.3mb of memory. You can sort the running applications based on a specific metric in ascending or descending order. For example, to sort the applications in descending order of memory usage, use the command below:

pm2 list --sort memory:desc
Copied!

Here's the sort of output you can expect if you had multiple running applications:

Output
┌─────┬────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name           │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 1   │ hacker-news    │ default     │ 1.0.0   │ fork    │ 10079    │ 2m     │ 0    │ online    │ 0%       │ 57.9mb   │ ayo      │ disabled │
│ 0   │ dadjokes       │ default     │ 1.0.0   │ fork    │ 29857    │ 14m    │ 0    │ online    │ 0%       │ 56.1mb   │ ayo      │ disabled │
│ 2   │ covid-stats    │ default     │ 1.0.0   │ fork    │ 11252    │ 104s   │ 0    │ online    │ 0%       │ 53.8mb   │ ayo      │ disabled │
└─────┴────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

Here are the other ways to sort applications using the list subcommand:

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

PM2 can display more information about a running application beyond what is presented in the output of the list command. It can provide other metadata such as the node version in use, output and error log paths, heap size and usage, HTTP request rate and latency information, and more. This is where the show subcommand comes in. All you need to do is provide the application's name or id to the command as shown below:

pm2 show dadjokes
Copied!

This should yield the following output:

Output
  Describing process with id 0 - name dadjokes
┌───────────────────┬───────────────────────────────────────────┐
│ status            │ online                                    │
│ name              │ dadjokes                                  │
│ namespace         │ default                                   │
│ version           │ 1.0.0                                     │
│ restarts          │ 0                                         │
│ uptime            │ 3m                                        │
│ script path       │ /home/ayo/dev/demo/dadjokes/server.js     │
│ script args       │ N/A                                       │
│ error log path    │ /home/ayo/.pm2/logs/dadjokes-error.log    │
│ out log path      │ /home/ayo/.pm2/logs/dadjokes-out.log      │
│ pid path          │ /home/ayo/.pm2/pids/dadjokes-0.pid        │
│ interpreter       │ node                                      │
│ interpreter args  │ N/A                                       │
│ script id         │ 0                                         │
│ exec cwd          │ /home/ayo/dev/demo/dadjokes               │
│ exec mode         │ fork_mode                                 │
│ node.js version   │ 16.14.0                                   │
│ node env          │ N/A                                       │
│ watch & reload    │ ✘                                         │
│ unstable restarts │ 0                                         │
│ created at        │ 2022-03-10T11:34:09.788Z                  │
└───────────────────┴───────────────────────────────────────────┘
 Revision control metadata
┌──────────────────┬──────────────────────────────────────────────────────┐
│ revision control │ git                                                  │
│ remote url       │ [email protected]:betterstack-community/dadjokes.git    │
│ repository root  │ /home/ayo/dev/demo/dadjokes                          │
│ last update      │ 2022-03-10T11:34:09.805Z                             │
│ revision         │ 20d68430ac3e0be20b65cd26455292220a743b5f             │
│ comment          │ Initial commit                                       │
│ branch           │ main                                                 │
└──────────────────┴──────────────────────────────────────────────────────┘
 Actions available
┌────────────────────────┐
│ km:heapdump            │
│ km:cpu:profiling:start │
│ km:cpu:profiling:stop  │
│ km:heap:sampling:start │
│ km:heap:sampling:stop  │
└────────────────────────┘
 Trigger via: pm2 trigger dadjokes <action_name>

 Code metrics value
┌────────────────────────┬───────────┐
│ Used Heap Size         │ 28.14 MiB │
│ Heap Usage             │ 96.48 %   │
│ Heap Size              │ 29.16 MiB │
│ Event Loop Latency p95 │ 1.33 ms   │
│ Event Loop Latency     │ 0.41 ms   │
│ Active handles         │ 4         │
│ Active requests        │ 0         │
│ HTTP                   │ 0 req/min │
│ HTTP P95 Latency       │ 2770 ms   │
│ HTTP Mean Latency      │ 1276 ms   │
└────────────────────────┴───────────┘

 Divergent env variables from local env
┌──────────────────────┬────────────────────────────────────────────────┐
│ npm_lifecycle_script │ pm2 "start" "--name" "dadjokes" "server.js"    │
└──────────────────────┴────────────────────────────────────────────────┘

 Add your own code metrics: http://bit.ly/code-metrics
 Use `pm2 logs dadjokes [--lines 1000]` to display logs
 Use `pm2 env 0` to display environment variables
 Use `pm2 monit` to monitor CPU and Memory usage dadjokes

The above output is neatly divided into a few sections. The first section describes some metadata about the application and its environment, while the next provides relevant versioning metadata if your application is in a git repository. The section titled "Actions available" defines the actions that you can perform on your running process in real-time. In the penultimate section, you'll see a few metrics regarding memory efficiency and some relevant HTTP statistics for your server. Here's what each value signifies:

  • Heap Size: this is the amount of memory allocated when your program creates objects. It is released upon garbage collection.
  • Used Heap Size: this is the amount of heap memory currently being used by the program.
  • Heap Usage: reflects the percentage of the allocated memory that is being used. The closer to 100%, the better because it is an indication that program efficiently uses memory and releases the memory that is not in use.
  • Event Loop Latency: this value reflects the time used by the event loop to complete a single loop iteration. It corresponds to how much time the main thread is blocked performing synchronous work without giving the event loop a chance to iterate.
  • Event Loop Latency p95: the 95th percentile latency value.
  • Active handles: represents long-lived objects capable of performing certain operations when active.
  • Active requests represents short-lived objects usually performed over a handle.
  • HTTP: number of requests received by the server per minute.
  • HTTP Mean Latency: the average amount of time taken to complete a request.
  • HTTP P95 Latency: measures latency from 95% of requests.

PM2 also provides the monit subcommand for viewing real-time data about a running process including the metrics and metadata we've discussed already and log messages produced by the application.

pm2 monit
Copied!

Screenshot of pm2 monit in action

Step 6 — Restarting your application with PM2

One of the significant advantages of using a process manager like PM2 to manage your Node.js application in production is its ability to automatically restart the application when accidents occur. You can also restart your application at any time with the restart subcommand. It kills the process and relaunches it afterward:

pm2 restart dadjokes
Copied!
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 incremented to 1, indicating that the application has been restarted once.

PM2 will also automatically restart your application when the process exits regardless of the exit code. You can simulate a graceful shutdown by making a request to the /graceful-shutdown route. It calls process.exit() with a success code (0).

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

Also, you can simulate a crash by making a request to the /crashme route. It calls process.exit() with a failure code (1).

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

When you run pm2 list, you'll notice that the number of restarts has increased to 3, and the application remains online:

pm2 list
Copied!
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 │
└─────┴─────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

At the time of writing, PM2 does not allow you to configure the restart behavior based on the exit code of an application. A --stop-exit-codes flag is mentioned in the docs, but it doesn't appear to be working in the latest version. There is an open issue on GitHub tracking this feature.

pm2 start --stop-exit-codes 0 --name 'dadjokes' server.js
Copied!
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 — Configuring auto-restart strategies

PM2 uses an ecosystem.config.js file to organize configuration details for one or more applications. You can create one in your project directory through the command below:

pm2 init simple
Copied!

You should observe the following output, which indicates that the file was created successfully.

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

Open the ecosystem.config.js in your text editor. It should look like this:

ecosystem.config.js
module.exports = {
  apps : [{
    name   : "app1",
    script : "./app.js"
  }]
}
Copied!

Change name field to dadjokes and script to ./server.js:

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

At this point, you can use the config file to start, stop or restart all the applications declared within it.

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

Now, let's go ahead configure additional restart strategies for our application through the config file.

Restarting based on memory usage

PM2 supports restarting a Node.js application once a memory threshold is reached. This can help prevent a "heap out of memory error" where the application's memory usage exceeds what the system can allocate. You can use the max_memory_restart option in your ecosystem.config.js file as shown below:

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

The highlighted line above instructs PM2 to auto restart the dadjokes application if its memory usage exceeds 1 Gigabyte. You can also configure the max_memory_restart option in Kilobyte (K), and Megabyte (M).

Restarting based on a CRON schedule

You can also configure PM2 to restart your application based on a cron schedule. For example, you can restart an application every 24 hours with the following syntax:

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

If you're not familiar with the CRON syntax, you can experiment with the crontab guru editor.

Restarting based on file changes

Similar to how the pm2-dev binary works, you can configure pm2 to restart the application whenever a file is modified in the current directory or subdirectories through the watch option. You can set this option to true to activate watch mode or an array of directories that should be monitored for changes. You can also use the watch_delay option to specify a delay in milliseconds before restarting the application after a file change, and an ignore_watch option with an array of directories to ignore.

ecosystem.config.js
module.exports = {
  apps: [{
    name: 'dadjokes',
    script: './server.js',
watch: ['views'],
ignore_watch: ['node_modules', 'public'],
watch_delay: 1000,
}] }
Copied!

Setting a delay before auto restarting

By default, PM2 will attempt to restart your application immediately it exits. However, it's possible to delay this automatic restart through the restart_delay option. It accepts the number of milliseconds before PM2 restarts the application.

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

Utilizing an exponential backoff restart delay

Instead of setting a fixed delay before restarting the application, you can use the exp_backoff_restart_delay option to incrementally raise the time between restarts up to 15 seconds. The initial delay time set through this option, and it 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
}] }
Copied!

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 more than 30 seconds.

Configuring the maximum number of restarts

PM2 provides a max_restarts option for configuring the maximum number of unstable restarts (less than one second) 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. The default appears to be 16 restarts, but this value doesn't appear in the documentation so you shouldn't rely on it.

You can also change what is considered 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,
}] }
Copied!

With the above configuration in place, any automatic restart of the application will be considered unstable if it happens within five seconds of the previous restart. Suppose the application crashes in the 6th second (or above) each time it is restarted, it will be considered a "stable" restart so the max_restarts value is ignored, leading PM2 to restart it indefinitely. Therefore, it is important to carefully choose your minimum uptime value to avoid such situations.

Once an application has reached the maximum number of restarts, it will display an errored status. Your intervention will be required at this point 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 │
└─────┴────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Copied!

Disabling automatic restarts

If you want to disable all forms of automatic restarts, you can do so by setting the autorestart option to false:

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

Let's go ahead and configure the following restart strategies in the ecosystem.config.js file, then save the file.

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,
}, ], };
Copied!

Delete your existing dadjokes app instance with the pm2 delete command. This will kill the app and remove it from the process list.

pm2 delete dadjokes
Copied!
Output
[PM2] Applying action deleteProcessId on app [dadjokes](ids: [ 0 ])
[PM2] [dadjokes](0) ✓
┌─────┬───────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name      │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
└─────┴───────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

Afterward, start it again through the configuration file as shown below. This will launch the dadjokes application with the defined options.

pm2 start ecosystem.config.js
Copied!
Output
[PM2][WARN] Applications dadjokes not running, starting...
[PM2] App [dadjokes] launched (1 instances)
┌─────┬─────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name        │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼─────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ dadjokes    │ default     │ 1.0.0   │ fork    │ 26229    │ 0s     │ 0    │ online    │ 0%       │ 19.1mb   │ ayo      │ disabled │
└─────┴─────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

In the next section, we will discover how to ensure that our applications continue to run even after a system reboot.

Step 8 — Launching applications on system startup

We covered several ways to ensure your application remains up and running even when it crashes unexpectedly in the previous two steps. However, another situation that needs to be accounted for is the initial application launch after a system reboot. In this section, you will learn how to configure a Node.js application to start automatically after a reboot or crash using PM2 and Systemd on Linux, which is the init system for most mainstream distributions. Note that PM2 supports other init systems aside from Systemd, including upstart, launchd (macOS), openrc, rcd (FreeBSD), and systemv (Centos 6, Amazon Linux). If you're on Windows, take a look at the pm2-installer package.

The first step is to generate a startup script for your init system, which will be used to launch the pm2 daemon on system startup. Subsequently, the daemon will launch any saved applications so that they are fully operational once again without your intervention. Here's how to generate the init script for systemd:

pm2 startup systemd
Copied!

You should observe the following output:

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/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>

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>
Copied!

If the command is successful, you should see the following output:

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 the command fails, you may see an output that looks like this:

Output
[PM2][ERROR] Init system not found
Platform systemd
Template
[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target

[Service]
Type=forking
User=<username>
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=PATH=/home/<username>/.local/share/nvm/v16.14.0/bin/:/home/<username>/.local/share/nvm/v16.14.0/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PM2_HOME=/home/<username>/.pm2
PIDFile=/home/<username>/.pm2/pm2.pid
Restart=on-failure

ExecStart=/home/<username>/.local/share/nvm/v16.14.0/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/home/<username>/.local/share/nvm/v16.14.0/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/home/<username>/.local/share/nvm/v16.14.0/lib/node_modules/pm2/bin/pm2 kill

[Install]
WantedBy=multi-user.target

Target path
/etc/systemd/system/pm2-<username>.service
Command list
[ 'systemctl enable pm2-<username>' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-<username>.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-<username>...
/bin/sh: 1: systemctl: not found
[ERROR] Exit code : 127
[PM2][ERROR] systemctl enable pm2-<username> failed, see error above.

The above output indicates that the pm2 service file was created in the /etc/systemd/system directory as pm2-<username>.service, but it couldn't be enabled due to an error. If you get such output, go ahead and execute the following command as root to enable the service manually. You only need to do this once:

sudo systemctl enable pm2-<username>
Copied!

You are almost all set to automatically restart any managed applications on system startup. The final step is to save your PM2 process list to a dump file to be accessible to the daemon on system startup. Here's the command you need to run:

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

At this point, PM2 will automatically launch any saved process on system startup. You can update this list by running pm2 save once again. Try it out by rebooting your server and running pm2 list immediately after logging in. You should notice that all your applications are fully operation as expected. If for some reason, this isn't the case, you can run the command below to bring them all up at once instead of launching them one after the other.

pm2 resurrect
Copied!

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 stage, we've demonstrated all the ways that PM2 can ensure that your application remains operation even when external factors (like a system reboot) interfere with the process. If you need to keep tabs on the online status of your application, ensure to set up uptime monitoring checks to get notified of server availability issues and more.

Step 9 — Stopping and deleting your application

PM2 provides the stop and delete subcommands for stopping an application, and removing it from the process list respectively. You can pass one or more application names, ids, namespaces, config files, or a special all keyword to either command.

The command below will stop all the applications managed through the ecosystem.config.js file:

pm2 stop ecosystem.config.js
Copied!
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
Copied!
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 ensure a graceful shutdown when utilizing the restart, stop, reload, or delete commands, you need to intercept the SIGINT or SIGTERM program interruption signals. This is important so that any necessary cleanup operations are performed before the application exits such as handling all existing requests and freeing up resources (files, database connections, etc).

Here's an example that stops accepting new connections on the server when the SIGINT and SIGTERM signals are received before exiting the process:

server.js
. . .

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

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

Step 10 — Viewing and configuring application logs

PM2 saves the logs produced by an application in the $HOME/.pm2/logs directory. The entries logged to the standard output are placed in the <app_name>-out.log file, while those logged to the standard error are placed in the <app_name>-error.log file.

ls ~/.pm2/logs
Copied!
Output
covid-stats-error.log  cryptoStats-out.log    index-error.log  main-out.log           server-error.log
covid-stats-out.log    hacker-news-error.log  index-out.log    dadjokes-error.log  server-out.log
cryptoStats-error.log  hacker-news-out.log    main-error.log   dadjokes-out.log

In the ecosystem.config.js file, you can specify the error_file and out_file options to customize the location of the application's output and error log files. The example below configures PM2 to output the logs for the dadjokes application in the standard /var/log directory.

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: '/var/log/pm2/dadjokes/out.log',
error_file: '/var/log/pm2/dadjokes/error.log',
}, ], };
Copied!

If you try to start the application afterward (after deleting first), you may get the error below. It indicates that there was a permissions issue while attempting to create and write to the provided path.

pm2 delete ecosystem.config.js
Copied!
pm2 start ecosystem.config.js
Copied!
Output
[PM2][WARN] Applications dadjokes not running, starting...
[PM2][WARN] Folder does not exist: /var/log/pm2/dadjokes
[PM2] Creating folder: /var/log/pm2/dadjokes
[PM2][ERROR] Could not create folder: /var/log/pm2/dadjokes
[PM2][ERROR] Error: Could not create folder

To fix such errors, you need to create the /var/log/pm2 directory first if it doesn't exist already:

sudo mkdir /var/log/pm2
Copied!

Afterward, change the ownership of the /var/log/pm2 directory and all subdirectories so that it's owned by the user running PM2 so that it can write the log files to that directory.

sudo chown -R <your_username> /var/log/pm2
Copied!

At this point, starting the application shouldn't yield errors related to the log file configuration anymore.

Output
[PM2][WARN] Applications dadjokes not running, starting...
[PM2][WARN] Folder does not exist: /var/log/pm2/dadjokes
[PM2] Creating folder: /var/log/pm2/dadjokes
[PM2] App [dadjokes] launched (1 instances)
┌─────┬─────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name        │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼─────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ dadjokes    │ default     │ 1.0.0   │ fork    │ 9408     │ 0s     │ 0    │ online    │ 0%       │ 18.9mb   │ ayo      │ disabled │
└─────┴─────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

Enabling log rotation

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

pm2 install pm2-logrotate
Copied!

Afterward, when you run pm2 list, you should see a new "Modules" 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      │
└────┴──────────────────────────────┴───────────────┴──────────┴──────────┴──────┴──────────┴──────────┴──────────┘

The pm2-logrotate module defaults to rotating log files once they grow over 10 megabytes in size. It keeps up to 30 rotated files before deleting old ones, but it does not compress the backup files by default. You can configure these values and more by following the instructions in its GitHub repository.

If you'd like to prevent PM2 from writing log entries to the filesystem, you can set the error_file and out_file options to /dev/null as shown below. You might want to do this when shipping your logs to a log management platform.

ecosystem.config.js
module.exports = {
  apps: [
    {
out_file: '/dev/null',
error_file: '/dev/null',
}, ], };
Copied!

Regardless of whether you choose to persist log messages to the filesystem or not, you can use the pm2 logs command to display incoming entries in real time:

pm2 logs dadjokes
Copied!
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

You can use the --help flag to investigate all the options provided on the logs command to discover how to customize its output.

pm2 logs --help
Copied!

Step 11 — Working with environmental variables

PM2 provides the ability to specify the environmental variables for an application in its config file. You'll need to add each variable in a special env object:

ecosystem.config.js
module.exports = {
  apps: [
    {
      . . .
env: {
PORT: 3000,
},
}, ], };
Copied!

The defined variables will be injected into the application's environment when you restart the application. You can also specify addition environments such as staging and production for example:

ecosystem.config.js
module.exports = {
  apps: [
    {
      . . .
env: {
PORT: 3000,
NODE_ENV: 'development',
},
env_staging: {
PORT: 3000,
NODE_ENV: 'staging',
},
env_production: {
PORT: 80,
NODE_ENV: 'production',
},
}, ], };
Copied!

Here, env represents the default environment, while env_staging and env_production represent the environmental variables for staging and production. You can define as many environments as long as they are all prefixed with env_.

When you're ready to start or restart the application, use the --env flag to specify your preferred environment:

pm2 restart ecosystem.config.js --env staging
Copied!

You can also change a specific variable when restarting an application. Here's an example that causes the application to restart on port 4000 instead of 3000:

PORT=4000 pm2 restart dadjokes --update-env
Copied!

Adding the --update-env option causes the dadjokes app to update the application's environment without specifying it in the config file. If you want to view the entire environment, for an application you need to pass its id to the pm2 env command:

pm2 env 0
Copied!
Output
namespace: default
max_restarts: 10
min_uptime: 2000
exp_backoff_restart_delay: 100
max_memory_restart: 1073741824
km_link: false
vizion_running: false
NODE_APP_INSTANCE: 0
dadjokes: {}
PM2_HOME: /home/ayo/.pm2
PORT: 4000
PM2_JSON_PROCESSING: true
PM2_USAGE: CLI
_VOLTA_TOOL_RECURSION: 1
XDG_DATA_DIRS: /var/lib/snapd/desktop
WT_SESSION: 26514071-b8c5-4687-a9b2-b2f34b1b9361
. . .
version: 1.0.0
node_version: 16.14.0
error_file: /var/log/pm2/dadjokes/error.log
out_file: /var/log/pm2/dadjokes/out.log
script: ./server.js
NODE_ENV: staging

If you want to add secrets to your application's environment, the ecosystem.config.js file is not the best place to do it since it will be committed to the Git repository. Unfortunately, at the moment, there is no official solution to this common use case so try to investigate the strategies suggested by other PM2 users in this GitHub issue.

Step 12 — Clustering with PM2

PM2 supports a cluster mode that allows networked Node.js applications to utilize all the CPUs on the host without any code modifications. This means that multiple instances of your application (depending on the CPU count on your machine) will be launched on the same port so that incoming workloads can be distributed among them increasing performance compared to a single instance.

Under the hood, PM2 uses the cluster module in the Node.js standard library to create child processes that share the same server port. For PM2's cluster mode to be effective, you need to ensure that your application is stateless. This means knowledge of or reference to past transactions happens solely through a shared stateful medium like a database or cache.

Once you're ready to start your application in cluster mode, ensure to delete it from the process list first before executing the start subcommand with the -i flag as shown below:

pm2 start ecosystem.config.js -i max
Copied!

The max value above specifies that PM2 should spawn as many workers as the number of available CPU cores. You can also use a specific number, but note that it is not advisable to spawn workers than the available CPU cores because each worker will start to compete for resources which could lead to performance degradation.

pm2 start ecosystem.config.js -i 2
Copied!

The general recommendation is to spawn one less worker than available CPU cores. For example, if your server has four cores, you should spawn three workers. This can be configured in PM2 by passing a negative integer to the -i flag. The -1 value below means one less than available CPU cores.

pm2 start ecosystem.config.js -i -1
Copied!

You can also configure cluster mode in your ecosystem.config.js file by specifying the number of workers via the instances option and setting the exec_mode option to cluster:

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

Then start the application with command below:

pm2 start ecosystem.config.js
Copied!

You should observe the following output informing you that the application started in cluster mode as instead of fork mode. The number of spawned workers will depend on how many CPU cores are available on your machine or server.

Output
[PM2][WARN] Applications dadjokes not running, starting...
[PM2] App [dadjokes] launched (3 instances)
┌─────┬──────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name             │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼──────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 3   │ dadjokes         │ default     │ 1.0.0   │ cluster │ 349      │ 0s     │ 0    │ online    │ 0%       │ 41.4mb   │ ayo      │ disabled │
│ 4   │ dadjokes         │ default     │ 1.0.0   │ cluster │ 356      │ 0s     │ 0    │ online    │ 0%       │ 39.6mb   │ ayo      │ disabled │
│ 5   │ dadjokes         │ default     │ 1.0.0   │ cluster │ 367      │ 0s     │ 0    │ online    │ 0%       │ 33.0mb   │ ayo      │ disabled │
└─────┴──────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

Using cluster mode ensures that incoming requests are balanced between the workers. You can use the NODE_APP_INSTANCE environmental variable to differentiate between each process. It is useful when you want to execute an operation on only 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);
  }
});
Copied!

Restart the application through the command below:

pm2 restart ecosystem.server.js
Copied!

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

pm2 logs dadjokes
Copied!
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

One added advantage of using cluster mode is that you can achieve zero-downtime reloads in production when you use pm2 reload as PM2 will restart each process one after the other so that at least one is permanently active.If you use pm2 restart instead, all the running processes will be killed and restarted at once, leading to a short period of downtime.

PM2 can also create additional workers or delete currently running ones via the scale subcommand. For example, run the following command to add two workers to the existing ones:

pm2 scale dadjokes +2
Copied!
Output
[PM2] Scaling up application
┌─────┬──────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name             │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼──────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 3   │ dadjokes         │ default     │ 1.0.0   │ cluster │ 349      │ 8m     │ 0    │ online    │ 0%       │ 52.6mb   │ ayo      │ disabled │
│ 4   │ dadjokes         │ default     │ 1.0.0   │ cluster │ 356      │ 8m     │ 0    │ online    │ 0%       │ 52.5mb   │ ayo      │ disabled │
│ 5   │ dadjokes         │ default     │ 1.0.0   │ cluster │ 367      │ 8m     │ 0    │ online    │ 0%       │ 49.5mb   │ ayo      │ disabled │
│ 6   │ dadjokes         │ default     │ 1.0.0   │ cluster │ 8842     │ 0s     │ 0    │ online    │ 0%       │ 39.0mb   │ ayo      │ disabled │
│ 7   │ dadjokes         │ default     │ 1.0.0   │ cluster │ 8849     │ 0s     │ 0    │ online    │ 0%       │ 31.4mb   │ ayo      │ disabled │
└─────┴──────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

You can also set a fixed number of workers as shown below. PM2 will scale up or scale down as appropriate

pm2 scale dadjokes 2
Copied!

Step 13 — Deploying your application to production

PM2 features a comprehensive deployment system that can help you deploy your application to one or more remote servers. Open up your ecosystem.config.js file and add the following highlighted lines:

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

Here's an explanation of each property in the production object:

  • user: the user used to authenticate on each remote server.
  • host: an array of IP addresses for each remote server.
  • ref: the git branch and remote to deploy. For example origin/master, upstream/main, etc.
  • repo: the git repository remote URL, which could be HTTPS or SSH.
  • path: the path on the remote server where the repo will be cloned to.
  • post-setup: the command or script to execute after cloning the repo.
  • post-deploy: the command or script to execute on deploying the application.

Before you provision the host servers, ensure that pm2 is installed on each server and that they have the relevant permissions to clone the Git repository (like the relevant SSH key setup, for example). After you've confirmed that the remote servers are correctly configured, run the command below from your project root to provision the server:

pm2 deploy production setup
Copied!

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 get an error at this stage, it will most likely be an SSH-related error that makes PM2 unable to access the remote server or Git repository.

Output
--> Deploying to production environment
--> on host <your_remote_server_ip>
  ○ hook pre-setup
  ○ running setup
  ○ cloning [email protected]:betterstack-community/dadjokes.git
  ○ full fetch
Cloning into '/home/ayo/dadjokes/source'...
Host key verification failed.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

  failed to clone

Deploy failed
Deploy failed with exit code: 1

Ensure that running git clone <your_git_repo_url> works on the remote server, then follow the troubleshooting instructions on this page to solve the problem.

Once the provisioning step is completed successfully, you can deploy the application using the command below. You may need to add the --force option to force deployment if local changes are in your repository.

pm2 deploy production
Copied!

You should observe the following output if all goes well:

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)
┌─────┬─────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name        │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼─────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ dadjokes    │ default     │ 1.0.0   │ cluster │ 1368097  │ 0s     │ 0    │ online    │ 0%       │ 30.9mb   │ ayo      │ disabled │
└─────┴─────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
[PM2][WARN] Current process list is not synchronized with saved list. App server differs. Type 'pm2 save' to synchronize.
  ○ successfully deployed origin/prod
--> Success

If you log into your remote server and run pm2 list, you will observe that the application has been deployed and is fully operational. You can also run commands on each remote server without logging in through the pm2 deploy command. For example, here's how you can reload the dadjokes application on all remote servers in one fell swoop:

pm2 deploy production exec "pm2 reload dadjokes"
Copied!

You should observe the following output indicating that the command was successful:

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

If you want to learn more about other deployment commands and hooks, ensure to read the relevant PM2 documentation.

Conclusion and next steps

In this article, we covered various aspects of managing Node.js applications with PM2 starting from using it during development to managing the entire lifecycle of an application in production. We discussed many topics in this article, but there's more to discover such as docker integeration, daemon-less mode, or NGINX integration. Don't forget to check out the PM2 docs for more information on these topics and more.

You can find the entire source code used for this tutorial in the prod branch of this GitHub repository. Thanks for reading, and happy coding!

Check Uptime, Ping, Ports, SSL and more.
Get Slack, SMS and phone incident alerts.
Easy on-call duty scheduling.
Create free status page on your domain.
Got an article suggestion? Let us know
Next article
A Complete Guide to Using TypeScript in Node.js
Licensed under CC-BY-NC-SA

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