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:
- 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.
- LABEL it enables setting any metadata to be viewed about the image like from
docker inspect <image_id>
. - 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) - WORKDIR instruction sets the working directory for any
RUN
,CMD
,ENTRYPOINT
,COPY
andADD
instructions that follow it in theDockerfile
. If theWORKDIR
doesn’t exist, it will be created even if it’s not used in any subsequentDockerfile
instruction. - 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.
- CMD provide defaults for an executing container. There can only be one
CMD
instruction in aDockerfile
.
Building and Running Image
Run these commands to build and run the image:
Builddocker build -t clojure-server .
Rundocker 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:
- 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
. 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.