Shipit is... awesome.

Really, though. I've found this to be a fantastic tool for managing my release process. Albeit I'm only using this for my personal website at this time, I can see how I'd use this for continuously deploying at my day job. Over there we use Capistrano, and there's many parallels between the two. Let's start by looking at how it works, and then we'll compare.

How it works

Chances are you're familiar with grunt or gulp. Aside from them both being build task managers, they both are common in that they have a file that you use to define the tasks, a gruntfile or a gulpfile, respectively. When a task is ran, the file is located and the referenced task becomes executed. Shipit works the same way with a shipitfile and a command line interface.

First, make sure you have shipit installed

npm install -g shipit-cli

And once we get to the shipitfile, you'll be able to run deployments as you would run a build with grunt or gulp.

shipit production deploy

This command is made up of three things:

  1. shipit -- the executable
  2. production -- the environment
  3. deploy -- the task

Shipitfile

The first thing you're going to do is create a file called shipitfile.js. Let's look to the simple example from the readme.

module.exports = function (shipit) {
  shipit.initConfig({
    staging: {
      servers: 'myproject.com'
    }
  });

  shipit.task('pwd', function () {
    return shipit.remote('pwd');
  });
};

This, again, is pretty similar to both grunt and gulp.

Grunt similarities

It's similar to grunt because we declaratively create a configuration for the environment. Just like grunt, we can define multiple configuration modes in addition to a set of defaults.

shipit.initConfig({
  default: {
    foo: 'bar',
    hello: 'world'
  },
  staging: {
    foo: 'BAR'
  },
  production: {
    hello: 'WORLD'
  }
});

You get the gist. If we were to run shipit staging XYZ, it's going to use the staging configuration rules first, and then the default to default to the rest of the configuration. If we were to run shipit production XYZ, same idea -- it'll use the production configuration followed by coalescing with the default configuration.

Gulp similarities

It's similar to gulp because we functionally compose our tasks.

shipit.task('createFile', function () {
  return shipit.remote('touch helloworld.txt');
});

shipit.task('writeFile', function () {
  return shipit.remote('echo "hello, world!" > helloworld.txt');
});

These will run just like how you might run a gulp task -- shipit production createFile and shipit production writeFile.

And, you may have guessed it, yes, you can define dependency tasks for other tasks like so:

// createFile will run as a dependency before `writeFile` executes
shipit.task('writeFile', ['createFile'], function () {
  return shipit.remote('echo "hello, world!" > helloworld.txt');
});

Why is this so familiar? They both use Orchestrator for sequencing and executing their tasks. So whatever you're familiar with in gulp, you'll feel at home here with shipit.

You can think of this almost like a gulp file for your server deployments.

Deploying releases with shipit-deploy

Shipit, on its own, is not a fully integrated release manager. It is an underlying set of organized tools to facilitate with executing remote commands and performing rsync to deploy files, but out of the box it is not a release manager.

To take advantage of continuous releases with Shipit comes shipit-deploy. This can be thought of as a predefined set of tasks that take care of building and churning a set of releases. This is where things start to get similar to Capistrano.

First, let's take a look at the shipitfile that shipit-deploy provides as an example in their readme.

module.exports = function (shipit) {
  require('shipit-deploy')(shipit);

  shipit.initConfig({
    default: {
      workspace: '/tmp/github-monitor',
      deployTo: '/tmp/deploy_to',
      repositoryUrl: 'https://github.com/user/repo.git',
      ignores: ['.git', 'node_modules'],
      keepReleases: 2,
      deleteOnRollback: false,
      key: '/path/to/key',
      shallowClone: true
    },
    staging: {
      servers: '[email protected]'
    },
    production: {
      servers: '[email protected]'
    }
  });
};

That's it. Really. There's nothing more that you have to do than specify your repository, the path on the server you want to deploy to, and the server itself. If you have a well prepared environment, such as having configurations in ~/.ssh/config, then you don't even need to specify the key location or the ssh username. Let's look at what each property does starting with the most important pieces.

deployTo

This is the remote path on your server where you want your releases to live. It will not be your servable directory. We'll get to the directory structure that gets generated below.

repositoryUrl

You're likely deploying from version control. This would specify where that lives.

staging.servers, production.servers, etc

First, this is plural because this can be a collection. This is useful if you have multiple instances of your application behind a load balancer. This is the hostname of that(those) box(es).

key

This is the path to your private key file. I'd suggest leaving this up to ~/.ssh/config so that your deployment can be flexible and agnostic to the environment its run on. If you use CircleCI, it will automatically configure a hostname when you set up private SSH keys.


Onto the less important configuration details

workspace

This is not very significant, and it should probably live in /tmp so it will certainly become removed when your reboot. This is simply the "workspace" where shipit will deal with fetching and preparing files to sync remotely.

ignores

This is a collection of paths, and I believe expressions, that you want your deployment to exclude

keepReleases

You can specify how many historical releases you want to to live on your server. We'll go into detail about how this is set up below, but the gist is that you'll want atleast a few that you can rollback to.

deleteOnRollback

This is pretty self explanatory -- if you rollback a release, do you want it to also become deleted?

shallowClone

Most of the time this will be true. This simply refers to the depth of the git clone. Typically for builds you're going to be deploying the HEAD commit and you won't really need any history of revisions.

You can find the whole set of options here.

Release structure

As hinted above, your deployment is not simply a set of files that gets thrown up a directory served by your webserver. This is where things get very similar to Capistrano's structure. Your deployment safely creates a release directory sibling to previous release directories, and simply rotates a symbolic link used by your webserver which points to the latest release. This makes for zero downtime, and it allows you to rotate -- forward or backward.

Shipit release structure

Serving the right directory

Shipit is going to create a symlink to the latest release, or the latest rollback, named current/.

If your deployTo path is /www/application, then your latest release will always live in /www/application/current. If you serve your application from the root of the project, then current/ is where you're going to want to point it. If, say, you serve it out of a subdirectory, public/, then /www/application/current/public is where you'll be serving from

Releases

As seen in the release structure there is a directory of releases/ with many sibling releases of directories named with timestamps. The current/ is always pointing to the latest release, or the latest rollback. Depending on what you set for keepReleases, that will be the number of releases that are kept. As you can assume, Shipit uses a FIFO approach to keep rotating the oldest releases off of your server.

As to clear up any misunderstanding, if we were to look in one of these directories, you'll find your deployed application.

Shipit release content

Real world examples

If you'd like to see how I used Shipit with CircleCI and Jekyll to deploy this website, you can read me other post about continuously deploying Jekyll with Shipit, or simply take a look at the repository.