12 April 2020

Fulcro on Heroku - Time to deploy

Getting a working build

With minimal hair on fire

So you've got a Fulcro site and you want to show it to the world! So you go to www.heroku.com, and why wouldn't you? They really help to make things nice, straightforward and easy.

You might want to have a read of some pitfalls.

Step 1 - deps deps deps

So you push your code to heroku's friendly git repo and watch the build log, 💥.

What happened?

deps.edn did!

Heroku's really focused on leiningen being the way you build stuff, I mean really focused! If you don't have a file called project.clj its clojure build process just falls over. (This may be fixed by the time you read this, or not...) 🤷

So let's create one:

touch project.clj

That's right, it doesn't even need to have anything in it, it just has to exist...

Now obviously that's not going to be enough, so let's take your deps.edn file and adjust it:

{:paths   ["src/main" "resources"]

 :deps    {...}

 :aliases {:clj-tests {:extra-paths ["src/test"]
           ...
            ;; v-- Add this!
           :depstar {:extra-deps {seancorfield/depstar {:mvn/version "0.5.2"}}}}}

and your shadow-cljs.edn file:

{:deps   {:aliases [:dev]}
 :nrepl  {:port 9000}
 :builds {:main       {:target  :browser
                       ...
                       ;; v-- and this!
                       :release {:compiler-options {:optimizations :advanced}}}

          :test       {...}

          :ci-tests   {...}

          :workspaces {...}

Next, we need to add a bin/build file, we're doing this because the presence of such a file will make heroku use that for our build.

#!/usr/bin/env bash
npm install
npx shadow-cljs release main
clojure -A:depstar -m hf.depstar.uberjar fulcro.jar

Don't forget to chmod u+x bin/build!

Finally, we need to add some buildpacks!

$ heroku buildpacks:clear
$ heroku buildpacks:add heroku/nodejs
$ heroku buildpacks:add heroku/clojure

When you're done they should look like this:

$ heroku buildpacks
=== <your machine name> Buildpack URLs
1. heroku/nodejs
2. heroku/clojure

Step 2 - Talking to Postgres

First let's adjust our config/prod.edn:

{...

 :org.httpkit.server/config {:port :env.edn/PORT}  ;; <-- Grab the port from heroku
 ...
 :database-spec {:jdbc-url :env/DATABASE_URL}}  ;; <-- Grab the DATABASE_URL as well

We'll need to make some adjustments to our postgres connection to parse heroku's DATABASE_URL, stick this in app.model.database:

(defn create-uri [url] (URI. url))

(defn parse-username-and-password [db-uri]
  (str/split (.getUserInfo db-uri) #":"))

(defn parse-db-name [db-uri]
  (str/replace (.getPath db-uri) "/" ""))

(defn parse-host [db-uri]
  (.getHost db-uri))

(defn parse-port [db-uri]
  (.getPort db-uri))

(defn hikari-connection-map
  "Converts Heroku's DATABASE_URL to a map that you can pass to hikari"
  [heroku-database-url]
  (let [db-uri (create-uri heroku-database-url)
        host    (parse-host db-uri)
        port    (parse-port db-uri)
        db-name (parse-db-name db-uri)
        [username password] (parse-username-and-password db-uri)]
    {:username      username
     :password      password
     :server-name   host
     :port-number   port
     :database-name db-name
     :sslmode       "require"}))

(defn process-database-spec [{:keys [jdbc-url] :as db-spec}]
  (if jdbc-url
    (hikari-connection-map jdbc-url)
    db-spec))

(defn datasource-options []
  (merge {:auto-commit        true
          :read-only          false
          :connection-timeout 30000
          :validation-timeout 5000
          :idle-timeout       600000
          :max-lifetime       1800000
          :minimum-idle       10
          :maximum-pool-size  10
          :pool-name          "db-pool"
          :adapter            "postgresql"
          :register-mbeans    false}
    (process-database-spec (:database-spec config))))

(defstate pool
  :start (pool/make-datasource (datasource-options))
  :stop (pool/close-datasource pool))

Step 3 - Something to Run

Now we just need a Procfile:

web: java -cp fulcro.jar clojure.main -m app.server-main

And we're good to go! 👍


PS: If you're getting:

Problem 1 - Connection Refused during deploy

[web.1]: Execution error (ConnectException) at java.net.PlainSocketImpl/socketConnect (PlainSocketImpl.java:-2).
[web.1]: Connection refused (Connection refused)

Then you many not be passing in src/main/config/prod.edn:

:sslmode "require"

Basically, your postgres database is refusing the connection, so you need to pass in that attribute (or something else if you aren't using hikari) =)...

Problem 2 - Just 404 everywhere!

You can't see anything and when you try and access your javascript files, they're 404ing. You may need to also wrap-files in src/main/config/default.edn.

:static    {:resources "public"
            :files     "resources/public"} ; <-- Add this!

This is to do with the build order, if you built your clojurescipt code with shadow-cljs first, then when you build your depstar uberjar, the jar file you make will have a copy of your code, so you won't need this.

On the other hand, if you're wanting to update your static files separate to what's in your jar, say because you're updating them at a different rate, you want to make sure your uberjaring doesn't see them.

So do the shadow-cljs after the uberjar. That way they'll get served off the file system. This is probably also what's wrong if you're asking yourself why the file you've just edited/changed isn't showing up on your server.

You probably don't want to do this on Heroku, but it's useful to know about :)...

Problem 3 - Some sort of CORS error?

You may have forgotten to add heroku to your list of legal origins in src/main/config/prod.edn:

:legal-origins #{"<some pretty heroku name>.herokuapp.com" "localhost"}

CORS is not a small topic, but well worth understanding.

Cliff notes, browsers don't let pages fetch content from other domain names, unless those other domain names say they already know them and are ok with it.

Depending on what you're doing, say serving content from s3 under a different domain and wanting to access this site you just deployed, you need to say that the domain serving the content is allowed to fetch. Not doing so leads to all kinds of annoying behaviour for you, caused by browsers trying to be secure.

Problem 4 - I just keep getting "Application Error" 🤯

First thing to do, check your logs!

heroku logs --tail

Look for stuff like this:

2020-04-12T12:15:00.000000+00:00 heroku[web.1]: State changed from crashed to starting
2020-04-12T12:15:01.000000+00:00 heroku[router]: at=error code=H10 desc="App crashed"

Your app's crashed, try restarting it!

heroku ps:restart

This can also happen with an R10 or boot timeout, if that's happening a lot, try and work out why your startup time is too slow?

Another one is:

2020-04-12T17:40:10.000000+00:00 app[worker.1]: Working
2020-04-12T17:40:10.000000+00:00 heroku[worker.1]: Process running mem=569MB(111.1%)
2020-04-12T17:40:11.000000+00:00 heroku[worker.1]: Error R14 (Memory quota exceeded)
2020-04-12T17:41:52.000000+00:00 app[worker.1]: Working

That R14 is you're going over the memory by a fair bit!

Maybe scale up your dyno?

heroku ps:scale web=1:standard-2x

Pay attention to that standard-2x bit, we're jumping up to a dyno that has 1GB memory, so your server cost is going to be $50 per month per dyno!

Problem 5 - What about all my out of memory errors?

If you're still getting out of memory and application crashes there are a few simple things you can try do:

1) Are you using the lastest jvm?

If you set your system.properties file to:

java.runtime.version=14

You'll get openjdk version 14, which has a bit of improved performance =)...

2) Try a different GC

You can try to see how your application runs under some of the newer garbage collectors, such as ZGC and Shenandoah.

You need to first pass -XX:+UnlockExperimentalVMOptions, then either -XX:+UseShenandoahGC for Shenandoah or -XX:+UseZGC for ZGC.

You do this by setting JAVA_OPTS in the heroku config:

heroku config:set JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC"

Don't forget to edit your Procfile so that your web command takes those args:

web: java $JAVA_OPTS -cp fulcro.jar clojure.main -m app.server-main

I've had some success personally with Shenandoah and you should spend some time reading what they do, and what trade-offs they come with, but the options are there.

Just a note that, if you're already using openjdk do give G1 a good go, the developers have been doing some tweaks to it, and [here's mention that even if you do swap to a different GC, when version 15 comes out it will be even better.

3) Enhanced Metrics

If your dyno is at least on the hobby tier, you should be able to see metrics, under the options gear, there's a setting, Enhanced Language Metrics. Turn that on and take a look, do the numbers differ from your local profiling of the application?

Try running the application with reduced memory and see how it performs, if you're running on anything less than a standard-2x, heroku is going to set -Xmx300m limiting you max heap size to 300mb, try doing the same on your development machine and see how it does!

PPS: An Updated Example!

If you're having issues, I've updated my currently up-to-date version of the code here.

Acknowledgements

Thanks to Gert Goet for his enlightening blog post And the friendly folks in the #fulcro slack!

Tags: beginner heroku fulcro