It might surprise you to learn that postgres
is probably not going to be the password
for your production database.
To have something that can work in development as well as where you deploy it, you should read your configuration using environment variables.
dotenv-java
to your deps.edn
.There are more solutions to this - like direnv
or bash profiles - but this
is the one we are going with for the moment.
{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.12.0"}
ring/ring {:mvn/version "1.13.0"}
metosin/reitit-ring {:mvn/version "0.7.2"}
org.clojure/tools.logging {:mvn/version "1.3.0"}
org.slf4j/slf4j-simple {:mvn/version "2.0.16"}
hiccup/hiccup {:mvn/version "2.0.0-RC3"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.955"}
org.postgresql/postgresql {:mvn/version "42.7.4"}
com.zaxxer/HikariCP {:mvn/version "6.0.0"}
io.github.cdimascio/dotenv-java {:mvn/version "3.0.2"}}
:aliases {:dev {:extra-paths ["dev" "test"]
:extra-deps {nrepl/nrepl {:mvn/version "1.3.0"}
lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
:format {:deps {dev.weavejester/cljfmt {:mvn/version "0.13.0"}}}
:lint {:deps {clj-kondo/clj-kondo {:mvn/version "2024.09.27"}}}}}
env
component to the system.This is all the other components will read from as they start up to access environment variables.
In this way we can make whatever is in .env
be available as if it was an environment variable
in dev and use real environment variables when we deploy.
(ns example.system
(:require [example.routes :as routes]
[next.jdbc.connection :as connection]
[ring.adapter.jetty :as jetty])
(:import (com.zaxxer.hikari HikariDataSource)
(io.github.cdimascio.dotenv Dotenv)
(org.eclipse.jetty.server Server)))
(set! *warn-on-reflection* true)
(defn start-env
[]
(Dotenv/load))
(defn start-db
[]
(connection/->pool HikariDataSource
{:dbtype "postgres"
:dbname "postgres"
:username "postgres"
:password "postgres"}))
(defn stop-db
[db]
(HikariDataSource/.close db))
(defn start-server
[system]
(jetty/run-jetty
(partial #'routes/root-handler system)
{:port 9999
:join? false}))
(defn stop-server
[server]
(Server/.stop server))
(defn start-system
[]
(let [system-so-far {::env (start-env)}
system-so-far (merge system-so-far {::db (start-db)})]
(merge system-so-far {::server (start-server system-so-far)})))
(defn stop-system
[system]
(stop-db (::db system))
(stop-server (::server system)))
PORT
, POSTGRES_USERNAME
, POSTGRES_PASSWORD
from the environmentFirst add those values to the .env
file.
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=postgres
PORT=9999
Then read values off the env
component when making other components.
(ns example.system
(:require [example.routes :as routes]
[next.jdbc.connection :as connection]
[ring.adapter.jetty :as jetty])
(:import (com.zaxxer.hikari HikariDataSource)
(io.github.cdimascio.dotenv Dotenv)
(org.eclipse.jetty.server Server)))
(set! *warn-on-reflection* true)
(defn start-env
[]
(Dotenv/load))
(defn start-db
[{::keys [env]}]
(connection/->pool HikariDataSource
{:dbtype "postgres"
:dbname "postgres"
:username (Dotenv/.get env "POSTGRES_USERNAME")
:password (Dotenv/.get env "POSTGRES_PASSWORD")}))
(defn stop-db
[db]
(HikariDataSource/.close db))
(defn start-server
[{::keys [env] :as system}]
(jetty/run-jetty
(partial #'routes/root-handler system)
{:port (parse-long (Dotenv/.get env "PORT"))
:join? false}))
(defn stop-server
[server]
(Server/.stop server))
(defn start-system
[]
(let [system-so-far {::env (start-env)}
system-so-far (merge system-so-far {::db (start-db system-so-far)})]
(merge system-so-far {::server (start-server system-so-far)})))
(defn stop-system
[system]
(stop-db (::db system))
(stop-server (::server system)))
I know, I can hear your Clojure senses tingling. Isn't env
conceptually just a map? Why are we using
a Java library for this?
It boils down to two factors.
dotenv-java
is more well maintained than Clojure equivalents. Not that things can't be "done" and not require further maintenance.
If you have a library you prefer, have at it.toString
implementation not leaking sensitive info by default.env
function to your user
namespace(ns user
(:require [example.system :as system]))
(def system nil)
(defn start-system!
[]
(if system
(println "Already Started")
(alter-var-root #'system (constantly (system/start-system)))))
(defn stop-system!
[]
(when system
(system/stop-system system)
(alter-var-root #'system (constantly nil))))
(defn restart-system!
[]
(stop-system!)
(start-system!))
(defn server
[]
(::system/server system))
(defn db
[]
(::system/db system))
(defn env
[]
(::system/env system))