SHR's Occasional Braindump

Crafting Code and Business in a Beautiful Place

Simple boot setup to run a clojure app

I’ve been meaning to try out boot for quite a while. But never quite got there due to the projects I’ve been working on. This week the moment arrived, as I was starting a new project without any immediate external pressure.

The project will eventually combine both rich client and (clojure) backend. But my starting point was getting a ring app running with boot. Due mainly, I guess, to boot’s origins, there’s plenty online about using boot to build frontend (ClojureScript) projects, but relatively little for web projects. For some reason - stupidity, not doing enough reading up front, or some combination of the two - getting this working took me longer than it should have. So here’s the essence for anyone else who may be in a similar place.

For context, here’s my versions:

$ boot --version
#http://boot-clj.com
#Sat Feb 13 18:16:05 ICT 2016
BOOT_CLOJURE_NAME=org.clojure/clojure
BOOT_CLOJURE_VERSION=1.7.0
BOOT_VERSION=2.5.5

Here’s the (somehwat minimal) app itself:

;; in src/myapp/core.clj
(ns myapp.core
  (:require [ring.adapter.jetty :as jt])
  (:gen-class))

(defn handler
  [req]
  {:status 200
   :body "Hello, boot"})

(defn -main
  [& args]
  (jt/run-jetty handler {:port 3200}))

And here’s the build.boot file:

;; in build.boot
(set-env!
  :source-paths #{"src"}
  :dependencies '[[org.clojure/clojure "1.7.0"]
                  [ring "1.4.0"]
                  [ring/ring-jetty-adapter "1.4.0"]])

(require '[myapp.core :as app])

(deftask run []
  (with-pre-wrap fileset
    (app/-main)
    fileset))

With those two pieces (and boot being installed, of course), you should be able to run boot run and visit http://localhost:3200.

It really is very simple.

Actually, it turns out to be too simple. The application block when Jetty starts, and this in turn blocks the task. The end result is that while the application runs fine, the task itself is not composable.

Take 2: a composable approach

To make the task composable, we’re going to introduce two changes.

One change is to our application itself - we’ll allow passing in a parameter to the main function to specify whether the application should block or not (handily, jetty provides a join? configuration flag which set to false prevents waiting for the server thread to exit).

The other change is in our task - we’ll start the application in a Pod and require our application only in that context.

For good measure we’ll also allow calling the run task in blocking or non-blocking mode. And also make the server port confgurable.

First of all, here’s the revised application code:

;; in src/myapp/core.clj
(ns myapp.core
  (:require [ring.adapter.jetty :as jt])
  (:gen-class))

(defn handler
  [req]
  {:status 200
  :body "Hello, boot"})

(defn -main
  [& args]
  (let [opts (first args)
	    port (or (:port opts) 3200)
	    blocking (get opts :blocking true)]
    (jt/run-jetty handler {:port port :join? blocking})))

And here is the new build.boot:

(set-env!
  :source-paths #{"src"}
  :dependencies '[[org.clojure/clojure "1.7.0"]
                  [ring "1.4.0"]
                  [ring/ring-jetty-adapter "1.4.0"]])

(require '[boot.pod :as pod])

(deftask run
  "Run the application"
  [p port PORT int "Port web server listens on"
   n non-blocking bool "Run server in non-blocking mode"]
  (let [worker (pod/make-pod (get-env))
        start (delay
                (pod/with-eval-in worker
                  (require '[myapp.core :as app])
                  (app/-main {:port ~port :blocking (not ~non-blocking)})))]
    (with-pre-wrap fileset
      @start
      (info "Server started")
      fileset)))

This should now be composable with other tasks such as cljs/asset compilation. Still fairly basic, but moving in the right direction.

Incidentally, if serving a ring application is your goal, you may well want to consider boot-http.

blog comments powered by Disqus