Tutorial
Tutorial
Source code for this tutorial can be found here: https://github.com/jlesquembre/clj-demo-project
Init
There is a template to help you start your new project:
For this tutorial you can clone the final version:
First thing we need to do is to generate a lock file:
Note
The following examples assume that you cloned the demo repository, and
you are executing the commands from the root of the repository. But with Nix
flakes, it's possible to point to the remote git repository. E.g.: We can
replace nix run .#foo
with nix run github:/jlesquembre/clj-demo-project#foo
Create a binary from a Clojure application
First, we create a new package in our flake:
clj-tuto = cljpkgs.mkCljBin {
projectSrc = ./.;
name = "me.lafuente/cljdemo";
main-ns = "demo.core";
};
Let's try it:
Nice! We have a binary for our application. But how big is our app? We can find it with:
Um, the size of our application is 1.3G
, not ideal if we want to create a
container. We can use a headless JDK to reduce the size, let's try that:
clj-tuto = cljpkgs.mkCljBin {
projectSrc = ./.;
name = "me.lafuente/cljdemo";
main-ns = "demo.core";
jdkRunner = pkgs.jdk17_headless;
};
Good, now the size is 703.9M
. It's an improvement, but still big. To reduce
the size, we can use the customJdk
helper.
Create custom JDK for a Clojure application
We add a package to our flake, to build a customized JDK for our Clojure application:
Not bad! We reduced the size to 96.3M
. That's something we can put in a
container. Let's create a container with our application.
Create a container
Again, we add a new package to our flake, in this case it will create a container:
clj-container =
pkgs.dockerTools.buildLayeredImage {
name = "clj-nix";
tag = "latest";
config = {
Cmd = clj-nix.lib.mkCljCli self.packages."${system}".jdk-tuto { };
};
};
The container's size is 52.8M
. Wait, how can be smaller than our custom JDK
derivation? There are 2 things to consider.
First, notice that we used the mkCljCli
helper function. In the original
version, our binary is a bash script, so bash
is a dependency. But in a
container we don't need bash
, the container runtime can launch the command,
and we can reduce the size by removing bash
Second, notice that the image was compressed with gzip.
Let's load and execute the image:
Docker reports an image size of 99.2MB
Create a native image with GraalVM
If we want to continue reducing the size of our derivation, we can compile the application with GraalVM. Keep in mind that size it's not the only factor to consider. There is a nice slide from the GraalVM team, illustrating what technology to use for which use case:
(The image was taken from a tweet by Thomas Würthinger)
For more details, see: Does GraalVM native image increase overall application performance or just reduce startup times?
Let's compile our Clojure application with GraalVM:
The size is just 43.4M
.
We can create a container from this derivation too:
graal-container =
let
graalDrv = self.packages."${system}".graal-tuto;
in
pkgs.dockerTools.buildLayeredImage {
name = "clj-graal-nix";
tag = "latest";
config = {
Cmd = "${graalDrv}/bin/${graalDrv.name}";
};
};
In this case, the container image size is 45.3MB
, aproximately half the size
of the custom JDK image.