Skip to content
AgileLogics
6 min read

Building and Deploying Applications

Prerequisites: Projects with deps.edn and the Clojure CLI

A Clojure service is usually deployed as an uberjar: one jar with your code (AOT-compiled) and every dependency, run with java -jar.

build.clj with tools.build

;; illustrative — build.clj, run with: clj -T:build uber
(ns build
  (:require [clojure.tools.build.api :as b]))

(def class-dir "target/classes")
(def uber-file "target/inventory.jar")
(def basis (delay (b/create-basis {:project "deps.edn"})))

(defn clean [_]
  (b/delete {:path "target"}))

(defn uber [_]
  (clean nil)
  (b/copy-dir {:src-dirs ["src" "resources"] :target-dir class-dir})
  (b/compile-clj {:basis @basis :ns-compile '[inventory.core] :class-dir class-dir})
  (b/uber {:class-dir class-dir :uber-file uber-file :basis @basis
           :main 'inventory.core}))

The main namespace needs (:gen-class) in its ns form and a -main function. Then:

clj -T:build uber
java -jar target/inventory.jar

Configuration

;; illustrative — resources/config.edn, read with aero or clojure.edn
{:port #long #or [#env PORT 3000]
 :db   {:jdbc-url #env DATABASE_URL}
 :log-level #keyword #or [#env LOG_LEVEL "info"]}

Keep configuration in EDN and secrets in environment variables. The aero library adds the #env/#or tags shown here; with plain clojure.edn you'd read the environment in code instead.

Containers

# illustrative — Dockerfile
FROM clojure:temurin-21-tools-deps AS build
WORKDIR /app
COPY deps.edn build.clj ./
RUN clj -P -T:build            # download dependencies (cached layer)
COPY src src
COPY resources resources
RUN clj -T:build uber

FROM eclipse-temurin:21-jre
COPY --from=build /app/target/inventory.jar /app/inventory.jar
USER 10001
EXPOSE 3000
CMD ["java", "-XX:MaxRAMPercentage=75", "-jar", "/app/inventory.jar"]

Running in production

  • Logging — clojure.tools.logging over SLF4J/Logback, or mulog/telemere for structured event logging.
  • Lifecycle — start and stop components (HTTP server, DB pool) with Integrant or Component, and add a shutdown hook.
  • Observability — JVM metrics and tracing via standard Java agents (OpenTelemetry works unchanged).
  • REPL in production — a socket REPL (-Dclojure.server.repl=...) lets you inspect a live system. Powerful but dangerous: bind it to localhost only and guard access carefully.
  • Native images — GraalVM native-image can compile many Clojure apps to fast-starting binaries, useful for CLIs and serverless.