Over the last few years, the software development community has been undergoing a lot of transformation. There’s a tug of war in progress across our entire community; while our languages and tools are getting faster, simpler, sharper, the problems we face grow ever larger and more complex.
We tackle a lot of the complexity in how we build our applications. I use Clojure, but there are plenty of other fresh, new sharp tools out there. There’s more to solving a problem than building something, however; you have to deploy it for the world to see!
This is where things get really complicated. As our applications have grown, distributed has become the new default. Not only do we need sharp tools to build our applications, we need sharp devops tools to deploy them.
Docker is one such sharp tool.
Cloud, meet Immutability
If you haven’t heard, Docker is a tool for containerizing applications, such that they can be run on any host–laptop, cloud provider, or otherwise. Docker images are lightweight, disposable and most importantly, immutable. Like Clojure’s persistent data structures, an image is built up of immutable layers, allowing you consistent operations, small size, and reusability.
All of this is well and good, but how can you Dockerize your own Clojure applications? Simple, really. Throughout the rest of this post I’ll show you how to:
- Create a
Dockerfilefor your application.
- Package your application to be invoked as a standalone JAR file.
- Configure your application via environment variables.
Learn more about Docker
A Basic Dockerfile
The basis for any Docker image is a
Dockerfile–a set of simple directives for packaging binaries into a runnable environment. Each line consists of a command, and its arguments. I’m going to presume if you’ve made it this far you’re passably familiar with Docker, so I won’t elaborate on the meaning of each command. You’ll find a complete listing for your reference here.
With that said, here is a basic Dockerfile for a Clojure application:
FROM dockerfile/java ;; 1 MAINTAINER Ryan Neufeld <email@example.com> RUN sudo apt-get update ADD target/my-app-0.0.1-SNAPSHOT-standalone.jar /srv/my-app.jar ;; 2 EXPOSE 8080 ;; 3 CMD ["java", "-jar", "/srv/my-app.jar"] ;; 4
The play by play:
- We need Java, no better way to get it than via the trusted Java distribution
- Place an executable JAR file into the image. More on this next…
- Expose ports the application will listen on
- And finally, tell Docker how to actually run the damned thing
Obviously, this is a pretty spartan image. That said, it does one thing, and it does it well. One could layer in something like supervisord for process control (see pedestal-micro for an example), but this task can just as well be managed by an external tool.
A Runnable JAR
Now, you can’t just slap any old Clojure project JAR into a Docker image and expect it to work. It needs to be a runnable JAR with all of its dependencies packaged with it.
The easiest way to accomplish this is by adding a
:main entrypoint to your project file and compiling your application into an
Before you can do any of this, you’ll need a function to bootstrap your application. For Jetty-based applications (e.g. Ring, Pedestal), this is often as simple as invoking
ring.adapter.jetty/run-jetty with your applications configuration. I generally call this function
start in a high-level namespace–often the top-most namespace that shares the name of the application itself (e.g.
To expose this function as an entrypoint into your application, create a second, smaller wrapper function
-main that invokes
(defn -main [& args] (start))
The only other detail in this namespace, is to ensure that a Java class is generated. Do this by adding the
:gen-class directive to the
(ns my-app (:require ...) (:gen-class))
Finally, inside your
project.clj file add the namespace including
-main as the project’s
:main entrypoint. Additionally, add the namespace to the list of classes to be AOT-compiled:
(defproject my-app "0.1.0-SNAPSHOT" ;;... :main my-app :aot [my-app])
With all of that in place, you can now test that your app functions correctly when invoked from the command-line:
# Via lein $ lein run ... Listening on 8080 # Via Java & Uberjar $ lein uberjar Compiling my-app ... $ java -jar target/my-app-0.1.0-SNAPSHOT-standalone.jar ... Listening on 8080
Putting it all together
You’re now ready to build/run your application inside of Docker.
To finally run your application inside of Docker, first build, then run your application’s Docker image:
$ docker build -t my-app . ... $ docker run -p 8080:8080 my-app ... Listening on 8080
On Configuration and Beyond
One of the bigger paradigm shifts moving to Docker is 12 Factor: Config. The model of reading configuration from environment variables at runtime has some special considerations on the JVM. When you prepare your Uberjar you compile your main namespace using AOT compilation. If you don’t wrap your
System/getenv in a function invokation, the call will reference the compilation environment, not the runtime environment.
# somefile.clj # Bad (def db-uri (System/getenv "DB_URI")) (defn create-user [params] (sql/insert! db-uri ...)) # Good (defn db-uri  (System/getenv "DB_URI")) (defn create-user [params] (sql/insert! (db-uri) ...))
Other considerations we won’t cover in this post:
For those already using Docker with Clojure: are you doing anything different? Have you hit any of your own snags you want to share?
Docker is a great way to run Clojure applications. To package an application, you need a simple Dockerfile and a runnable JAR. Remember to take care when you read environment variables from an AOT-compiled file.
To quickly see a sample of a Dockerized Clojure application, generate one with pedestal-micro:
$ lein new pedestal-micro my-clojure-docker-app