PM2 is a process manager. It manages your applications states, so you can start, stop, restart and delete processes.

PM2’s cluster mode allows networked Node.js applications (http(s)/tcp/udp server) to be scaled across all CPUs available, without any code modifications. This greatly increases the performance and reliability of your applications, depending on the number of CPUs available. Under the hood, this uses the Node.js cluster module such that the scaled application’s child processes can automatically share server ports.

In this post, I’ll explain how to scale your NodeJS app via the PM2 process manager inside Docker. Kubernetes is a great way to scale apps via Pods. But sometimes you might need to run your app inside a standalone Docker platform.

AWS Elastic Beanstalk Docker platform is a good example of this. Let’s assume you have a CPU with 4 cores. If you start your app via node app.js, you won’t take advantage of all CPU cores.

Even if you run your app on a Kubernetes cluster, using PM2 as a process manager has advantages.

From the official documentation:

The goal of pm2-runtime is to wrap your applications into a proper Node.js production environment. It solves major issues when running Node.js applications inside a container like:

  • Second Process Fallback for High Application Reliability
  • Process Flow Control
  • Automatic Application Monitoring to keep it always sane and high performing
  • Automatic Source Map Discovery and Resolving Support
  • Further than that, using PM2 as a layer between the container and the application brings PM2 powerful features like application declaration file, customizable log system and other great features to manage your Node.js application in a production environment.

Clone the example repo and cd into it:

git clone https://github.com/ogunacik/docker-pm2.git
cd docker-pm2

Here is a simple Hello World application (app.js)

const http = require('http');

const hostname = '0.0.0.0';
const port = 3000;

const server = http.createServer((req, res) => {
	  res.statusCode = 200;
	  res.setHeader('Content-Type', 'text/plain');
	  res.end('Hello World');
});

server.listen(port, hostname, () => {
	  console.log(`Server running at http://${hostname}:${port}/`);
});

Here is the Dockerfile that install PM2 and runs our app. The -i max option tells PM2 to start a NodeJS instance per CPU core.

FROM node:12

WORKDIR /usr/src/app

RUN npm install -g pm2

COPY . .

EXPOSE 3000

CMD ["pm2-runtime", "app.js", "-i", "max"]

Build the Docker image. I’m using my Docker Hub registry username in tagging. You use your own.

docker build -t acik/hello-pm2 .

Start the app.

docker run -d --name hello-pm2 -p 3000:3000 acik/hello-pm2

Test it from your host. You should get a Hello World response.

curl localhost:3000

Get into container and inspect how PM2 runs the app.

docker exec -it hello-pm2 bash

Inspect PM2 and node processes.

ps -ef

How many node processes do you see?

root          1      0  1 17:18 ?        00:00:01 node /usr/local/bin/pm2-runtime app.js -i max
root         18      1  0 17:18 ?        00:00:00 node /usr/src/app/app.js
root         25      1  0 17:18 ?        00:00:00 node /usr/src/app/app.js
  • “node /usr/local/bin/pm2-runtime app.js -i max” is PM2 process manager. It is the init process of our container.
  • The other 2 “node /usr/src/app/app.js” processes are our app’s instances.

We can say that PM2 has scaled out our app to 2 instances.

Lastly, let’s confirm that we have the same number of CPU cores as app.js instances.

lscpu

Here is my shortened output.

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                2

CPU(s) line shows that I have 2 CPU cores, which means that PM2 has successfully scaled our app.