This weekend I decided to finally bite the bullet and deploy one of my Clojure apps as a JAR. “Certainly” I thought to myself, “there are great tutorials for this online”. Yes, yes there are, except none of them worked for me and in fact did lead to the consumption of some rye whiskey.
Caveats: Every Clojure app is different, and the details of your app will determine the steps necessary to build a JAR. None of the information I found online specifically addressed the problems I had, so I made this post for those who have a similar application with similar needs. YMMV.
What are we doing?
This post will walk through distilling a small Compojure web-app (codegumi.com) into a nice JAR for deployment on a VPS. This is an older app that gathers images from the Flickr API and dumps them into your browser. I used it to teach myself ClojureScript and core.async back before Om and React hit the scene.
The application needs to compile ClojureScript and has static resources. It also works with the VPS’s file system (caching searches as JSON files), a small feature that caused much pain.
We still want the project to work in development with a simple lein ring server
as well as a standalone JAR file. You can read all the source code if you like.
Why do I want a JAR?
Prior to this exercise I deployed my app the way real hackers deploy apps, in the most ridiculous way possible: I tmuxed into my VPS, did a quick lein trampoline ring server-headless 8080
and detached. This worked great! Except when my VPS got restarted or crashed (as they do) and I would then have to manually shell back in and restart the app.
Creating a JAR would allow me to create an upstart script so the OS could restart my app when necessary. It also seemed like the right thing to do.
Previously I attempted to install Tomcat and deploy a WAR file, all of which failed miserably. The concept of a JAR file with an embedded server is very attractive to me and seems like something I should know how to do, so onwards!
Überjar
You want to make a JAR, so you quickly drop “clojure jar” into DuckDuckGo and you will quickly see something about: lein uberjar
. You type this into your terminal, and the whole world explodes into a Java stacktrace. “But, but, my app ran fine with lein ring server
!” you despair. Yes, yes it probably did, however JARs are different animals. Fickle animals with sharp teeth. Lets take a look at how they work.
Main
To make a JAR you need to have a -main
function in your app. In a lot of Compojure tutorials you just define a ring
handler in your project.clj
:
(defproject codegumi "0.2.0"
:description "codegumi.com in Clojure/ClojureScript"
:url "http://codegumi.com"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.5.1"]
...
[ring "1.3.2"]]
:plugins [[lein-cljsbuild "1.0.1-SNAPSHOT"]
[lein-ring "0.8.8"]
[lein-environ "0.4.0"]]
:source-paths ["src/clj" "src/cljs"]
:ring {:handler codegumi.handler/app} ;; <---- Ring Handler
When you run it with lein
the handler is picked up and passed to ring
from your Clojure file and away you go. This won’t work with a JAR, you need to define a main entry point for your application. It can be as simple as passing your handler to jetty:
(ns foo.handler
(:gen-class) ;; <--- you need this too!
(:require [compojure.core :refer :all]
[compojure.handler :as handler]
[compojure.route :as route]
[ring.adapter.jetty :refer :all] ;; <-- need a server!
...
))
;; Compojure routes
(defroutes app-routes
(GET "/" ... ))
;; Ring handler
(def app
(handler/site app-routes))
;; Pass the handler to Jetty on port 8080
(defn -main []
(run-jetty app {:port 8080}))
So in the above we did a few things:
- We added
(:gen-class)
which Uberjar needs. - We added
ring.adapter.jetty
to embed the Jetty web server in our JAR. - We added a
-main
function which passes thering
handler to jetty.
Did you catch that part about embedding jetty into the JAR? That’s right, our JAR will contain its own server. Previously ring
would spin one up for us when we did lein ring server
. Our new -main
function will handle that for us.
We need to tell Uberjar where the main file is, so we need to add a setting to our project.clj
:
(defproject codegumi "0.2.0"
:description "codegumi.com in Clojure/ClojureScript"
:url "http://codegumi.com"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.5.1"]
...
[ring "1.3.2"]]
:plugins [[lein-cljsbuild "1.0.1-SNAPSHOT"]
[lein-ring "0.8.8"]
[lein-environ "0.4.0"]]
:source-paths ["src/clj" "src/cljs"]
:main codegumi.handler ;; <---- Where -main lives
:ring {:handler codegumi.handler/app}
If your app is very simple, these may be the only changes you need to build a JAR. You should give it a shot and see what happens. In my case I needed to do more research to deal with files and compiling ClojureScript.
Where are the files?
When I first tried to build a JAR, after creating my -main
function, I got tons of errors about accessing directories that didn’t exist within the JAR’s classpath. Oh noes. The one thing every non-Java person fears is any mention of classpath, as this word is burdened with much lore and spoken through the gnashing of teeth on many a message board.
I got these errors because my app needs to read/write JSON files to function and I was doing it all horribly wrong when it comes to JARs. So lets go over some basics.
Your typical Clojure/Compojure app has a directory structure something like:
├── LICENSE
├── README.md
├── doc
├── project.clj
├── resources
├── src
├── target
└── test
In my case I have clj
files for the server side and cljs
files for the client in src
:
src
├── clj
└── cljs
My Compojure app has a number of static resources, like css, images, etc. I also need a place for my compiled ClojureScript to live. The convention is to drop those into the resources/public
directory, as that will be included in the JAR automatically:
resources
└── public
├── css
├── favicon.ico
├── fonts
└── img
If you’re like me you don’t come from a Java background and you don’t know much about JAR files. By default anything you put in resources
will be packed into the JAR you compile and made available to your Clojure code. However, it isn’t accessible as a filepath because it is on the classpath. Within your app you access these files in a couple of ways.
As mentioned above, Compojure defaults to looking for classpath files in resources/public
. It also gives you a route/resources
method to handle this:
(defroutes app-routes
(GET "/" [] ... )
(route/resources "/")
(route/not-found "Not Found"))
All of my css, font and image files are now available from the root within my app. When it’s running my template can call /css/styles.css
and Compojure will pull that file from the classpath and serve it up.
If you needed to access a file from the resource
directory from within your Clojure code, you would use (clojure.java.io/resource "filename")
. This would return a classpath which you could use to read the file.
Now, these files are effectively static. JAR files are basically zip archives. You can read files within them, but you writing to them is probably not a good idea. It would mean mutating the JAR at runtime. This is important, because my app needs to write to the filesystem to cache JSON responses from Flickr, so it needs to know where to park those files and find them later.
You may say to yourself, “I will define the path to my directory in the app!” and do something like (def json "foo/bar")
. That may work great when running locally with lein
, and it might work in a JAR depending on what you put in there. It might not, and it might not because a JAR is run by the Java Virtual Machine (JVM) and the JVM may have a different idea about relative paths than you do. You also may not want to embed a directory path into your application.
There are different ways to approach this problem. You might make the writable path an environment variable. You might, like I did, make it an argument that you pass into the JAR when you execute it. You could do both. Either way, you should determine how to tell your app where to find/write files, and use clojure.java.io
to access them. I ended up packing the filepath passed in as an arg into an atom that I could use elsewhere in the application.
To recap: Use (clojure.java.io/resource "foo.css")
to access files within your JAR’s classpath and (clojure.java.io/file "foo.json")
to access a filepath outside the JAR.
Compile the ClojureScripts
Now, assuming your project has ClojureScript, you have probably already been compiling it with lein-cljsbuild
. You are probably building it right into your resources/public/js
folder via your compiler settings. This works great when your running with lein
. This works not so great when you need to build a JAR.
Luckily fixing this isn’t a big deal. As we mentioned above, we want our ClojureScript to compile into resources/public/js
so it will get packed into the JAR with our other resources files. All we need to do is add some hooks to our project.clj
file to make this happen during an Uberjar build:
(defproject codegumi "0.2.0"
:description "codegumi.com in Clojure/ClojureScript"
:url "http://codegumi.com"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.5.1"]
...
[ring "1.3.2"]]
:plugins [[lein-cljsbuild "1.0.1-SNAPSHOT"]
[lein-ring "0.8.8"]
[lein-environ "0.4.0"]]
:source-paths ["src/clj" "src/cljs"]
:hooks [leiningen.cljsbuild] ;; <--- Hook for uberjar to grab
:main codegumi.handler
:ring {:handler codegumi.handler/app}
:profiles
;; Uberjar profiles entry to specify how CLJS is compiled
{:uberjar {:aot :all
:cljsbuild {:builds [{:source-paths ["src/cljs"]
:compiler {:output-to "resources/public/js/script.js"
:optimizations :simple
:pretty-print false}
}]}}})
Adding :hooks [leiningen.cljsbuild]
will tell Uberjar to follow any CLJS compile directives when building the JAR. The compiled files, since they are in the resources
folder, will get picked up.
The addition of the :uberjar
entry in :profiles
give you some control over how your ClojureScript is compiled for JARs, which are usually meant for production.
You also may have noticed the addition of the {:aot :all}
directive to :uberjar
. This tells Leiningen to Ahead of Time compile your Clojure source into JVM bytecode as part of the JAR building process. There are pros and cons to this, so if you run into issues you might remove this directive and do some more reading.
Ermagerd, we might have a JAR
So after much trial and error you may have generated a JAR. Cool. By default Uberjar builds them in /target
. We’re interested in the standalone JAR as that is self-contained and has everything we need. Lets go ahead and run it: $ java -jar target/my-cool-app-standalone.jar
.
If the stars are aligned and you have sacrificed the appropriate items to your gods of choice there might be a working application on whatever port it was told to run on. Say, http://127.0.0.1:8080.
It may have exploded into a Java Stack Trace in which case remain calm, pour two fingers of rye whiskey into a glass and settle in for some debugging time.
Once it is working, go ahead and upload it to your VPS and see if it runs there as well. Make sure you have Java installed and all the directories your app needs to run, environment vars, etc. If you do, it runs and you’re on Ubuntu we can now make a simple Upstart script so the OS can launch it.
Upstart
Writing upstart scripts is a topic unto itself. I’ll give you a quick starter one, but you may need to do some research to meet your needs: The Upstart Cookbook.
Shell into your VPS and cd
into /etc/init/
. You will need sudo
powers to make this file, and name it after your app, something like coolapp.conf
:
# Start the process when the VPS comes up, and stop it when it goes down
start on runlevel [2345]
stop on runlevel [016]
# If the process crashes, restart it up to 10 times in 5min
respawn
respawn limit 10 5
# Log stdout to file
console log
# Set these to the desired uid/gid you want Java to run under
setuid clojure
setgid clojure
# Set any environment vars you might need
env BAR=quux
chdir /path/to/jar
exec /usr/bin/java -jar cool-app-standalone.jar
Save that file, and then run it $ sudo start coolapp
. If all goes according to plan, your JAR should get launched and you should see it running with ps
or top
. It should be running as whatever user/group you instructed it to. Point your browser at it and see how it goes. If it does huzzah!, if not, pour some more whisky into your glass.
Some things to note: your Upstart script is (in this case) run by the system, so it won’t have access to ENV
vars you set in your user configs, this is why we set them in the conf
script.
Upstart will automatically log output to /var/log/upstart/coolapp.log
. This will help you when things go wrong (and they will.) You may also consider using alternative logging libraries available to Java/Clojure.
I’d like to give a big shout out to my man Aaron Bull Schaefer who cleaned up my initial shot at an upstart script. If you need to know anything about running infrastructure or Factor he’s your man.
That’s a wrap
Hopefully you found this collection of information helpful in packaging up your application. Maybe you just got loaded. Either way, I hope you had fun.
Helpful references
- Creating an executable jar from a clojure project?
- Serving static files with ring/compojure - from a war
- Opening file in the same directory as running code in Clojure
- Developing and Deploying a Simple Clojure Web Application
- compojure.route.files
- clojure.tools/parse-opts
- lein-cljsbuild hooks
- Upstart Cookbook
Photo courtesy of Eli Christman via Flickr - Creative Commons
Comments