Show HN: How to Setup Node.js App Automated Deployment and CI with PM2 for MVP’s

April 2016

tldr; This article shows you how to configure an insanely simple
automated continuous integration and deployment setup for a Node.js
app using GitHub, PM2, Digital Ocean, and
SemaphoreCI. I wrote it because nothing like this in its
entirety exists. It should take you 30 minutes to set up properly.

CI PM2 Node GitHub Server Setup



I’ve used or explored nearly every CI testing tool there is for Node.js
(maybe?). I have tried TravisCI, but grew tired of constant downtime
and slow, very slow build times (…OK, the builds ran fast, but they did not
kick off quickly!). Also I’ve tried CircleCI, but their founders
removed my thoughts from their community because they didn’t
agree to allow the file name for YAML config to be .circle.yml instead of
circle.yml. I also faced troubles while trying to configure and set up
Jenkins (though it was while I was working with an inexperienced
team, whom were the ones setting it up). I’ve also looked at
Shippable, but it really didn’t interest me, just like the rest
– because I now enjoy working with SemaphoreCI
namely since the prodigy TJ Holowaychuk recommended it to me.

For anyone interested in getting into the automated CI deployment business,
it’s relatively straightforward to market yourself – just list yourself
in all the Wikipedia articles, on Quora (with some upvote magic), have a good
service that doesn’t shut down or lie about build times, and have clear docs.
If you do those four things, you’re on the way to at least some passive income!

With regards to server hosting, I chose Digital Ocean because they rock.
I have never had a problem with them in over five years. That’s something!
I also printed t-shirts for Digital Ocean before I sold Teelaunch, and really
liked working with them.

Not only all that, but their service has great uptime, and their boxes
“droplets” are really fast to set up and reliable. I’m not a huge fan of
using Amazon EC2 and AWS in general for building Rapid MVP’s (of course
I would definitely use load balancing or something for scaling an app that has
thousands of users across the world). If your first question about building
an app is “How can I scale it?” or “Will Digital Ocean let me scale?” –
take my advice, you’re doing it wrong. Stop it. Think Rapid MVP.

To put it simply, Amazon has an interface that resembles a wild jungle with
overgrown vines on every tree, and Digital Ocean’s interface is a beautiful
oasis in a vast VPS desert.

As a side note, I can almost guarantee you that sometime in the future,
everyone will want barebones boxes connected to ethernet plugs. Because imagine
when everyone has fiber internet and anyone can host their e-commerce store
from a RaspberryPI running from their kitchen table.

1. Create your Droplet

First, you need a Digital Ocean account. Be patient as their signup process
may require you to verify your email and enter your credit card.

Sign up with this link to get $10 of free credit (2 months of hosting):

When you create your Digital Ocean (“DO”) droplet be sure to only allow SSH
only access and add your SSH key to Digital Ocean. You can do this from DO’s
dashboard and you can find more about this on a Digital Ocean article.

Make sure you create a droplet using the latest stable Ubuntu release.

Digital Ocean Droplet

SSH into the droplet and install dependencies for your stack with Node.
In my case, I needed to install Node, MongoDB, and Redis. Of course, MongoDB
and Redis are optional dependencies, but I use them because they allow me to
build Rapid MVP’s (quick prototypes in other words). Also, I really
like to use NVM to manage various version of Node installed, which
was created by another prodigy, Tim Caswell.

Make sure you replace all instances in this article of droplet-ip-address
with the IP address given to you by Digital Ocean for your droplet.

ssh root@droplet-ip-address

Install the basic requirements needed for the server:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install vim build-essential libssl-dev git unattended-upgrades authbind openssl fail2ban

Install NVM and set it up to use the latest stable version:

curl -o- | bash
nvm install stable
nvm alias default stable

Install PM2, which will handle deployments for us and manage our processes:

npm i -g pm2

Install MongoDB, which is optional:

sudo apt-key adv --keyserver hkp:// --recv 7F0CEB10
echo "deb "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org
service mongod status

Install Redis, which is optional:

