🎁 Packaging Scala Applications¶
Scala compiles down to Java byte code, which can then be run on any system running the JVM. It would be nice if one could extend this to native system binaries that can be run anywhere. Here I walk through the steps of getting SBT-Native-Packager to create a native binary as well as a docker image that can run my 'Hello World' application.
For this short post, I have created a "simpleApp" which is a simple "Hello, World!" application in Scala. The example code for this post is in the standard maven directory layout, i.e:
.
├── build.sbt
├── project
│ ├── build.properties
│ ├── plugins.sbt
│ ├── project
│ └── target
├── src
│ ├── main
│ │ └── scala
│ │ └── simple
│ │ └── SimpleApp.scala
│ └── test
│ └── scala
│ └── simple
└── target
and with the SimpleApp.scala
code looking like so:
package simple;
object SimpleApp {
def main(args: Array[String]): Unit = {
println("Hello, world!")
}
}
Assuming you are familiar with sbt
, one can use the following to compile the program to Java byte code and run:
$ cd simpleApp
$ sbt clean compile run
[info] Loading settings for project simpleapp-build from plugins.sbt ...
[info] Loading project definition from /Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/project
[info] Loading settings for project simpleapp from build.sbt ...
[info] Set current project to SimpleSBTProject (in build file:/Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/)
[info] Executing in batch mode. For better performance use sbt's shell
[success] Total time: 0 s, completed 21-May-2020 11:16:49
[info] Compiling 1 Scala source to /Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/target/scala-2.12/classes ...
[success] Total time: 5 s, completed 21-May-2020 11:16:54
[info] running simple.SimpleApp
Hello, world!
[success] Total time: 0 s, completed 21-May-2020 11:16:54
$ sbt "runMain simple.SimpleApp"
[info] Loading settings for project simpleapp-build from plugins.sbt ...
[info] Loading project definition from /Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/project
[info] Loading settings for project simpleapp from build.sbt ...
[info] Set current project to SimpleSBTProject (in build file:/Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/)
[info] Compiling 1 Scala source to /Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/target/scala-2.12/classes ...
[info] running simple.SimpleApp
Hello, world!
[success] Total time: 4 s, completed 21-May-2020 11:22:34
Ok, great. We have a running Scala application built and run with sbt
. But the point of this post is that we want to run a native binary...
To achieve this sbt-native-packager
plugin is used. Before we use it, one must ensure the plugin is "installed" in the project/plugins.sbt
file like so:
$ cat -n project/plugins.sbt
1 logLevel := Level.Warn
2
3 // for autoplugins
4 addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.6.1")
Furthermore we need to ensure we have set the right variables in the build.sbt
file such as:
$ sed -n 1,7p build.sbt
name := "SimpleSBTProject"
version := "1.0"
scalaVersion := "2.12.8"
enablePlugins(JavaAppPackaging)
With these two modified files and the project directory set up the way it is, we are ready to create our binary using the stage
directive for sbt
.
$ sbt clean compile stage
[info] Loading settings for project simpleapp-build from plugins.sbt ...
[info] Loading project definition from /Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/project
[info] Loading settings for project simpleapp from build.sbt ...
[info] Set current project to SimpleSBTProject (in build file:/Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/)
[info] Executing in batch mode. For better performance use sbt's shell
[success] Total time: 0 s, completed 21-May-2020 11:29:50
[info] Compiling 1 Scala source to /Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/target/scala-2.12/classes ...
[success] Total time: 5 s, completed 21-May-2020 11:29:55
[info] Main Scala API documentation to /Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/target/scala-2.12/api...
[info] Wrote /Users/tallamjr/www/blog/code/posts/2020-02-10-SBT-native-packaging/simpleApp/target/scala-2.12/simplesbtproject_2.12-1.0.pom
model contains 3 documentable templates
[info] Main Scala API documentation successful.
[success] Total time: 2 s, completed 21-May-2020 11:29:56
Using stage
creates a new set of files, found in the target/universal
directory:
$ tree target/universal/
target/universal/
├── scripts
│ └── bin
│ ├── simplesbtproject
│ └── simplesbtproject.bat
└── stage
├── bin
│ ├── simplesbtproject
│ └── simplesbtproject.bat
└── lib
├── org.scala-lang.scala-library-2.12.8.jar
└── simplesbtproject.simplesbtproject-1.0.jar
5 directories, 6 files
Notice it creates a Unix and Windows version.
Now let's run it...
$ ./target/universal/stage/bin/simplesbtproject
Hello, world!
Success, we can run a single "binary" for our application. I put binary in quotation marks there because it is not exactly a binary file that is created, and one would still need Java installed on the system, but it allows a user to run a command like the above which is better suited for production settings.
Ok, now that we can do this, "What about making a Docker image!" I hear you cry. This can also be done with some minor additions to the build.sbt
file with:
$ sed -n 9,18p build.sbt
enablePlugins(DockerPlugin)
// change the name of the project adding the prefix of the user
packageName in Docker := "tallamjr/" + packageName.value
//the base docker images
dockerBaseImage := "java:8-jre"
//the exposed port
dockerExposedPorts := Seq(9000)
//exposed volumes
dockerExposedVolumes := Seq("/opt/docker/logs")
With this in place, we can use publishLocal
to create a docker image locally, which, when built we will run.
Just to convince ourselves a new docker images is created docker images
reveals only other projects after doing a docker system prune -a
:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
machcoll_bluebird latest 140ca65afae2 2 months ago 1.15GB
machcoll-img dev-1bea126 38298723afa1 2 months ago 1.01GB
nats-metrics-img master-9a39559 3af43d2f4bd7 2 months ago 1.1GB
Running sbt publish:Local
creates the docker image for us (I have excluded much of the output for brevity)
$ sbt publish:Local
...
[info] Built image tallamjr/simpleapp with tags [1.0]
[success] Total time: 53 s, completed 21-May-2020 11:46:06
Now when we do docker images
again we should see our new docker image:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
tallamjr/simpleapp 1.0 6eb32a8b2939 About a minute ago 317MB
machcoll_bluebird latest 140ca65afae2 2 months ago 1.15GB
machcoll-img dev-1bea126 38298723afa1 2 months ago 1.01GB
nats-metrics-img master-9a39559 3af43d2f4bd7 2 months ago 1.1GB
java 8-jre e44d62cf8862 3 years ago 311MB
Let's test this:
$ docker run tallamjr/simpleapp:1.0
Hello, world!
So there we have it! sbt-native-packager
is able to also publish to a docker registry and in addition to standard scripts like the one above, one can also create other formats
universal:packageBin - Generates a universal zip file
universal:packageZipTarball - Generates a universal tgz file
debian:packageBin - Generates a deb
docker:publishLocal - Builds a Docker image using the local Docker server
rpm:packageBin - Generates an rpm
universal:packageOsxDmg - Generates a DMG file with the same contents as the universal zip/tgz.
windows:packageBin - Generates an MSI
For more check out the great documentation here
All the code for this post is available on Github