This is just a simple tutorial on how to package and deploy a Clojure application to a Docker file, which can then be deployed locally, on a server, or in the cloud.
We are going to follow a pretty classic pattern in Clojure:
- DEVELOP. In this case, we’ll use
lein repl
and emacs (cider). For a mixed Clojure/Clojurescript app,lein figwheel
. - BUILD UBERJAR. Very simply,
lein uberjar
is all we need to do to package all java dependencies and app resources into a single file. - BUILD DOCKER IMAGE. Because the uberjar does most of the work for us, we just need a Java JRE and the uberjar.
- DEPLOY. If you are running your own a production system, Kubernetes would be a good choice here. Amazon, Google, Microsoft all have their own container services as well.
Develop
It’s probably not the latest and greatest way of making a front end and back end, but I find for many of my small apps Reagent for the front end and Ring for the back end are enough. Creating a project is simple:
lein new reagent myapp +cider
cd myapp
git init
And then you just develop like normal until you are happy with the way your app works. The only slightly tricky thing to remember when rolling up an app that you will put in a Docker container is that your web server should be configured to listen on 0.0.0.0 (which means to listen on all interfaces). Typically you just add :host "0.0.0.0"
as an argument to whatever webserver you are starting, jetty
in this case.
Build the UberJar
lein
makes packaging up a web server, front end, and assets all together extremely easy:
lein uberjar
The JAR file will appear in the target/
directory. If you need to control the name of the output uberjar, adjust the :uberjar-name
key in your project.clj
.
Build Docker Image
The steps for building a Docker image are stored in a special file called Dockerfile
, which I typically place in the root directory of my project repo. Since all the assets are already stored in the uberjar, the contents of Dockerfile
are simple:
# Use https://hub.docker.com/_/oracle-serverjre-8
FROM java:8-alpine
# Make a directory
RUN mkdir -p /app
WORKDIR /app
# Copy only the target jar over
COPY app-standalone.jar .
# Open the port
EXPOSE 3000
# Run the JAR
CMD java -jar app-standalone.jar
During the build process, Docker needs a “context” directory that contains all of the files needed to build the image. Since we have already packaged assets in the JAR, and compiled the source code into bytecode, we do not need to copy the source over in the build process. We can let the Docker build process use the “target” directory only. This can speed up the Docker build, and saves space because it is not copying resources twiec. The only downside is that it means we have to explicitly specify the Dockerfile to use explicitly, and explicitly specify the directory to use as the “context” directory. Run this from the root of the project directory:
docker build --tag myapp -f Dockerfile target
And that’s it!
Deploy Locally to Test
If your app has no state (and it shouldn’t, if you are making a 12-factor app, you can now create a new container from your docker image, passing it whatever environment varibales you need, and exposing internal port 3000 to external port 3000:
docker run --name my-app-container --env MY_ENV_VAR=some_value -p 3000:3000 -rm myapp
Check to see that it is running:
docker containers ls
or more concisely,
docker ps
I still find it slightly misleading that run
actually means “create and start a container” in the language of Docker-ese. Stopping the container will not delete it in general, and that often means containers accumulating silently in the background. Hence the -rm
flag, which tells Docker to delete the container when it is done.
If you don’t want the container to delete itself when done, omit the -rm
option, and maybe consider instead the --detach
option so you get your shell back. If you aren’t building a new container regularly and the container is lying around, starting and stopping the named container is as simple as you would expect:
docker container start my-app-container
docker container stop my-app-container
Resources and Interesting Reading
- https://blog.jessfraz.com/post/docker-containers-on-the-desktop/
- https://docs.docker.com/get-started/
- https://kubernetes.io/docs/getting-started-guides/ubuntu/installation/
- https://marketplace.automic.com/details/clojure-official-docker-image
- https://medium.com/@mprokopov/deployment-of-clojure-app-to-production-with-docker-9dbffeac6ef5
- https://medium.com/@divyum/building-a-simple-http-server-in-clojure-part-iii-dockerizing-clojure-application-1f53a6a90af2
- https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- https://devcenter.heroku.com/articles/clojure-web-application