sudo add-apt-repository ppa:chris-lea/redis-server
sudo apt-get update
sudo apt-get install redis-server
redis-benchmark -q -n 1000 -c 10 -P 5
source ~/.profile

You might also want to look into installing fail2ban, changing the default
SSH port, and remove password-based login access. You can find how to do this
from this section in my security article, or just Google it.

2. Write your Node App File

This article assumes you already have created a GitHub repository for your
project and that you already have some app.js file in the root of it. If you
haven’t done that yet, then this section is for you. This section also
describes how to configure that app.js file for zero-downtime and graceful
reloading upon deployment of code.

For the purpose of this article, I share a basic app example that will respond
with “hello world” when you visit your droplet later on (over port 3000).

Answer yes to all the prompts or just hit ENTER to breeze through it:

npm init

Now save the basic express dependency:

npm i --save express

Create a new file called app.js (or edit your existing to include SIGINT):

vim app.js
var express = require('express');
var app = express();

app.get('/', function(req, res) {
  res.send('hello world')


process.on('SIGINT', function() {


  setTimeout(function() {
  }, 300);


Let’s test this out locally before you bother to continue further.

node app.js

Visit this URL in your browser (it should say “hello world”):

By default, PM2 will allow 1.6 seconds for your app to gracefully exit,
and you can read more on how to configure your app for zero-downtime here:

3. Set up SSH for SemaphoreCI

First, go to and sign up for an account.

Once you’ve logged in, create a project and connect with your GitHub account.

SemaphoreCI Loading

Make sure that your “Node version” shown under your SemaphoreCI project’s
build settings matches the output from your droplet when you run node -v.

For example, in this screenshot I have selected the v5.8.0 that I’m using.

SemaphoreCI Node Version

Now we need to add a user to the droplet to let SemaphoreCI deploy the app
after all tests have successfully passed.

Keep your SemaphoreCI browser tab open, because we will come back to that
in just a bit!

Copy to your clipboard the contents of your local ~/.ssh/ file.
If you have not yet already created this file, see GitHub’s instructions.

I’m using pbcopy (while on Mac OS X) to make it easy and do it the CLI way:

cat ~/.ssh/ | pbcopy

Now SSH back into your droplet if you’re not still connected:

ssh root@droplet-ip-address

Add the user semaphoreci on the droplet, so you can then SSH in as them.
When you are prompted for a password, write it down or make it memorable.

sudo adduser semaphoreci

Switch user to semaphoreci and paste your clipboard contents into the file
called ~/.ssh/authorized_keys. This will let you test deployments from
your local computer as the semaphoreci user later on. In other words, you
can SSH into your droplet as the semaphoreci user easily. It’ll make sense
later, don’t worry.

su semaphoreci
mkdir ~/.ssh
chmod 700 ~/.ssh
vim ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

We need to create an SSH key for the actual semaphoreci user, so we can then
share the contents of the private key we create on the SemaphoreCI dashboard.

Change directories to your local box’s SSH folder and create a key:

cd ~/.ssh
ssh-keygen -t rsa -b 4096

When you’re prompted to enter a file to save the key, enter the following:


Don’t enter a password for simplicity.

Again, copy the contents of this SSH key now to your clipboard using pbcopy:

cat ~/.ssh/ | pbcopy

Now SSH back into your droplet, switch to the semaphoreci user (see above),
and add this as a new authorized key to that same file you created earlier
(and added your own SSH key into). You should add it as the next line in the
file on your droplet at /home/semaphoreci/.ssh/authorized_keys.
This will allow SemaphoreCI access to your droplet later on:

ssh root@droplet-ip-address
su semaphoreci
vim ~/.ssh/authorized_keys

Now go back to that browser tab you have open for SemaphoreCI, and click
on the link for “Set Up Deployment”. This link is found on the page that
looks like this:

Semaphore Settings

It will then present you with options to choose from. Scroll down and select
the option titled “Generic Deployment”, and then click “Automatic”. You should
now be on a screen that looks like this:

Semaphore Deploy Commands

Add the following deploy commands where it says “Enter your deploy commands”:

Make sure you replace droplet-ip-address with the IP address of your
Digital Ocean droplet. Also, if you changed to a non-standard SSH port, change
where it says 22 in -p 22 below.

npm i -g pm2

ssh-keyscan -p 22 -H droplet-ip-address >> ~/.ssh/known_hosts

pm2 deploy ecosystem.json production

After you enter this command, it will now prompt you to paste in the value
of the private key file for the semaphoreci user. You don’t have this on
your clipboard yet, so you need to use pbcopy again locally:

cat ~/.ssh/semaphoreci_id_rsa | pbcopy

Paste the contents of your clipboard in the box shown in this screenshot:

Semaphore Private Key

If you want to easily simulate SemaphoreCI logging in as the semaphoreci user
then you can do this by the running following from your local box:

ssh -i ~/.ssh/semaphoreci_id_rsa semaphoreci@droplet-ip-address

You can also do this command much easier by creating a file on your
local box called ~/.ssh/config with these contents (replace your droplet IP):

Host semaphoreci-droplet
  Hostname droplet-ip-address
  User semaphoreci
  ForwardAgent yes
  Port 22
  IdentityFile ~/.ssh/semaphoreci_id_rsa

Then you can just run ssh semaphoreci-droplet and save a bit of typing.
Note that I left the line Port 22 in there in case you change your SSH port.
The line that says ForwardAgent yes means it forwards your SSH agent.

I’d highly recommend you test this out right now to make sure it’s set up OK.

4. Add new GitHub Deployment Key

Since we have a semaphoreci on our droplet, we now need to add a deployment
key on GitHub for our project, so that we can test deployment locally.

SemaphoreCI already has added a deployment key for your project (if you set
it up correctly), so don’t be alarmed if there’s already a key created when
you get to the GitHub Deployment Key settings page for your repo. You’ll be
creating another one for local testing purposes, don’t worry!

First SSH into your repository as the semaphoreci user:

ssh semaphoreci-droplet

Now create an SSH key pair:

cd ~/.ssh
ssh-keygen -t rsa -b 4096

When it asks you where to save the file, use the default and hit ENTER.

Don’t enter a password for simplicity, again.

Go to and click on your project, then go to its Settings.

Under “Deploy keys” add a new deployment key, allow it write access, and
paste the public key file’s content we just created. To easily
get the contents of this public key on your clipboard, from your local box
run this command:

ssh semaphoreci-droplet "cat ~/.ssh/" | pbcopy

Here’s the screen showing where you enter your key. Don’t be alarmed if you
already see a Deploy here in here; it’s supposed to be there, as it was added
automatically by SemaphoreCI in a previous step (yes, you’re adding another!):

GitHub Deployment Key

If you get stuck on this step or need more instructions, see this article:

5. Share /var/www Access

We created the user semaphoreci in the previous section, and now we need
to give it recursive read and write access to the /var/www folder on the
server – so that the pm2 command can deploy to the server (from both
our local box if we want to deploy manually, and also from SemaphoreCI’s
environment for the automated continuous integration deployments).

We need to SSH into the droplet as the root user, so we can then add this
folder and then give permissions on it to the semaphoreci user.

ssh root@droplet-ip-address

Now create the folder using sudo:

sudo mkdir /var/www

To stay in compliance with standards used widely by infrastructure teams,
we’ll use the classic www-data group to manage permissions on this folder.

Add the semaphoreci user to this group:

sudo adduser semaphoreci www-data

Change ownership of the folder and its files recursively:

sudo chown -R www-data:www-data /var/www

Grant the group read and write permissions (say that phrase five times fast!):

sudo chmod -R g+wr /var/www

That’s all.

If you wanted to test it out, then SSH in as the semaphoreci
user, and try to run the command touch /var/www/test.txt. It should let
you create a blank text file in that folder as the semaphoreci user. If you
did not do this properly, then you will encounter the following read/write
error later on:

pm2 deploy ecosystem.json production setup
--> Deploying to production environment
--> on host droplet-ip-address
mkdir: cannot create directory ‘/var/www’: Permission denied
mkdir: cannot create directory ‘/var/www’: Permission denied
mkdir: cannot create directory ‘/var/www’: Permission denied

6. Configure PM2 for Deployment

We’re going to set up a configuration file to be read by PM2.

On your local box, make sure you have pm2 installed globally:

npm i -g pm2

Create a new file in the root of your GitHub project called ecosystem.json.

vim ecosystem.json

Note that you can automatically create this file (with defaults) from
PM2’s CLI using pm2 ecosystem, however for the purpose of this article
I’m providing you with the content here. You need to replace the following:

  • droplet-ip-address with your droplet’s IP
  • repo property value with the path to your GitHub repo
  "apps": [
      "name": "App",
      "script": "app.js",
      "exec_mode": "cluster",
      "instances": "max",
      "env_production": {
        "NODE_ENV": "production"
  "deploy": {
    "production": {
      "user": "semaphoreci",
      "host": "droplet-ip-address",
      "ref": "origin/master",
      "repo": "",
      "path": "/var/www/production",
      "post-deploy": "npm i && pm2 startOrGracefulReload ecosystem.json --env production",
      "forward-agent": "yes"

If you need a reference for the options here, see the official docs here:

Note, if you have a custom port, you’ll need to add that as a "port"
property in your ecosystem.json‘s deploy nested object for each env.

Now run setup for deployment with PM2 using the CLI command, and make sure
you run this command from the root of your project’s folder locally:

pm2 deploy ecosystem.json production setup

You could (for fun) try running this command twice. If it worked the first
time, you will get an error on the second try; it will say the folder exists
already at the path /var/www/production!

Go ahead and deploy the production environment and start its processes:

pm2 deploy ecosystem.json production

You can test it out at the following link (replace with your IP):

If all is OK, then make sure that PM2 is scheduled to
startup automatically if your server reboots or something happens.

Make sure you run this command as the semaphoreci user on the droplet:

ssh semaphoreci-droplet
pm2 startup ubuntu

It will give you output which you will then need to run as a user with root
access, which you can get by running:

ssh root@droplet-ip-address
sudo su -c "env PATH=$PATH:/usr/local/bin pm2 startup ubuntu -u semaphoreci --hp /home/semaphoreci"

Now save the current processes to automatically restore them if your
server reboots or something happens. To do this, first make sure we have PM2
processes running that we’ll be able to save:

ssh semaphoreci-droplet
pm2 status

If no processes appear, go back to the section with PM2 deployment commands,

If your processes appear, then run this command as the semaphoreci user
on the droplet, so that these processes will get restored if something happens:

ssh semaphoreci-droplet
pm2 save

All done! Now try to commit some code and watch SemaphoreCI deploy it for you.

For example, you could make it say “thanks nifty” instead of “hello world”:

vim app.js
app.get('/', function(req, res) {
-  res.send('hello world')
+  res.send('thanks nifty')
git add .
git commit -m 'testing out semaphoreci automatically deploy my project'
git push origin master

Now just wait and watch the SemaphoreCI dashboard. It will run a build,
then it will deploy it to your Digital Ocean droplet for you using PM2.

If you want to see the pm2 save do its magic, then just run sudo reboot,
or reboot your droplet from Digital Ocean’s interface. When it powers back on,
SSH into it as semaphoreci, and run pm2 status to see your app is running.

7. PM2 Deployment Commands

This documentation is sourced directly from Keymetrics Blog and
also from the official PM2 deploy documentation.

pm2 deploy production update

pm2 deploy production revert 1

pm2 deploy production exec "pm2 restart all"

This deploy command option is inspired from TJ’s deploy shell script at:

Notes | Github | Twitter | Updates | RSS/XML FeedPowered by Wintersmith

Original URL:  

Original article

Comments are closed.

Proudly powered by WordPress | Theme: Baskerville 2 by Anders Noren.

Up ↑

%d bloggers like this: