Deploying with private NPM and Docker

When you want to deploy your nodejs webapp to a production environment, it can still be a right pain to handle dependencies!

Let NPM handle it

The simplest way to manage dependencies is to run npm install on your production server when you deploy. There's a bunch of problems with this though:

  • Between testing and deploying code, the package version might change! This will inevitably break something eventually - it certainly has for us.
  • If npm is ever down, you can't deploy code!
  • You can't take advantage of nodejs's module system - if you make your own internal modules, you'll have to check them in to your sourcetree, which makes them really hard to reuse between projects.

You can npm shrinkwrap your project to solve some of these problems. We've avoided shrinkwrap because until recently module maintainers could (and would) sometimes unpublish & republish modules. So we still weren't guaranteed that we were running the same code in staging & production environments. Recently, npm has stopped allowing package versions to be reused. If a maintainer decides that a package is broken, they can still unpublish it and your deploy will unexpectedly break.

Checking in modules

Another option is to check in your node_modules directory to git. We did this for about a year at Lever. It solves the problem of reliable deploys because you're deploying exactly the same code you're developing & testing with. You can also add private modules managed by npm using git. Eg:

"dependencies": {
  "lever-components": "git+ssh://[email protected]:lever/components.git",
  ...
}

Unfortunately, checking in modules creates its own set of problems:

  • We found ourselves running dozens of out of date packages. After awhile, you start getting nervous updating them all.
  • Some modules get compiled when they're installed. We develop on MacOS and deploy on linux. Some modules ended up compiled for macos, and some for linux. npm rebuild helps here, but you have to run it all the time - and when you do, git diff becomes huge and you risk conflicts if you commit it.
  • Our git tree grew rediculously quickly, and git got a lot slower. (It had to traverse all our dependencies to figure out what changed).
  • If we update one of our own modules, we needed to npm install it in about 6 different places (all the deployed products). Inevitably you'd forget one of those places, and one of our deployed artifacts would be running old code. If we changed any APIs, it became 10x worse because you would have to change and test every usage of the module.

If only nodejs had a solution...

Oh wait. Duh. Its called npm.

In my opinion, npm (combined with semantic versioning) is the best part of nodejs. If you can't take advantage of it you're missing out on a fantastic dependency system. Our current system uses sinopia combined with docker to handle versioning and deployment. It works like this:

NPM through Sinopia

The nodejs way is to write tiny modules for everything. Your code should be included in this. All modules should be versioned, and you bump the major version whenever you make API-incompatible changes.

To (privately) publish and access the modules, we put them all in our own private npm registry. Sinopia is perfect for this:

  • Lever modules are prefixed with lever- or l-. Our developers can publish them directly to our npm registry.
  • For any other module, sinopia will act as a caching proxy and fetch the module from the official npm servers.

Our sinopia configuration file looks like this:

packages:  
  'lever-*':
    allow_access: &USERS
      - sexy
      - people
    allow_publish: *USERS
    storage: 'lever'

  '*':
    allow_access: all
    allow_publish: none
    proxy: npmjs

Our developers simply tell npm to use our private registry. Everything works just the same, except they can install our private modules.

Well, almost everything - the one downside is we need to switch back and forth to publish to public npm. (Things like derby and sharejs. We use some clever shell aliases to make it easier.

Docker

However, modules might still get updated between when we test on our stage environment and when we deploy! Docker solves this beautifully - we run npm install as part of the docker build process, so the node_modules get baked into the docker image.

Not only do we run the same code in stage and production as a result, but we can also do perfect rollbacks by deploying previous docker images.

Here's an abridged version of our Dockerfile:

FROM node

# Exclude npm cache from the image
VOLUME /root/.npm

# Use private npm
ADD /conf/private_npmrc /root/.npmrc

# Add source files & deps
ADD . /var/lever/coolproduct  
WORKDIR /var/lever/coolproduct  
RUN npm install

ENV NODE_ENV production

EXPOSE 4321

CMD ["/usr/local/bin/node", "/var/lever/coolproduct/server.js"]  

This is pretty standard for a docker file. We point to our private npm registry by adding the .npmrc file before doing npm install.

We've also made some other docker related tweaks:

  • The VOLUME /root/.npm line puts the npm module cache on the build server's hard disk. This lets npm reuse its module cache with each build (making builds faster), and the cache doesn't end up in the docker image (nearly halving the size of the docker image).
  • You should always use CMD ["node", "xyz"] notation instead of CMD "node xyz". This gets around a bug where sometimes SIGTERM signals won't be passed into the image, and docker stop will be really slow. (It has to wait for an ineffective SIGTERM to timeout, and then SIGKILL your process).
  • Its not shown here, but you should add a .dockerignore file which excludes your .git directory from the docker image. This saves a lot of space!

This infrastructure took us quite a long time to settle on, and now that we're using it we're all amazed how much easier its becoming to push code. Its still not perfect, but its dozens of times better than it was before. I've said it before and I'll say it again - the best part of nodejs is npm (with semver). You want to be using node packages internally in your company. Using the right tools for deployment will make your life a whole lot easier.

If you think this stuff is cool and want to help make it even better, lever is hiring! Come help out!