How to Containerizing Node.js Apps with Docker and StrongLoop

5/5 - (1 vote)

BestASPNETHostingReview.com | Best and cheap Node.js hosting. Here is the newest addition: the ability to deploy apps on Process Manager as containers, using Docker.

This post outlines challenges with Docker, especially when you are “Dockerizing” your Node applications, demonstrates some failed attempts at trimming it down, and shows how you can use StrongLoop Process Manager to containerize your Node apps with a drastically reduced footprint!

docker

Containerizing Apps The Hard Way

Anyone who has heard of Docker has probably spent at least a few seconds at least thinking about containerizing their app. If you are like me, you probably scanned the docs and decided you didn’t have time to figure it out, and that was the end of it.

And then I went back a month later and got a little bit better idea of what this Docker thing is, but ran out of time again. And then I repeated the process a couple more times until I finally had an idea of what Docker is and how I could use it to streamline some of my app deployments.

So then I started “Dockerizing” my app. It isn’t too hard: there is an example of Dockerizing a Node.js Web App on the Docker website that covers the basic steps.  Unfortunately, it uses an outdated version of Node and npm because it uses the RPMs provided by CentOS.  It turns a single-file node package into a ~550MB image. Disk is cheap, as they say, but anyone who has used Docker a while  knows that it really likes to fill up your hard drive.

Trimming it Down

I’ve copied the index.js, package.json, and Dockerfile from that guide and put them in a directory, then ran docker build -t node-experiment:original. to get my baseline and then ran a Docker images node-experiment to get a list of the images in the node-experiment repo:

OK, let’s trim this down a bit, and see if we can make it a little friendlier to distributing across a network, or over the Internet.

First we need to figure out what’s taking up all this space. Here are some of the larger items in the resulting image:

  • 66M /var/cache/yum
  • 95M /usr/lib/locale
  • 11M /usr/lib/gcc
  • 62M /usr/share/locale

And those are just the directories that are easy to clump together, but it’s a good start. We can do some experimenting and add the following lines to the Dockerfile from that tutorial:

Now we rebuild our image, docker build -t node-experiment:take-2 .. Notice that it doesn’t take as long the second time. That’s because it is able to re-use the layers from the previous build.

Now look at the image again, with Docker images node-experiment, and revel in our genius:

Docker Images are Like Ogres

Remember how the second build went faster because it was re-using the layers from the previous build? Every line in the Dockerfile is creating one of those layers, including those rm -rf … lines we added. So those lines aren’t actually deleting anything, they’re adding another layer that applies a mask to “delete” the given files on the resulting multi-layer file system. If you’re thinking that actually uses more space than it saves, you’re right. If you’re thinking this sounds a lot like what happens when you commit a large file to Git and then later remove it, but your repo doesn’t shrink, you’re right again.

So what can we do about this?

Notice we had to do some rearranging so that we copied the app over before installing Node. That means our speed-up from layer re-use is going to disappear since it will have to re-install Node every time. Mildly annoying, but if the image is significantly smaller, it might be worth the extra minute it takes to rebuild the image. Let’s give that a try and tag it as node-experiment:take-3.Notice we had to do some rearranging so that we copied the app over before installing Node. That means our speed-up from layer re-use is going to disappear since it will have to re-install Node every time. Mildly annoying, but if the image is significantly smaller, it might be worth the extra minute it takes to rebuild the image. Let’s give that a try and tag it as node-experiment:take-3.

Not bad! Let’s take it one step further and forget about optimizing for layer re-use at all and move all the RUN commands into a single layer and tag it as node-experiment:take-4.

So we’ve managed to hack about 200MB off the size of the image. But what’s left in our image?

  • Node
  • npm
  • Our app
  • Mangled installation of gcc
  • Broken locale data

A Better Way

If you’re an avid Git user you might be wishing for a Docker rebase command right about now. Sadly, there is no such command. So what commands are there? Here’s some useful-looking lines from docker help:

It looks like with sufficient scripting we could build our image without even using a Dockerfile. I’ll leave it as an exercise for the reader to come up with a way of tying these commands together to produce an image. Instead I’ll describe the general approach I took with StrongLoop’s Process Manager:

  1. Create a build container with Node, npm, and a complete build toolchain.
  2. Install strong-supervisor (for clustering, control channel, metrics, etc.)
  3. Import your app and install its dependencies, including binary addons.
  4. Create a deployment container with no Node, npm, or build tools of any kind.
  5. Copy Node, strong-supervisor, and your app into the deployment container.
  6. Commit the deployment container as an image.

I used the strong-docker-build module to build the one-file app from above for comparison:

Looks like success! Not only is it smaller, but it has an embedded process supervisor with automatic clustering and metrics reporting, and no extra weight from compilers for languages we don’t need.

How to Use It

This is my favorite part. Now that you have some insight into the problem and how to deal with it the hard/wrong way, here’s how to solve it the easy way using StrongLoop PM:

  1. Install Docker on your server (same as installing the Dockerized StrongLoop PM).
  2. Install StrongLoop PM (same as installing a production server):
    sudo sl-pm-install --driver docker (the new hotness).

Now, whenever you deploy an app to StrongLoop PM on this server, your app will be turned into a Docker image using the approach described above and then run in a Docker container.

If you’re on a Mac and you want to experiment with the new Docker driver, fear not! While the sl-pm-install command is Linux only, the sl-pm and slc pm commands also accept the --driver docker option, and it works right out of the box if you have a working boot2docker installation.

However you install it, you’ll notice the first startup takes a little longer than usual as it pulls down official Debian and Node images from Docker Hub. You can speed this up by pulling them ahead of time yourself with this command:

Docker in Docker

While it is possible to run Docker inside Docker, or connect to a Docker daemon from within a Docker container, this first release doesn’t support it. At the time of this writing, that means the --driver docker option is not compatible with running StrongLoop PM itself as a Docker container.

Whats Next

We’ll continue to develop StrongLoop tools into a full-fledged orchestration and deployment solution for production.