Deploy a Node.js application with PM2 and Nginx
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

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.

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

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.

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.

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:

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.

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.