Like many web companies we no longer write HTML, CSS and JS and simply push them to our live servers. We have a front-end build 'system', which manages all of the tasks required to convert our source content to our static output. This includes CSS pre-processing, CSS and JS minification and concatenation, something clever to do with icons that I don't understand, and some file copying. In the good old days, someone would have written an incomprehensible shell script that Just Works™ to do all of this, possibly in Perl, but nowadays we use Node, just like everyone else.
I hate Node, npm (its package manager) and the 'incredible eco-system' of Open Source libraries and apps that exist in its universe. My main complaints (and I have a lot more) are:
npm install appears to download the entire internet locally
npm install then appears to recompile your local OS
- However hard we try, we just can't get a guaranteed output
Even with a small team, using a common Vagrant VM, we get inconsistent results - which often results in 'git ping-pong' - every person who runs
grunt locally gets a different output, and overwrites the previous commit.
Added to that - when attempting to rebuild everything from scratch, you run into issues like this - PhantomJS has a secret dependency.
We've looked into various solutions to all this - including have our node_modules directory in a separate git repo - but they're all hacks and workarounds. It's just a gigantic PITA.
The solution we've arrived at is to bundle our entire runtime requirements into a single Docker image, thus removing any individual's unique environment issues, and to make 'grunt' (our task runner of choice) run as if it were a single (dockerised) executable.
We actually have three images, building on top of each other, in the following sequence:
- contains node and npm only
- as above, but with global npm installs of bower and grunt-cli
- as above, with installs of all our packages.json and bower.json dependencies - it's 'grunt-in-a-box' for our specific project.
Developers need only concern themselves with the latter (until we need to upgrade node) - and because it's hosted on the public docker registry, docker will automatically download it the first time it's run.
The full Dockerfile used to build this is:
# Dockerfile for local compiler (yunojuno/grunt)
# If building this locally, it must be built from a directory that
# contains the Dockerfile, package.json and bower.json files.
MAINTAINER Hugo Rodger-Brown <firstname.lastname@example.org>
# Phantomjs is ubiquitous in frontend build systems, and
# it has a hidden dependency on libfontconfig:
RUN apt-get install -y libfontconfig
# Install local project node dependencies
ADD package.json .
RUN npm install
# Add local project bower dependencies
ADD bower.json .
RUN bower install --allow-root
# set the default working directory to /frontend - you will want
# to mount your host frontend directory (containing the Gruntfile,
# and src, static subdirectories) into here on docker run:
# docker run -v /path/to/host/frontend:/frontend yunojuno/grunt
# Default assumption is that you will be running grunt
This image contains a fixed node runtime, with all of our project requirements. The only thing missing is the local Gruntfile to tell it what to do when it runs, which we deliberately exclude from the image as it gets updated fairly frequently, and re-building the image each time seems unnecessary.
We mount the Gruntfile into the container at runtime using the
$ docker run -v /path/to/host/frontend:/frontend yunojuno/grunt
This will mount the local directory into the container as /frontend, and run the default ENTRYPOINT ('grunt').
The container's directory structure now looks like this:
├── src -> contains src files for processing
└── static -> contains processed output
The net result of this is a front-end build system that enforces:
- The runtime inside the container is fixed, meaning a consistent output
- No one has to run
npm install any more
- No extraneous node_modules or bower_components directories