A Vagrant+Docker based workflow for clojure web development

This post outlines a container based development workflow using Vagrant and Docker.

Many common docker tutorials (eg. the official node tutorial) suggest a workflow where projects source is copied onto the image, which is then built and run through docker. This approach is not really practical for clojure development as normal clojure programming leans heavily on rapid prototyping and REPL driven development.

The setup below utilizes Vagrant and docker volumes to setup a development environment which ensures reproducibility and container isolation while retaining the short feedback cycle which clojure developers take pride in.

Our tiny Vagrantfile looks something like this:

A multi-machine setup makes it easy for us to later add additional services like databases etc. in the same Vagrant file and manage and run them together.

The current directory is the project directory which will be mounted as /app/pedestal_home. The synced_folder terminology is slightly confusing in this context, but as Vagrant docker provider documentation clarifies:

When using Docker, Vagrant automatically converts synced folders and networking options into Docker volumes and forwarded ports

Using the docker-run command we can run arbitrary commands within the docker container.

For example: We can use lein new to scaffold our pedestal application:

This directory structure will be populated directly in the host, and can be edited by an editor (Emacs, obviously) installed in the host operating system

Not that we are passing current user’s UID and GID in the Vagrantfile through the --user argument (All create_args are passed directly to docker).

If this was not done, files would be owned by root (docker daemon always runs as root). Marc Campbell has written a good post on how uid and gid work in docker containers.

Also in environment of host we set:

Docker’s default HOME env variable defaults to /, which leiningen wouldn’t have write permission to.
I had not expected to be required to set the _JAVA_OPTIONS but in its absence Maven dependencies are installed in ?/.m2/repo (Yes, a directory named ?), presumably because there is no user with specified UID in container context.

Also we don’t want dependencies to be downloaded for each container run, so having them in a synced folder .docker_home is useful:

Thanks to the line above, all our maven dependencies are downloaded in .docker_home/.m2/repository as seen in the tree above.

Also note that we have set our workdir through a create_arg to the project folder which allows us to run lein directory without having to cd into that directory first (Docker’s workdir defaults to /).

We can invoke lein in a similar fashion to run nrepl:

We can use docker ps and docker inspect to find the container ID and IP address of the container respectively.

docker inspect outputs a large JSON object, so tools like jq come in handy:

Now, assuming we have cider installed in emacs, we can invoke: M-x cider-connect and in the prompt that follows enter above IP address, and 9000 as the port.

This is the port on which we have asked nrepl to bind to, and our Vagrant file maps this port to the same port in host machine.

We should now have direct access to nREPL running on docker within our editor running on host.

To conclude, it should be obvious at this point that we can also run our web server in a similar way:

After this we should be able to visit the container’s mapped IP address in our browser running in host.

It is not a coincidence that the default port on which pedestal server is configured to listen to, is also configured to be mapped in our Vagrantfile.

Note that we don’t assume that the application is actually deployed as a docker container. There is nothing stopping us from building the application as an uberjar (within docker) and running it through any JVM compatible deployment solution.