We built a lein plugin to make it easy to run clojure projects in Docker containers. This article first introduces containers and Docker, and then shows how you can effortlessly create such containers for your Clojure projects with
Why Containers and Why Docker
A container is a lighweight form of computer virtualization. Containers run within a host, and a host can run many containers concurrently. Each container is functionally equivalent to a computer loaded with a full OS, but behind the scenes all containers in the same host are sharing the same host resources: kernel, memory, IO, and disk.
There are many ways of virtualization technologies used today, VirtualBox, Xen (used by Amazon Web Services) and vmware to name a few, but what makes containers special is their lightweight nature. Each container uses very little resources (disk, memory, cpu) and they all boot orders of magnitude faster than a virtual machine or a computer.
Containers are very convenient for both development and production environments. For development, they provide a fast and simple way to run different and isolated runtime environments for each application. In production, they are very fast and efficient, with the added bonus that containers in production always match the ones in development; the same exact containers are used during all app's lifecycle.
Docker is a novel container technology for Linux that provides a very low friction access to containers. Containers have been around for a while, and the Linux kernel has provided key container technologies for a while already. Docker wraps all these kernel features under a high level API and defines a standard container and image formats using union filesystems. To make containers easy to share and distribute, Docker also provides public and private image repositories. All these features have brought containers to the masses.
Although Docker is currently Linux only, you can use it on OSX installing boot2docker
For a more detailed introduction to Docker, check out this video on sysadmincasts.com.
Each container is a process tree that is started from an existing image. These images contain a filesystem with the files necessary to run your application. Sometimes this entails a linux distribution and sometimes just one statically linked binary. The common practice in the Docker world is to build a custom image for each type of service you intend to run. This contrasts with other forms of virtualization where you start the VMs from a base image and then you configure them with the required software. In the case of VMs, the time it takes a VM to boot until it is ready to run your application is measured in minutes, whereas in Docker a container can boot in sub-second times.
Building Docker images with uberimage
lein-uberimage builds a Docker image for clojure projects. The generated images contain a base operating system, a java runtime, and your project's
uberjar. Once this image is built, you can instantiate as many containers as you need using this image, each container running your
At the time of writing this,
lein-uberimage requires that the project builds with
lein uberjar and that the resulting jar can be run via
java -jar <your-uberjar>.jar. This means your
project.clj will have to have a
:main entry pointing to the
-main function in your code. This restriction will be removed in the future.
To build the Docker image for your project, run the following in your project's root:
$ lein uberimage
uberimage will call lein's
uberjar to build the standalone jar file. Then it will build a new image with
OpenJDK 7 and the freshly built jar. The image is also configured to run your jar file on boot. This task will return the
uuid for this image.
From this image you just created, you can spawn as many containers as you need. The most basic way to start a container is this:
$ docker run <my-image-uuid>
In most cases, your app won't be useful unless you can access it via known ports, and by default, Docker does not expose any ports of a container, so you need to tell Docker to expose those ports. Since you will be running many containers on the same host you need to ensure that not two containers are bound to the same port. For example, if your application listens on port 3000, you need to tell Docker to bind the container's port 3000 to any unbound port in the host, e.g. 8080:
$ docker run -p 8080:3000 <my-image-uuid>
Once the container has started, just head over to
http://<docker-host>:8080 to access your newly deployed service. If you launch a second container with the same image, you cannot bind it to the same host port (8080 in this case) and you should instead map it to port 8081
docker run -p 8081:3000 ....
Trying It Out
- Install Docker.
- Clone this example clojure project.
- On the project root, run
lein uberimageand copy the supplied image uuid
- At the shell, run
docker run -d -p 3000:3000 <image-uuid>
- Open the browser and head on to
http://<docker-host>:3000. Docker-host could be
localhostin case you are in linux, or the boot2docker IP address if you are using boot2docker.
docker run -d -p 3001:3000 <image-uuid>to run a second instance and check
http://<docker-host>:3001with your browser. Notice the different IP address.
In case the supplied OS and java versions are not what you're looking for, you can supply your own base image to
$ lein uberimage -b <your-image-with-jvm>
And in case your Docker is not locally installed on the default port or it is remote, you can override its url:
$ lein uberimage -H http://<host>:<port>
There is not much more to it for now. This is our first stab at helping clojurians leverage containers. Please submit bugs and ideas to our project's Issues or drop by our chatroom #pallet on freenode.
lein-uberimage is build on top of clj-docker, a clojure wrapper over docker.
Are you working on speeding up your development to production pathways and enhance the reliability of your online services? We at PalletOps provide consulting services specializing in infrastructure automation, both for cloud and non-cloud environments and lightweight containers. Please contact us to see how our services can get you close to your goals.