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 indeps.edn
last time - build the docker image:
docker build -t clojure-cli-fly .
with theDockerfile
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.