Clojure Fiddlings 2

Clojure CLI, Docker, fly.io - Gonna fly now

Part of the Clojure Fiddlings series.

Last 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

I got to the point where I was able to build the Docker image with all the dependencies and access the server inside the container through a mapped port from the outside. Here’s a reminder what I wanted to do:

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 set out to explore:

  • What’s the minimal amount of tools required to get a simple Clojure webapp into fly.io?
  • How easy is it to run Clojure in Docker?
  • Compared to Leiningen, how does working with the built-in tools feel?
  • How easy is it to get the app into fly.io once I can run the Docker container on my local machine?

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. I could imagine using this for FaaS, but Alex would kill me :-P.
  • 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.

Pressing the deploy button

I’ve signed up for a fly.io account and I’m on the free tier. Only paying for what I use after using up the free tier’s allowance feels good. Thanks for offering that.

Let’s fire the lazer >:). I’ll dump the whole output here:

~/workspaces/clojure/clojure-cli-fly (main ✔) fly launch

Creating app in .../workspaces/clojure/clojure-cli-fly
Scanning source code
Detected a Dockerfile app
? Create .dockerignore from 2 .gitignore files? Yes
Created .../workspaces/clojure/clojure-cli-fly/.dockerignore from 2 .gitignore files.
? Choose an app name (leave blank to generate one):
automatically selected personal organization: Georg Berky
? Choose a region for deployment: Frankfurt, Germany (fra)
Created app ... personal
Wrote config file fly.toml
? Would you like to set up a Postgresql database now? No
? Would you like to deploy now? Yes
==> Building image
Remote builder fly-builder-holy-glade-5712 ready
==> Creating build context
--> Creating build context done
==> Building image with Docker
--> docker host: 20.10.12 linux x86_64
Sending build context to Docker daemon  22.26kB
[+] Building 13.0s (8/8) FINISHED
 => [internal] load remote build context                                                                                             0.0s
 => copy /context /                                                                                                                  0.1s
 => [internal] load metadata for docker.io/library/clojure:temurin-19-alpine                                                         1.9s
 => [1/4] FROM docker.io/library/clojure:temurin-19-alpine@sha256:7af130e354dfc6e8efe8beda70cd7864c7fe343eecb2bcb35522067cba3b8af0   7.7s
 => => resolve docker.io/library/clojure:temurin-19-alpine@sha256:7af130e354dfc6e8efe8beda70cd7864c7fe343eecb2bcb35522067cba3b8af0   0.0s
 => => sha256:625079a111ba2eb7aeee77c0b37e81a42153e37489e4924943edf19c12defb44 405B / 405B                                           1.0s
 => => sha256:65caacad1f1da56b38646cca7b0c07dcf043ec8c56d7efe21f63d8e1656c7eaf 200.30MB / 200.30MB                                   4.9s
 => => sha256:3b95def8d0bf0c89e46c7970be45d478ee2c94ee62d9ccef1e491fbf42e5ce9d 174B / 174B                                           0.5s
 => => sha256:953daaf7ec3b5a114955a8be155a8e209a95e476b1f2f3d0d3d00334af280d2a 30.08MB / 30.08MB                                     1.7s
 => => sha256:d731444a696758b1beb0e6bf3478305baa7054e6fb4d0f319da762f9701d7a84 625B / 625B                                           0.8s
 => => sha256:4e5835cc7e6eb64b728c9694dd72d8aede877af0a2ec44f981847853af9abbf5 12.04MB / 12.04MB                                     1.8s
 => => sha256:7af130e354dfc6e8efe8beda70cd7864c7fe343eecb2bcb35522067cba3b8af0 320B / 320B                                           0.0s
 => => sha256:8afa260ccf90a9f1963879b9761b62490566c784ee500c1ca8212e34d29e140c 1.79kB / 1.79kB                                       0.0s
 => => sha256:a075dba024602690ee0061caf4dbce0136d721b9102c539f7e5b5652f2e00875 5.94kB / 5.94kB                                       0.0s
 => => sha256:213ec9aee27d8be045c6a92b7eac22c9a64b44558193775a1a7f626352392b49 2.81MB / 2.81MB                                       1.1s
 => => extracting sha256:213ec9aee27d8be045c6a92b7eac22c9a64b44558193775a1a7f626352392b49                                            0.1s
 => => extracting sha256:4e5835cc7e6eb64b728c9694dd72d8aede877af0a2ec44f981847853af9abbf5                                            0.4s
 => => extracting sha256:65caacad1f1da56b38646cca7b0c07dcf043ec8c56d7efe21f63d8e1656c7eaf                                            2.2s
 => => extracting sha256:3b95def8d0bf0c89e46c7970be45d478ee2c94ee62d9ccef1e491fbf42e5ce9d                                            0.0s
 => => extracting sha256:953daaf7ec3b5a114955a8be155a8e209a95e476b1f2f3d0d3d00334af280d2a                                            0.3s
 => => extracting sha256:d731444a696758b1beb0e6bf3478305baa7054e6fb4d0f319da762f9701d7a84                                            0.0s
 => => extracting sha256:625079a111ba2eb7aeee77c0b37e81a42153e37489e4924943edf19c12defb44                                            0.0s
 => [2/4] RUN apk add rlwrap                                                                                                         3.2s
 => [3/4] COPY . /usr/src/app                                                                                                        0.0s
 => [4/4] WORKDIR /usr/src/app                                                                                                       0.0s
 => exporting to image                                                                                                               0.0s
 => => exporting layers                                                                                                              0.0s
 => => writing image sha256:bbeadc97794ef88cb6d8843e828509265ca674c54ef3935948958f2318d249cd                                         0.0s
 => => naming to registry.fly.io/...:deployment-01GHF85H0HVN6TN4AHREQNK3E3                                             0.0s
--> Building image done
==> Pushing image to fly
The push refers to repository [registry.fly.io/...]
5f70bf18a086: Pushed
70f4932e3047: Pushed
f1f9573cce5f: Pushed
27fd57366c41: Pushed
48e604eaf94f: Pushed
462db70b18b8: Pushed
1a3007dca0a0: Pushed
3ee9498d5ad1: Pushed
57c379a94f03: Pushed
994393dc58e7: Pushed
deployment-01GHF85H0HVN6TN4AHREQNK3E3: digest: sha256:4076998bef1dc461a2ab0c45638d17e9bdeb676ab3deeb27f705b25fb6ccbe55 size: 2412
--> Pushing image done
image: registry.fly.io/...:deployment-01GHF85H0HVN6TN4AHREQNK3E3
image size: 418 MB
==> Creating release
--> release v2 created

--> You can detach the terminal anytime without stopping the deployment
==> Monitoring deployment

 1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--> v0 deployed successfully

Yikes, more than 400MB for the image? I honestly expected Alpine to be smaller. I’ll have to research that big 200MB chunk there in the middle later:

 => => sha256:65caacad1f1da56b38646cca7b0c07dcf043ec8c56d7efe21f63d8e1656c7eaf 200.30MB / 200.30MB                                   4.9s

As I’ve chosen to deploy already because I don’t have config or secrets to set first, I can get to my app using

fly open

This was easier than I thought. I’ll write more later. Stay tuned.