CI/CD Using PM2 Deploy for Node Application

February 14th 2020

PM2 is a daemon process manager that helps manage and keep your application process running. It can restart your application if there is an unexpected error or exit code, ensuring that your application stays online.

Before we get started, this tutorial makes a few assumptions:

  • You have ssh access and root/sudo permissions to a remote host server
  • You have a (node) application that you want to keep online
  • You have your application hosted on a remote Git provider (Github/Gitlab) etc
  • You have a firewall/iptables configured to access the running application from external requests (ie. you have mapped port 80 to port 3000) or have a reverse web server proxy setup such as nginx reverse proxy

First things first we will need to get PM2 installed both on your local machine and on the remote server. On your local machine you can run:

npm install pm2 -g
//or
yard global add pm2

You can make sure PM2 is installed by running pm2 -v from the command line. With that out of the way we will want to create a SSH key pair that we can use to login to our server. This isn’t required, but you will be prompt for your password a lot when running the pm2 deploy command so this is a convenience factor. If you already have a public/private key installed you can copy the private key to the server (which I’ll show in a bit) otherwise we will walk through creating one. From the terminal change directories to: cd ~/.ssh/ and from there we can use the ssh-keygen utility to create a public/private key pair: ssh-keygen -t rsa -f deploy_rsa when prompt for the password you can press enter as we’ll leave this blank. The name of the keys that are generated is deploy_rsa.pub and deploy_rsa. We will use this later in our ecosystem config for pm2. You can copy the key onto the server by typing: ssh-copy-id -i ~/.ssh/deploy_rsa.pub user@host where user is your remote use and host is your IP address/domain name for the server. Once the key is copied over you can try logging in ssh user@host and you should be able to login without a password. Since we’re on the server we can run through the same steps to generate a private/public key that we can use to copy over to our Gitlab/Github account so PM2 can pull in our directory during deployment. Let’s cd ~/.ssh/ and we can run the same generate command: ssh-keygen -t rsa this time we let the default file names generate as PM2 will look for these default names id_rsa and id_rsa.pub as to not be confused with our local private ssh key. You can cat out the contents of id_rsa.pub and copy the contents over so you can add the key to your Gitlab/Github account. Here is how you would do that respectively:

Gitlab: https://docs.gitlab.com/ee/ssh/#adding-an-ssh-key-to-your-gitlab-account

Github: https://help.github.com/en/enterprise/2.18/user/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account

Once that is done want to make sure that our server can pull/clone from our Gitlab/Github account by adding the git server to our known_hosts. Let’s assume we’re going to store our application on our server at: /var/www/app we can create a folder in /var/www by using mkdir /var/www/app and then we will cd /var/www/app and we can clone down our git directory: git clone [email protected]:user/project.git you will have to use your git directory here so find that and copy the link to use in your clone command. This will add Gitlab/Github as known_hosts. We will delete the project folder that was added from by using rm -rf /var/www/app/project because PM2 will do this for us on deploy setup. Speaking of PM2 let’s make sure it is installed globally on our server:

npm install pm2 -g
yarn global add pm2

And finally from the server: pm2 -v

Perfect, now that is done we can logout and setup some configuration for our PM2 deploy process. From your local machine make sure you are in your projects working directory. We will then create a ecosystem.json file by using pm2 utility: pm2 ecosystem which generates the file for us. We can then open it up and start editing. Let’s first remove some extraneous configs that were added:

{
  "apps" : [{
    "name"      : "APP",
    "script"    : "app.js",
    "env": {
      "NODE_ENV": "production"
   },
  ...
}

We are assuming that the entry point for your app is app.js which PM2 will default to running the command: node app.js to start and daemonize your app. You can also use a package.json script here such as npm start or yarn start such that it looks like:

{
  "apps" : [{
    "name"      : "APP",
    "script"    : "yarn start",
    "env": {
      "NODE_ENV": "production"
   },
  ...
}

Of course in your package.json file you would need:

{
  ...
  "scripts": {
    "start": "node app.js"
  }
  ...
}

Let’s get back to our ecosystem.json file. Now that we have our app setup we can setup the configuration for our deploy. Let’s take a look at what that might look like:

{
  "apps" : [{
    "name"      : "APP",
    "script"    : "yarn start",
    "env": {
      "NODE_ENV": "production"
   },
  "deploy": {
    //your username to login to the server here
    "user" : "username",
    // Multi host is possible, just by passing IPs/hostname as an array. IP of your host(s) here
    "host" : ["212.83.163.1"],
    // path to the public key to authenticate
    "key": "~/.ssh/deploy_rsa.pub",
    // The branch you want to git pull from:
    "ref"  : "origin/master",
    // your gitlab/github repo link
    "repo" : "[email protected]:user/project.git",
    // path to the folder on the server your app will be
    "path" : "/var/www/app",
    // the command to run after the repo is pulled down and you are ready to run the start script
    "post-deploy" : "yarn && pm2 startOrRestart ecosystem.json --name APP"
  }
}

There are a lot of options here but a couple important notes: You will need to specify the path to your key that we created earlier: ~/.ssh/deploy_rsa.pub and this is the private/public key on your local machine. In your “post-deploy” script needs to do other installations you can do that here, but we’re assuming you just need to install the dependencies. We added yarn or npm install command incase you add more packages locally and need to be installed remotely. If you don’t need to install any packages you can leave this out. Every other option is pretty well commented so you can read through if there is anything else you would need to do for the deploy. Let’s stage our ecosystem.json file and push it up to our repo: git add . && git commit -m "added ecosystem file for pm2 deploy" && git push origin master. Now we can run our deploy setup, which you only need to do once (upon setup) and the other pm2 deploy command will not need to use the setup command again. In your project root on your local machine: pm2 deploy production setup. Once this is finished you are ready to deploy: pm2 deploy production and you should see pm2 go to work and give you a confirmation. If you run into errors you can look through the troubleshooting docs: https://pm2.keymetrics.io/docs/usage/deployment/#troubleshooting

Let’s login to our server and make sure our app/process is running: ssh user@host and then you can run pm2 list and you should see your app running:

┌────────────────┬────┬─────────┬──────┬───────┬────────┬─────────┬────────┬─────┬────────────┬─────────┬──────────┐
│ App name       │ id │ version │ mode │ pid   │ status │ restart │ uptime │ cpu │ mem        │ user    │ watching │
├────────────────┼────┼─────────┼──────┼───────┼────────┼─────────┼────────┼─────┼────────────┼─────────┼──────────┤
│       APP      │ 3  │ N/A     │ fork │ 13788 │ online │ 30      │ 0s     │ 0%  │ 832.0 KB   │   user  │ disabled │
└────────────────┴────┴─────────┴──────┴───────┴────────┴─────────┴────────┴─────┴────────────┴─────────┴──────────┘

Perfect! Now every time you want to redeploy your steps would be:

  • Make changes locally and test your application
  • Commit and push your changes to your master origin
  • Run the command pm2 deploy production

You have successfully setup a CI/CD strategy for your node app using PM2. Hopefully this saves a lot of time and ensures your app stays up and running. Until next time, stay curious, stay creative!