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"
  (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)

(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!

PPS: An Updated Example!

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


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

Tags: beginner heroku fulcro