Building a simple HTTP server in Clojure: Part III — Dockerizing Clojure Application

My previous blogs (Part I and Part II) focussed on how to build a simple HTTP server and add routes to it. In this blog I shall be discussing how we can deploy the server using Docker.

What is Docker ?

Docker is a tool for containerizing applications, such that they can be run on any host. Docker images are lightweight, disposable and immutable. An image is built up of immutable layers, allowing consistent operations, small size, and reusability.

You can learn more about Docker itself on its website, or take a deeper dive in its docs. Here is a great curriculum to start with Docker.

Creating a Dockerfile

Dockerfile forms the basis of any Docker image. It is a set of simple commands a user would call to assemble an image on command-line.
You can read more about it here.

Below is a basic Dockerfile for the app we have built:

# 1
FROM clojure
# 2
LABEL maintainer="Divyum Rastogi"
# 3
COPY . /usr/src/app
# 4
WORKDIR /usr/src/app
# 5
EXPOSE 8080
# 6
CMD ["lein", "run"]

Docker runs instructions in a Dockerfile in order. A Dockerfile must start with a `FROM` instruction. Below is the explanation of dockerfile written above:

  1. FROM instruction specifies the base image from which we shall be building our app image. If a FROM image is not found on the host, docker will try to find it (and download) from registry.
  2. LABEL it enables setting any metadata to be viewed about the image like from docker inspect <image_id> .
  3. COPY instruction copies files or directories from <src> and adds them to the filesystem of the container at the path <dest>.
    It has two forms:
     i. COPY <src>... <dest> 
    ii. COPY ["<src>",... "<dest>"] (this form is required for paths containing whitespace)
  4. WORKDIR instruction sets the working directory for any RUN, CMD,
    ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile. If the WORKDIR doesn’t exist, it will be created even if it’s not used in any subsequent Dockerfile instruction.
  5. EXPOSE instruction informs Docker that the container listens on the specified network ports at runtime. You can specify whether the port listens on TCP or UDP, and the default is TCP if the protocol is not specified.
  6. CMD provide defaults for an executing container. There can only be one CMD instruction in a Dockerfile.

Building and Running Image

Run these commands to build and run the image:

Build
docker build -t clojure-server . 
Run
docker run -it --rm -p 8088:8080 clojure-server

  • -t : tagname for the image
  • -it : interactive shell
  • - -rm: Remove intermediate containers after a successful build
  • -p: exposes the port inside container to external (host) port (here 8080 is exposed to 8088)

Phew! We have build and run the server from our docker image.

Concern:
While the above is the most straightforward example of a Dockerfile, it does have some drawbacks. The lein run command will download your dependencies, compile the project, and then run it. That's a lot of work, all of which you may not want done every time you run the image. To get around this, we can download the dependencies and compile the project ahead of time (aot). This will significantly reduce startup time of the image.

FROM clojure
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN mv "$(lein uberjar | sed -n 's/^Created \(.*standalone\.jar\)/\1/p')" app-standalone.jar
EXPOSE 8080
CMD ["java", "-jar", "app-standalone.jar"]

We see some new commands in the file:

  1. RUN instruction will execute any commands in a new layer on top of the current image and commit the results. The resulting committed image will be used for the next step in the Dockerfile.
  2. lein uberjar is a leiningen command to build runnale jar from clojure app.

The above dockerfile builds a jar and then build the image with that jar. Thus, dependencies will not be downloaded everytime we run the container.

Now we can use the above command to build image and run it.

Conclusion

This was a very simple guide of how to dockerize a clojure app. To package an application, you need a simple Dockerfile and a runnable JAR.

The complete code can be found on Github.

If you have any questions/suggestions, feel free to drop it in comments and if you found this article useful, clap below.