Docker and Node


How I build for Node and Docker.

I’ve been wanting to write about this subject for awhile. By now everyone knows what Docker is and how to use it. If you don’t yet, then some of this might seem weird but follow along anyways. 

We started very simply, we created our Dockerfile in our root project for our new Node.JS API. The Dockerfile imports directly from the node repository.

FROM node:6.4.0

We use that as our base image. You can find more information about the image from the Node.JS DockerHub. The next process we do once we have our image is we update and install our image magaick libraries. We need this to do photo resizing for user profile images. That might be saved for another day, but it requires quite a few libraries.

RUN apt-get update -y

You’ll notice that we run the -y switch on our apt-get commands. This is simply to tell apt-get to say Yes that we would like to install the updates without having to type “Y”.

RUN apt-get install -y imagemagick libmagickcore-dev libmagickwand-dev && apt-get install -y webp libwebp-dev

The next portion we copy our package over to the src directory. This is important because the src directory will contain all of our application API code. 

COPY package.json /src/package.json
RUN cd /src; npm install
COPY . /src

We run our NPM install like normally what you would do on a deployment. This reads the package.json we copied over earlier. 

Expose can actually take multiple port numbers. Which you might need to do. The easiest way to do it is just tab over from port 80 and type the other port number. 80 1433

EXPOSE 80

Last thing we do is we execute our server.js file which is main js file for the service. CMD tells Docker to run a command in the container. Since we are running just an API server, we only need to tell the node command which file to use.

CMD ["node", "/src/lib/server.js"]

If you are using npm scripts, this might look like

CMD["npm", "start"].

Environment variables are setup on a different task using Amazon Elastic Container Service. Here is what the entire Dockerfile ends up looking like.

To build the image with Docker you can execute just by 

$ docker build .

or 

$ docker build -t <name> .

The -t will add a tag to the name of the docker image, this makes it easy to find later. Once you have your Docker image you can deploy it anywhere that supports Docker. We have a Jenkins CI server which does this automatically for us when new code is changed in our GitHub account. 

The only thing I warn people about is that the images can be rather large. If you are constantly building images you might run out of hard drive space. Luckly it’s pretty easy to remove unwanted images using docker rmi command.

If you want to see how many docker images you have:

$ docker images

If you want to remove them 

$ docker rmi <image_id>

How to deploy to Amazon ECS.

I mentioned earlier, this can be kind of tricky to setup. We use Jenkins to automatically build our docker images. The docker images are then pushed up to Amazon ECR. Which is just like a holding place that stores all of our docker images. We use this instead of keeping them on Jenkins because it’s easier for docker to deploy from a registry.

This task generally takes about 5 minutes. It takes time to build but when you go to deploy it, it only takes 30 seconds.

Seperate the building of images with your deployment.

We seperate out our building of images and our deployment into two jobs. You could do them in one job but for us it makes more sense to have a seperate job. This gives us the ability to use a parameter in our jenkins job to tell it what version of the image we want to deploy.

The reason, is mostly because we might notice a bug in the last minute after we push up code. Maybe we have multiple developers working on a feature and we have code changes that need to go out after one. We found it more valuable to have the ability to decide what image of the code we wanted out there.

The jenkins job we use to deploy takes in a parameter called DockerTag. This is the same tag that the was pushed up to our Amazon Docker ECR. It was just called ${IMAGE_VERSION}. This is essentially just the build number from Jenkins. This makes it easy to find what image to deploy because the developers just have to look at what the jenkins job number is.

This next part shows an example of how we interface with Amazon ECS and how we update tasks in the cluster to tell it to deploy our specific docker image.

Thats about it from the Jenkins side. You can see from the commands that it gets the current task, stops it and registers a new version of the task definition from a file that is created. Once the task definition is registered, the service in the cluster is updated with the new revision. Then the task definition file is removed since it’s no longer needed. Once this job runs it might take 2-3 minutes for the cluster to be fully updated with the new image.

Task Definition

Inside of the software project we have a directory called tasks which contain a task definition. It’s essentially a file that contains the environment variables and port mappings. It also contains the name of the image we are going to use from the ecr.

Here is what it looks like:

When Jenkins runs a new build it creates a new version of this task definition file. It replaces the %IMAGE_VERSION% tag DockerTag with the one provided in Jenkins as a build parameter. The next part of the task definition responsiblities is setting up the cpu and memory params. Node isn’t very high in CPU and needs much more memory. You can configure these as how you would need them for your service.

The task definition is also handling the port mappings to the docker container. The ELB might be configured to run on port 80. If our dog service runs on port 1820 we could tell Amazon we need 80 to forward to that port in our docker container by using the information in the task definition.

The last part of the task definition is using environment variables. Environment variables are configured in the task definition, this is nice because you can use the same docker image for different environments just be configuring the image to use environment variables. You would probably want to find a more secure way to setup your db username and password but for the example it doesn’t matter.

You could create a seperate task definition for each environment if you want or just re-use the same one and replace the environment variables by some other process. It’s pretty flexible.

Conclusion

I hope this gives you some help and insight into how to use Amazon ECS and deploy docker images. My experience is it’s kind of weird when you first start but it gets easier as you learn more. I wish there was more visibility and logging inside of ECS. Amazon ECS has really made it easy for me to get started deploying my own Docker images. They also give us the ability to scale based on load which is pretty fantastic for me.