Clojure Fiddlings 3

Clojure CLI, Docker, fly.io - Makefiling things happen

Part of the Clojure Fiddlings series.

So far time I managed to build a Clojure app in a Docker container with a relatively small setup:

  • Clojure on the CLI
  • the built-in tool for dependencies and build: deps.edn
    • httpkit
    • Compojure
  • a Dockerfile

Then I ran my first deployment to fly.

What I want to build

I want to build a URL shortener. I want to POST a URL there and receive a shortened URL back that I can then share on Twitter. If someone calls it, it should return HTTP 302 to the original URL.

The POST calls should be authenticated. DELETE and listing all URLs I’ve saved, maybe exporting them to CSV or EDN are optional features for later.

What I’ve learned so far

  • It took me about half a day to get the basic setup running from a fairly uninitiated state. I’m not an experienced Clojure programmer who is using these tools every day. I have meddled with the language a bit and built a non-trivial app with it, but the tools were rather new to me.
  • Running Clojure in Docker was easier than expected. The Alpine base image took some fiddling but I wasn’t stuck or had to ask a Docker guru for help.
  • The startup time for Clojure has gone down significantly since I’ve last tried.
  • So far I really like deps.edn more than Leiningen. It just feels less complicated than getting to know all the sections, plugins and fields in Leiningen.
  • Deploying to fly was a non-issue. It even created the .dockerignore file for me that I forgot to add.
  • The final image size was bigger than I had expected.

Some time has passed

It’s been some time since I’ve last fiddled with Clojure, so, to be honest, I forgot which commands I had to run in order to run or test my app.

Usually I have my favorite build tool for this. I want my projects to be buildable with a single command after checking them out from SCM. They should also produce the same artifact that the deploy pipeline will later deploy to stages and finally to production if it wasn’t able to find any issues with it.

However, I wanted to build something low-tech, so Leiningen is out for now - except if I later decide that the low tech version got to complex. So I added a Makefile that does something my Maven build would usually do as well:

  • check if I have installed all required build tools
  • compile: not required for Clojure with clj on the CLI
  • run all tests: clj -X:test as defined in deps.edn last time
  • build the docker image: docker build -t clojure-cli-fly . with the Dockerfile we wrote last time
  • additional convenience build commands:
    • run the artifact
    • clean up everything

So I decided to try out something I haven’t used since my “build your own Linux kernel” days: a Makefile. I got the idea from my awesome colleague Amr Abdelwahab who’s been using this approach in many of his projects. I like that fact that make just comes with your operating system. Let’s see how far we can get.

Here’s the Makefile I ended up with. Let’s break it down together. N.b. that you will have to use TABS for indenting your code. Spaces will not work:

all: target/.package

clean:
	rm -rf ./target

target/.prepare:
	mkdir -p target

	which clj
	which docker

	touch target/.prepare

target/.test: target/.prepare
	clj -X:test
	touch target/.test

target/.package: target/.test
	docker build -t clojure-cli-fly .
	touch target/.package

run: target/.package
	docker run -p 8080:8080 clojure-cli-fly

deploy: target/.package
	fly deploy

open:
	fly open

The lines with no indent an a colon afterwards are called targets: clean:, target/.prepare, target/.test, target/.package, run, and deploy. If they have the name of another target after the colon, a prerequisite they depend on that target and it will run before this one.

Also, targets correspond to files. If the file exists, the target won’t run anymore. This is why some of the steps create files using touch when they are done. The special target all is the one make will run if you don’t supply any target as an argument. So, if I only run make in my project directory, we will build the Docker image because the build will run until the package step, i.e. prepare, test, and package. If any of the commands belonging to a target fails, make will stop, the touch command will not run but the previous steps' touch commands have run, so make will continue from the first target whose file doesn’t exist yet.

I’ve added run and deploy without a final touch step. I want to be able to run them whenever using make run or make deploy, but still dependent on a built artifact which we’ve created in the package target. I’ve also added make open as a shortcut for fly open.

Finally, the clean target removes all target files so we can start from scratch.