Deploy a Node.js application with PM2 and Nginx

Photo by Mike U / Unsplash
Photo by Mike U / Unsplash

Photo by Mike U / Unsplash

Deploying an application is one of the most prominent parts of the software development lifecycle. When working with Node.js, deploying our application can be challenging.

Fortunately, some tools make it easier, like PM2, which keeps your Node.js application alive and allows you to scale and distribute the load between the application instances.

Once the application is live, it is not accessible to the world, and you need a Webserver + a domain name to make it possible. In this tutorial, we will see how to deploy a Node.js application using PM2 and do a reverse proxy with Nginx.

Here is the picture to summarize what we want to achieve

Schema of the communication between the browser and the web application
Schema of the communication between the browser and the web application

Prerequisites

To follow this tutorial, you need to have these elements

  • A ready-to-use VPS; if you don't have one, I wrote a post on setting up a VPS.  You can buy a VPS on DigitalOcean. Sign up with my referral link to get 100$ credits to use over 60 days.
  • A domain or subdomain name
  • An SSH client (I use Termius)
  • Node.js 16+ and Nginx installed on the VPS

Create a simple Node.js application

The first step is to create a sample app we will deploy on the server and make it accessible to the world. We will use the boilerplate we created in this tutorial.


git clone https://github.com/tericcabrel/node-ts-starter.git -b express deploy-node-app

cd deploy-node-app

yarn install

yarn start

Navigate to the URL http://localhost:4500/ to make sure it works as expected.

Update the file src/index.ts with the code below:


import express from 'express';
import dotenv from 'dotenv';

dotenv.config();

const HOST = process.env.HOST || 'http://localhost';
const PORT = parseInt(process.env.PORT || '4500');
let requestCount = 0;

const app = express();

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.get('/', (req, res) => {
  requestCount++;

  return res.json({
    message: 'Hello World!',
    numberOfCall: requestCount,
  });
});

app.listen(PORT, () => {
  console.log(`Application started on URL ${HOST}:${PORT} ?`);
});

When we hit the endpoint with this code, it will increment the number of calls and display them in the browser.

Running the Node.js application locally.
Running the Node.js application locally.

Now, let's transpile the Typescript code to JavaScript.


yarn tsc

A folder named build will be created with the transpiled code. Now we can run the application with Node.js:


node build/index.js

The code still works as expected now we want to deploy it on the VPS.

Package the project and copy it to the server

The build contains only the index.js, but if you zip it and run it on the server, it will not work because the dependencies don't exist, so we will add the package.json and the yarn.lock.


cp package.json yarn.lock build

tar czf backend.tar.gz build/.

The second line compresses the build folder to a file called backend.tar.gz.

The next step is to copy the file to the VPS in the $HOME folder; we will use SCP, which stands for Secure Copy. It allows performing the distant copy.

The syntax is:


scp -P $PORT $ZIPPED_FILENAME $USER@$HOST:./

  • $PORT: Replace this with the port of your VPS
  • $ZIPPED_FILENAME: The compressed file on the local computer we want to copy on the server. The value is backend.tar.gz
  • $USER: Replace the username created on your VPS
  • $HOST: Replace this with the IP address of your VPS.

An example of the whole expression is:


scp -P 4321 backend.tar.gz user@192.186.11.12

Copy the application source to the server
Copy the application source to the server

Run the application with PM2

The file is copied to the server in the "home" directory of the user. Log into, uncompress the application and run with PM2.

ssh -P $PORT $USER@$HOST

tar xf backend.tar.gz

cd build

yarn install

pm2 start index.js --name backend-app

Below, are all these steps in a picture.

Running the application with PM2.
Running the application with PM2.

As we see, our app is working as expected

To install PM2, just run the command below:


npm install -g pm2

With PM2, you can create a configuration file that will be used to launch your application. You can indicate the application's name, the entry point, the environment variables to inject, and so on. Inside the build directory, create a file called ecosystem.config.js and add the code below:

module.exports = {
  apps: [
    {
      name: 'backend-app',
      script: './index.js',
      watch: false,
      force: true,
      env: {
        PORT: 4500,
        NODE_ENV: 'production',
        MY_ENV_VAR: 'MyVarValue',
      },
    },
  ],
};

Now to launch your app, you can do: pm2 start ; behind the scene, PM2 will detect the ecosystem.config.js, parse it and extract the required information to launch  the application

Note: In the ecosystem.config.js, the property "apps" take an array of applications as value, meaning you can manage many applications with a single file.

Set up a Reverse proxy with Nginx

The application is running on the server, but we can access it from the outside. We will create a subdomain and then use Nginx to do a reverse proxy to the application.

My domain's name is tericcabrel.com; I will create a subdomain that points to my VPS IP address. Log into the client space of your hosting platform and do that. I use OVH.

Create a subdomain for our Web application
Create a subdomain for our Web application

The change can take up to 24 hours to propagate. You can use this website to check if the DNS is ready.

Now we need to create an Nginx configuration for the website with the command below:


sudo nano /etc/nginx/sites-available/node.tericcabrel.com

Note: The file's name is node.tericcabrel.com I usually name it like that to quickly find the application I want to apply for a change. Feel free to call it as you want.

Paste the code below inside the file, save and exit:


server {
    server_name  node.tericcabrel.com;
    index index.html index.htm;
    access_log /var/log/nginx/nodeapp.log;
    error_log  /var/log/nginx/nodeapp-error.log error;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:4500;
        proxy_redirect off;
    }
}

Don't forget to replace node.tericcabrel.com by your domain or subdomain.

Enable the website with the command below:


sudo ln -s /etc/nginx/sites-available/node.tericcabrel.com /etc/nginx/sites-enabled/node.tericcabrel.com

We created a symbolic link of our config file inside the folder sites-enabled it is because Nginx only considers the websites present in this folder.

Let's verify if there is no Nginx error, then reload it to take the changes into account:

sudo nginx -t

sudo nginx -s reload

Navigate to http://node.tericcabrel.com to see the result:

The Node.js application accessible through the Web
The Node.js application accessible through the Web

Yess, it works!!!

Add SSL certificate with Letsencrypt

Install Certbot, which is the tool responsible for certificate generation:

sudo apt install snapd
sudo snap install --classic certbot

Generate and install an SSL certificate for our domain

sudo certbot --nginx -d node.tericcabrel.com

You will be asked if you want to redirect HTTP to HTTPS. You can press 1 to deny or 2 to accept. In this case, press 2, hit Enter, and wait for the installation to complete.

Reload Nginx configuration: sudo nginx -s reload

Navigate to https://node.tericcabrel.com.

Secure the web application with an SSL certificate
Secure the web application with an SSL certificate

Challenge

Now, you know how to deploy a Node.js application with PM2 and Nginx, but before you go, I have a challenge for you:

Let's make the number of calls on node.tericcabrel.com reach 500 000?

You can find the code source for this tutorial on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.