I'm facing some issues with my current workflow, where I develop mostly locally, and then only deploy sporadically to update my server. In this scenario, most of my edits to source happen locally, but I am discovering a few things (basically revolving around how both dev and prod use the same config.env file):
1. local config.env gets copied over to prod: this is fine, except that it also creates the situation where if u want to use reCAPTCHA in prod that u will have to use it for local dev too. And that u will also have to enable localhost as one of the domains for reCAPTCHA. Which as of now is not the advice as per https://developers.google.com/recaptcha/docs/faq#im-getting-an-error-localhost-is-not-in-the-list-of-supported-domains.-what-should-i-do: We advise to use separate keys for development and production, and to only allow localhost on your development site key.
2. if I want to connect to the prod REPL using clj -M:dev prod-repl , I will get a bind [127.0.0.1]:7888: Address already in use message, because I also have a local REPL for dev. Both REPLs use the same setting in the same config.env (`NREPL_PORT`) for the nREPL port. It looks like all prod-repl does is run an ssh command... so I could go around this by basically running the command myself with a different local port
Given 1 and 2 (and maybe other stuff too), it would be better in my situation to have separate config.env files for dev and prod. How can I make this happen?
1. To use different env values in dev vs. prod, I recommend putting them in separate env variables and then add conditional logic to resources/config.edn:
# config.env
DEV_RECAPTCHA_SECRET_KEY=...
DEV_RECAPTCHA_SITE_KEY=...
PROD_RECAPTCHA_SECRET_KEY=...
PROD_RECAPTCHA_SITE_KEY=...
;; resources/config.edn
:recaptcha/secret-key #profile {:dev #biff/secret DEV_RECAPTCHA_SECRET_KEY
:prod #biff/secret PROD_RECAPTCHA_SECRET_KEY}
:recaptcha/site-key #profile {:dev #biff/env DEV_RECAPTCHA_SITE_KEY
:prod #biff/env PROD_RECAPTCHA_SITE_KEY}
I opted for this approach so that as much complexity/logic as possible could be kept within source control, i.e. so the only thing outside of source control to keep track of is the single config.env file. If you're deploying in some non-default way to e.g. some container-based platform that includes env var management, then you'd probably opt to use the same env var names and just put different values for them in prod.
2. yeah, probably easiest to just override the prod-repl command. This should do it (though I haven't tested it):
diff --git a/starter/dev/tasks.clj b/starter/dev/tasks.clj
index 1f4f7f2..052adcc 100644
--- a/starter/dev/tasks.clj
+++ b/starter/dev/tasks.clj
@@ -1,14 +1,31 @@
(ns tasks
- (:require [com.biffweb.tasks :as tasks]))
+ (:require [com.biffweb.tasks :as tasks]
+ [com.biffweb.tasks.lazy.babashka.process :as process]
+ [com.biffweb.tasks.lazy.com.biffweb.config :as config]))
(defn hello
"Says 'Hello'"
[]
(println "Hello"))
+(def config (delay (config/use-aero-config {:biff.config/skip-validation true})))
+
+(defn prod-repl
+ "Opens an SSH tunnel so you can connect to the server via nREPL."
+ []
+ (let [{:keys [biff.tasks/server biff.nrepl/port]} @config]
+ (println "Connect to nrepl port" port)
+ ; May want to comment out the following line if you're running prod-repl at the same time as the local repl. Then
+ ; just need to manually connect to the right port when you want to use the prod repl.
+ (spit ".nrepl-port" "7889")
+ ;; Note, I don't remember if the local port comes first or second--if this doesn't work try:
+ ;; (str port ":localhost:7889")
+ (process/shell "ssh" "-NL" (str "7889:localhost:" port) (str "root@" server))))
+
;; Tasks should be vars (#'hello instead of hello) so that `clj -M:dev help` can
;; print their docstrings.
(def custom-tasks
- {"hello" #'hello})
+ {"hello" #'hello
+ "prod-repl" #'prod-repl})
(def tasks (merge tasks/tasks custom-tasks))
---
If for any reason you decided you still want to use different config.env files with biff's built-in deploy / soft-deploy commands, probably the easiest (albeit hacky) way would be to just make a prod-config.env file and swap it in before running the deploy:
diff --git a/starter/dev/tasks.clj b/starter/dev/tasks.clj
index 1f4f7f2..2354538 100644
--- a/starter/dev/tasks.clj
+++ b/starter/dev/tasks.clj
@@ -1,14 +1,22 @@
(ns tasks
- (:require [com.biffweb.tasks :as tasks]))
+ (:require [com.biffweb.tasks :as tasks]
+ [com.biffweb.tasks.lazy.babashka.fs :as fs]))
(defn hello
"Says 'Hello'"
[]
(println "Hello"))
+(defn deploy []
+ (fs/move "config.env" "dev-config.env")
+ (fs/copy "prod-config.env" "config.env")
+ (tasks/deploy)
+ (fs/move "dev-config.env" "config.env"))
+
;; Tasks should be vars (#'hello instead of hello) so that `clj -M:dev help` can
;; print their docstrings.
(def custom-tasks
- {"hello" #'hello})
+ {"hello" #'hello
+ "deploy" #'deploy})
(def tasks (merge tasks/tasks custom-tasks))(and do the same thing for soft-deploy)
The less convenient, less hacky way would be to copy-paste in the deploy task implementations from https://github.com/jacobobryant/biff/blob/master/libs/tasks/src/com/biffweb/tasks.clj and then modify the push-files-rsync function so that it uploads prod-config.env to config.env on the server.
I'm also not necessarily opposed to making it easier to use separate config.env files in dev/prod without doing a bunch of task overrides, but I'd just want to have a use-case in mind where the single config.env approach isn't good enough.
thanks, Jacob. Sorry for the late reply; I'm still looking through this.
A quick note: the local port comes first for the ssh command; so it's 7889:localhost:7888
one alternative, btw, would be to modify the deploy and soft-deploy tasks to accept an argument (with a default if not provided). This way u can have more than just "dev" and "prod", and can in fact name your environments whatever you want (`stage`?), and the deploy task will simply do use "config.env.<argument>" as the config.env at the other end
> but I'd just want to have a use-case in mind where the single config.env approach isn't good enough.
this would be if and when u have multiple config vars that require a dev/prod split. In my case, I just found another: MAILERSEND_API_KEY. When doing local testing, I dont want to have to send out an email. Does that make sense?
XTDB_TOPOLOGY would be another good candidate where a split would make sense
all these splits can be handled in resources/config.edn by using #profile as I described: https://clojurians.slack.com/archives/C013Y4VG20J/p1725559583196969?thread_ts=1725510697.778359&cid=C013Y4VG20J XTDB_TOPOLOGY in fact already has such a split: https://github.com/jacobobryant/biff/blob/a1d53efdab2046134575b71ed35a51fbcd212600/starter/resources/config.edn#L11
doh! 😅 Thanks!
Hi all! I’m planning to deploy a Biff app to a K8s cluster and was wondering how having two (or more) application servers in parallel would affect the WebSocket functionality. Both server wouldn’t talk to each user, thus users connected to server A would not receive updates from changes on server B. This is obviously out of scope for Biff, but perhaps someone in this channel managed to solve it already?
just out of curiosity, why k8s?
Hi @jf.slack-clojurians, I have already a K8s cluster running with a Clojure app (quite cheap at Hetzer, using https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner) with automatic builds and deployments from Github, so it’s not much effort to add one more app. What I like about K8s is that once you have it set up, you can quite easily scale and won’t have to worry about servers going down, K8s/Terraform will spin up a new one.
ok. Thank you for the reply! I have my own opinion about k8s, but am also looking to hear what others' experiences are like. Thanks for the link! k3s looks interesting and might be worth looking into if I have to go in a k8s direction
I was also hesitant about k8s because there are a lot of layers, adding complexity and potential point of failures. Getting everything set up took also some time during which I was wondering if a simple script using Ansible would be been good enough, but now I’m happy having pulled though and it has been very reliable so far.
did you go through the tutorial? I seem to recall a note somewhere where this is answered. Basically because you use XTDB as the update mechanism, all servers would receive the updates for messages (or whatever).
Hmmm… I must have skipped that. I’ll have a look, thank you!
Found it: > By using a transaction listener, we ensure that the chat room will work even if you scale out beyond a single web server: each web server will index the transaction and will send the message to any channel participants that have a websocket connection to that server.
what's your XTDB_TOPOLOGY setting in config.env?
jdbc, I’m using an existing Postgresql DB
yep, was gonna mention transaction listeners. basically you use XT's transaction log like a message queue. There are a couple other features you might want to avoid altogether when scaling beyond a single server--e.g. instead of Biff's scheduled tasks you'd probably want to do whatever the standard thing in k8s is for cron jobs.
let me know if you run into any issues with getting biff running on k8s--I think I got it deployed to digitalocean's managed k8s a while ago, but ended up not wanting to spend the time to figure out how to make SSL certs with auto-renewal work... anyway it's a use-case I'd like to support
speaking for myself--it's nice in commercial contexts when you need to be able to scale out/have high availability. or even if you're just deploying an internal app, you might be in an org that already uses k8s for everything and wants to keep stuff standardized.