How to automate the deployment of a Node.js app using Shipit.js

I love automated deployments as they are great to make deployments quicker and more importantly, safer. They are used to make app deployment as simple as few command lines and make it easy to manage multiple versions of an app for reversion if necessary.

I use Capistrano for deploying Ruby code and then sought a JavaScript alternative that was as simple and could perform the same task. As a note, the language you use to perform automated deployment does not matter. However, it is preferable to write deployment scripts in the same programming language as your code.

This tutorial is to guide Node.js app programmers to automate their app deployment. It starts with a source code copy to other useful processes:

  • Managing and restarting daemons
  • App setup

The deployment is done using Shipit package. This tutorial assumes that you are familiar with the basics of setting up a Node.js application on a server, including familiarity with:

  • Node.js setup
  • Git
  • SSH
  • Web server (Apache/Nginx)
  • Daemon (PM2/forever)

Prerequisite

You would first have to make sure that you can SSH into your server from your local machine and ensure to add the public keys.

It is also preferable to use an SSH remote URL git@bitbucket.org:companyname/my-repo.git for the source code repository as opposed to an HTTP remote URL https://username@bitbucket.org/companyname/my-repo.git that would require a password that adds a step to the automated deployment. You can check and change the Git remote URL using:

git remote -v
git remote set-url origin git@bitbucket.org:companyname/my-repo.git

Installation

The first step would install Shipit deployment tool

npm install --save-dev shipit-cli
npm install --save-dev shipit-deploy

Deployment file

Then create a ship shipitfile.js in the root of your project. The following is the base code to add to the config that needs to be changed.

// shipitfile.js
module.exports = shipit => {
    // Load shipit-deploy tasks
    require('shipit-deploy')(shipit)

    shipit.initConfig({
        default: {
            deployTo: '/var/apps/my-project',
            repositoryUrl: 'https://github.com/user/my-project.git',
        },
        staging: {
            servers: 'deploy@staging.my-project.com',
        },
    })
}

Set the deployTo to the path for the code on the server. Usually it is /var/www/my-project. Create the directory if it does not exist, and ensure that your user has access and ownership to that directory. To change the ownership, you can run the following on the server:

cd /var/www
mkdir my-project # Might have to sudo
sudo chown -R $USER:$USER ./my-project
  

Next, change the repositoryUrl to your repository. This config sets Shipit to pull the code from the remote repository to a temporary location in your machine and deploys it to your server. You can customize the temporary path to store in your device in the config but can ignore it as it doesn't matter.

Lastly, is to set your server SSH login credential under servers. You can set different servers for each environment. Use the default for any config that shared between each environment.

shipit.initConfig({
    default: {
        ...
    },
    production: {
        servers: 'user@mysite.com',
    },
    staging: {
        servers: 'user@stage.mysite.com',
    },
    development: {
        servers: 'user@dev.mysite.com',
    },
});

Run deployment

To run the deployment, use the commands.

npx shipit staging deploy
  

On deployment, the files from your source code repository are pushed from the temporary directory in your local to your server in the path specified in the config file. However, Shipit creates multiple directories in the path to store different releases. The name of the directories follows the date time.

/var/www/my-project
-- current -> releases/20190515072707
-- releases
    -- 20190515071949
    -- 20190515072226
    -- 20190515072707

The current symlink references the latest release. The symbolic link makes it easy to rollback, and change the release version.

Rollback

As Shipit keeps files from the previous releases, it is easy to rollback in case of any problems with the latest deployment. Run the following to rollback.

npx shipit staging rollback
  

The number of previous releases to keep can be configured in the Shipit config file, or ignore this to keep it to the defaults.

shipit.initConfig({
    default: {
        ...
        keepReleases: 5,
        deleteOnRollback: false
    }
}

Setup

After performing the initial deployment and copying the source files to the server, you have to perform the necessary installs and server restarts. You have to configure this part of the deployment as it is different for every application and setup. This guide shows how to perform an automated Node.js app install and PM2 daemon start or restart, and you can implement the same concept with whatever daemon or stack you use.

Add an on deployed listener to your config to listen for the end of the code copy to start the install task.

shipit.initConfig({
    # ....
});

// Add this
shipit.on('deployed', () => {

});

The command that you run to start or restart the daemon process would depend on the environment for which NODE_ENV to set. Here is a generic install I use.

shipit.on('deployed', () => {
    const processName = 'pm2-process-name';
    const env = shipit.environment;

    let cmd = `
        cd ${shipit.releasePath} && npm install --production && 
        (
            pm2 restart ${processName} ||
            NODE_ENV=${env} pm2 start server.js --name ${processName}
        )
    `;

    shipit.remote(cmd);
});

The code runs a production install in the release directory and then attempts to reload the PM2 process assuming there is one running, otherwise, if there is no process running it would start the process depending on the environment.

You can use the && to chain commands and the || enclosed in brackets as an if else. In the last line of the console commands, you can view that it attempts to restart the process and if there is no process running, PM2 throw an output error, and it proceeds to start the process.

In case you dislike to have too long of a multi-line string, here's another way to write the above script that might suit your taste better.

shipit.on('deployed', () => {
    const processName = 'pm2-process-name';
    const env = shipit.environment;

    let cmd = '';
    cmd += `cd ${shipit.releasePath} && `;
    cmd += 'npm install --production && ';
    cmd += `(
        pm2 restart ${processName} || 
        NODE_ENV=${env} pm2 start server.js --name ${processName}
    )`;

    shipit.remote(cmd);
});

The daemon restart updates your application. Now there is a complete basic deployment script for a Node.js application.

Conclusion

Automating code deployment makes your app updates consistent and reliable. It is good practice to automate what you can to avoid doing redundant work.

However, it is essential to understand what to automate. Therefore, it is crucial to begin to implement the manual and old-fashioned way of deploying first. Once the manual way has hit a point where it is no longer a learning experience and is more of a hindrance, then implementing automated deployment is the obvious next step.

Wei-Ming Thor

I write guides on Software Engineering, Data Science, and Machine Learning.

Background

Full-stack engineer who builds web and mobile apps. Now, exploring Machine Learning and Data Engineering.

Writing unmaintainable code since 2010.

Skill/languages

Best: JavaScript, Python
Others: Android, iOS, C, React Native, Ruby, PHP

Work

Engineering Manager

Location

Kuala Lumpur, Malaysia

Open Source
Support

Turn coffee into coding guides. Buy me coffee