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:
- Use docker to generate node_modules from package.json and
commit source control. - Use node_modules to create a docker image
- 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
control¹. 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.
- Application dependencies should be vendored (e.g. they
should be committed 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.