This post is outdated as of October 2016. Refer to the Docker Project Boilerplates for updated versions.

I recently shared a post on Dockerizing Ruby applications for TDD & Deployment. This post continues the theme by introducing a similar structure for Node.js applications. The general approach goes like this:

  1. Use docker to generate node_modules from package.json and commit source control.
  2. Use node_modules to create a docker image
  3. Use the previously built docker image to run arbitrary code w/o rebuilding the docker image (only useful in TDD).

The structure is available as a cloneable boilerplate repo. The rest of the post describes the process in-depth.

Start by creating a package.json. I will assume you need some testing libraries.

{
	"name": "docker-node-boilerplate",
	"version": "1.0.0",
	"description": "Slashdeploy Docker & Node.js boilerplate",
	"main": "index.js",
	"dependencies": {
		"mocha": "~2.4.5",
		"jshint": "~2.9.2"
	},
	"scripts": {
		"test": "mocha -u tdd test/*_test.js",
		"lint": "jshint src/*.js test/*.js"
	},
	"author": "",
	"license": "ISC"
}

Next create the runtime environment from the dependency information. This is done running npm install inside a docker container. Next node_modules is tar‘d into archive and committed to source control1. Here’s an excerpt from the Makefile.

.PHONY: foo
$(PACKAGE): package.json
	mkdir -p $(@D) tmp/cache
	docker run --rm \
		-v $(CURDIR):/data \
		-e NPM_CONFIG_CACHE=/data/tmp/cache \
		-u $(shell id -u) \
		-w /data \
		$(NODE_VERSION) \
			npm install
	tar -czf $@ node_modules
	rm -rf node_modules tmp/cache

There are few things going on in this make target. There are two specific bits worth calling out. First note the -u argument. This important because the current directory is a mounted volume in the docker container. New files will be created with the user’s ID instead of root (the docker default user). Next the -e NPM_CONFIG_CACHE option. This is set to a directory on /data. This ensures that npm will write package archives to a directory the user with id (-u) has access too. npm may get permission errors without it. This may not be required depending on how you run docker but it covers all bases.

Now that dependencies are available it is time to build the docker image. Here’s the relevant Makefile excerpt and the complete Dockerfile.

$(NODE_MODULES): $(PACKAGE)
	tar xzf $<
	mkdir -p $(@D)
	touch $@

$(DOCKER): $(NODE_MODULES)
	docker build -t $(IMAGE) .
	mkdir -p $(@D)
	touch $@

Now for the Dockerfile:

FROM node:4-slim

RUN mkdir -p /app
WORKDIR /app

COPY . /app

CMD [ "npm", "test" ]

The Dockerfile is refreshingly small! Effectively the source is copied into the Docker image and that’s a wrap. Now that we have a docker image, we can use a volume mount to run quick code changes.

.PHONY: test
test: $(DOCKER)
	docker run --rm -v $(CURDIR):/data -w /data $(IMAGE) \
		npm test

Everything is packaged up in a handy boilerplate repo for use on your projects. You can build on this structure to add more tests, push images to your docker registry, and finally to deploy.

  1. Application dependencies should be vendored (e.g. they should be commited to source control). Committing dependencies to source control puts you back in control. It ensures repeatability across all your pipeline stages and isolates you against changes in upstream package repo. The node_modules folder is tarred here because there is no need to commit that directory because it would create huge diffs of little value. Committing the tar file is a decent enough compromise.