Deploying a full stack Scala application on Heroku
Not so long ago I was thinking “how is it not possible in 2020 to have a free (crappy) host for a JVM based server with a (potentially also crappy) database?”. And then I discovered Heroku.
Heroku gives you the opportunity to host for free a server with a small (no more that 10k lines) Postgres database. They moreover have built in support for a Scala Play application, in which case you can very easily push your application code, and Heroku compiles and deploy everything for you.
In my case, though, I wanted a little bit more: my frontend is also developed in Scala, using Scala.js for generating the JavaScript. The advantage of doing so is real: you can share a lot of code between your frontend and your backend, avoiding a lot of hassle in writing twice model definitions and validations. The dream, then, is of course to be able to run a simple sbt magicCommand
in order to compile the frontend, package the backend with it, ship it to Heroku and deploy. Good news: that dream easily comes true.
A small example project is available here. The backend there uses Play (but could use anything else such as http4s or cask) and the frontend uses Laminar (but could certainly also use Slinky, Scala-js-react or Scalatags).
TLDR: use the sbt stage backend/deployHeroku
from the sbt-heroku plugin where stage should also compile the frontend.
Project structure
A full stack Scala project is typically divided into three different sub-projects: one for the backend, one for the frontend, and a third one on which the two first depend and that will therefore cross-compile for JS and for the JVM.
The general structure of the project then looks like
build.sbt
- project
- backend
-- src
- frontend
-- src
-- webpack
- shared
-- src
The shared
project is the place where you put all the code that both your frontend and your backend are going to use. For example, all of your model definitions can go there because you will need them everywhere. That way, there is only one source of truth for your models and they are always in sync.
In particular, the frontend and the backend communicate using JSON, and to that end the shared project (and hence all of them by transitive dependencies) uses the circe library for JSON (de)serialisation. Since the library itself is cross-compiled, we are sure that our protocoles will never mismatch.
Compiling the frontend
Following the great work done by Slinky, the frontend uses webpack and scala-js-bundler for development and build purposes. We get a nice “hot reload” functionality when coding (thanks to the dev
alias command) and a simple frontend/fullOptJS/webpack
task to build the frontend ready for production, by outputting the packages files to the backend’s public folder.
Create the app
Creating a new application is extremely easy with the Heroku CLI. In our case, we want to create a simple application, together with a free Postgres database. This database is limited to 10000 lines (which should not give you the idea to transpose your tables).
We simply need the two following lines (after issuing heroku login
):
heroku apps:create APP_NAME --region eu
This creates the app with the given name on a server located in Europe (creating the server in the US is also possible). The APP_NAME
will be important for the sbt plugin. Then we add the database with
heroku addons:create heroku-postgresql:hobby-dev
And that is it! We will now deploy some code on the app.
The sbt-Heroku plugin
A Heroku application can typically be deployed by sending the code and let Heroku manage compilation and deployment. And that is possible for 8 different programming languages and at least that many web frameworks. However, for Scala applications, there is another route which is through the “sbt-heroku” plugin. This plugin gives us the ability to package the application ourselves (which is good in this case since we need to package the frontend anyway) and then send the packaged output to Heroku so that it can deploy the app for us.
The first thing to do is to add the plugin to the project/plugins.sbt
file (you should check the version on the github repo):
addSbtPlugin("com.heroku" % "sbt-heroku" % "2.1.3")
And we can disable it for every project but the backend (via disablePlugins(HerokuPlugin)
), since it’s the only one that we are going to deploy. Then, sbt-heroku requires some settings that are described in the doc. The small catch is that in our case, these settings must be defined inside the backend project. I.e., something like
lazy val `backend` = (project in file("./backend"))
.enablePlugins(PlayScala) // for example
.settings(
herokuAppName in Compile := "APP_NAME",
herokuProcessTypes in Compile := Map(
"web" -> "target/universal/stage/bin/backend -Dhttp.port=$PORT" // command for Heroku to launch the server
//"worker" -> "java -jar target/universal/stage/lib/my-worker.jar"
),
herokuIncludePaths in Compile := Seq(
"backend/app",
"backend/conf/routes",
"backend/conf/application.conf",
"backend/public"
),
// [...]
)
.dependsOn(sharedJVM)
After login in to Heroku (via heroku login
), if you now run sbt stage backend/deployHeroku
, you will likely hit two problems
- the frontend does not build into the backend public directories, and
- you will see that the task
backend/deployHeroku
finishes with “success” but nothing actually happened.
(If you do not use Play, you might also have the problem that the stage
task is not defined. You need the sbt native packager for that to work.)
The first thing is easy to fix. We simply need to override the stage
task in the following way
stage := {
val webpackValue = (frontend / Compile / fullOptJS / webpack).value
println(s"Webpack value is $webpackValue")
(stage in backend).value
}
The second “failure” comes from the fact that when building, sbt-heroku checks that either there exists a “project” directories, or that herokuSkipSubProjects
is false. This is a bit weird but, in any case, it’s easily solved by adding herokuSkipSubProjects in Compile := false
in the backend settings.
You might also run into the issue of OOM with the fullOptJS
task (because of the Google closure compiler). This is easily fixed by giving sbt more memory.
And that is all you have to do in order to compile, package and deploy your full stack Scala application!
Accessing the database
We created an app with a Postgres database. To connect to it, we need to have, for example, the JDBC url. In Heroku, any application provided with a Postgres database has an environment variable called JDBC_DATABASE_URL
. If you use the configuration files from Typesafe, you can for example retrieve it with
url = ${?JDBC_DATABASE_URL}
The “?” means that it does not override a previous value for url if the environment variable does not exist. This is handy for working in dev: you can use a local database address in that case.
Specific to Play
If you are using the Play framework, here are some small random remarks that could serve as a FAQ:
- in production, you need to define an application secret. You can easily do that by setting an environment variable on your Heroku app, for example via
heroku config:set APPLICATION_SECRET=mycoolsecret
and then in the configuration file, useplay.http.secret.key = ${?APPLICATION_SECRET}
- your application in Heroku lives in a container, so the origin actually accessing your app is probably unknown. Play will reject incoming connections unless you set
play.filters.host.allowed = ["."]
. - another consequence on having the application living in a container is that you need to know the port on which you traffic will pass. On Heroku, you receive an environment variable
PORT
which contains that piece of information - you most likely want to enable auto evolutions of the database. This is done via
play.evolutions.autoApply = true
- in dev mode, you can create a configuration file with dev values for all the afore-mentioned variables, such as the db url
Wrap up
Hopefully this post can save you the hassle to collect all the small bits of information needed to put all of this together. Admittedly, this post is also a letter to me in the future, but I figured I could share it!
Once everything is set up (which is not even that hard/long), it’s a genuine pleasure to only have one sbt task to run, and have everything deployed within a few minutes!