# Create a hello world project
to create a new scala project you can use this command sbt new scala/hello-world.g8
. This require to have sbt already installed.
$ sbt new scala/hello-world.g8
Use hello-world
as application name.
This will generate the following tree
$ tree hello-world/
hello-world/
├── build.sbt
├── project
│ └── build.properties
└── src
└── main
└── scala
└── Main.scala
# Run the hello world project
$ sbt run
[info] [launcher] getting org.scala-sbt sbt 1.3.2 (this may take some time)...
downloading https://repo1.maven.org/maven2/org/scala-sbt/sbt/1.3.2/sbt-1.3.2.jar ...
...
...
[info] Compilation completed in 8.27s.
[info] running Main
Hello, World!
# Package the application
The JavaAppPackaging
archetype from sbt-native-packager
provides a default application structure and executable scripts to launch your application.
Add the sbt-native-packager
to your plugins.sbt
(Create a new one under project folder)
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.7.1")
Enable the JavaAppPackaging plugin in your build.sbt
with
enablePlugins(JavaAppPackaging)
This can be done using the following command.
$ sbt stage
The result is available under target/universal/stage
$ tree ./target/universal/stage/
./target/universal/stage/
├── bin
│ ├── hello-world
│ └── hello-world.bat
└── lib
├── ch.epfl.scala.hello-world-1.0.jar
├── org.scala-lang.scala-library-2.13.1.jar
├── org.typelevel.cats-core_2.13-2.0.0.jar
├── org.typelevel.cats-kernel_2.13-2.0.0.jar
└── org.typelevel.cats-macros_2.13-2.0.0.jar
Test the generated script
$ bash ./target/universal/stage/bin/hello-world
Hello, World!
The usage below show some useful option already available into generated script, like :
- verbose and debug options
- a multi main class support
- selection of java version to use
- JVM options
$ bash target/universal/stage/bin/hello-world -h
Usage: [options]
-h | -help print this message
-v | -verbose this runner is chattier
-d | -debug enable debug output for the launcher script
-no-version-check Don t run the java version check.
-main <classname> Define a custom main class
-jvm-debug <port> Turn on JVM debugging, open at the given port.
# java version (default: java from PATH, currently openjdk version "11.0.7" 2020-04-14)
-java-home <path> alternate JAVA_HOME
# jvm options and output control
JAVA_OPTS environment variable, if unset uses ""
-Dkey=val pass -Dkey=val directly to the java runtime
-J-X pass option -X directly to the java runtime
(-J is stripped)
# special option
-- To stop parsing built-in commands from the rest of the command-line.
e.g.) enabling debug and sending -d as app argument
$ ./start-script -d -- -d
In the case of duplicated or conflicting options, basically the order above
shows precedence: JAVA_OPTS lowest, command line options highest except "--".
Available main classes:
Main
# Generate a docker image for the application
The Docker Plugin
from sbt-native-packager
implement the following features
- generate a Dockerfile based on
JavaAppPackaging
archetype stage. - sbt integration to build the docker image
Enable the Docker Plugin in your build.sbt
with
enablePlugins(DockerPlugin)
Run this command to generate a Dockerfile
$ sbt docker:stage
You can see into below the tree of generated files
- stage 1 : script and dependencies lib
- stage 2 : the application jar
- the Dockerfile
$ tree target/docker/stage/
target/docker/stage/
├── 1
│ └── opt
│ └── docker
│ ├── bin
│ │ ├── hello-world
│ │ └── hello-world.bat
│ └── lib
│ ├── org.scala-lang.scala-library-2.13.1.jar
│ ├── org.typelevel.cats-core_2.13-2.0.0.jar
│ ├── org.typelevel.cats-kernel_2.13-2.0.0.jar
│ └── org.typelevel.cats-macros_2.13-2.0.0.jar
├── 2
│ └── opt
│ └── docker
│ └── lib
│ └── ch.epfl.scala.hello-world-1.0.jar
└── Dockerfile
9 directories, 8 files
Let's comment the content of the Dockerfile
$ more target/docker/stage/Dockerfile
# A multi stage Dockerfile
# the first stage named `stage0` based on `openjdk:8`
FROM openjdk:8 as stage0
# some meta data
LABEL snp-multi-stage="intermediate"
LABEL snp-multi-stage-id="2240a3f8-0c82-46f2-8c84-fdd8d43c83f0"
# change to /opt/docker folder
WORKDIR /opt/docker
# copy target/docker/stage/1/opt from host to /1/opt into container
COPY 1/opt /1/opt
# copy target/docker/stage/2/opt from host to /2/opt into container
COPY 2/opt /2/opt
# run following command as `root`
USER root
# make /1/opt/docker read only
RUN ["chmod", "-R", "u=rX,g=rX", "/1/opt/docker"]
# make /2/opt/docker read only
RUN ["chmod", "-R", "u=rX,g=rX", "/2/opt/docker"]
# add execution right to the generated script
RUN ["chmod", "u+x,g+x", "/1/opt/docker/bin/hello-world"]
# the second stage named `mainstage` based also on `openjdk:8`
FROM openjdk:8 as mainstage
USER root
# create a demiourgos728 user with id 1001
RUN id -u demiourgos728 1>/dev/null 2>&1 || (( getent group 0 1>/dev/null 2>&1 || ( type groupadd 1>/dev/null 2>&1 && groupadd -g 0 root || addgroup -g 0 -S root )) && ( type useradd 1>/dev/null 2>&1 &&
useradd --system --create-home --uid 1001 --gid 0 demiourgos728 || adduser -S -u 1001 -G root demiourgos728 ))
WORKDIR /opt/docker
# copy the content of /1/opt/docker and /2/opt/docker from stage0 into /opt/docker of the mainstage
COPY /1/opt/docker /opt/docker
COPY /2/opt/docker /opt/docker
# use the created user to run the final docker image
USER 1001:0
# set the hello-world script as an entrypoint of the final docker image
ENTRYPOINT ["/opt/docker/bin/hello-world"]
CMD []
To build the docker image run the following command
$ sbt docker:publishLocal
The result is a docker image named with the same name as the project and with the same version as the project.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world 1.0 e2585c330c0c 29 seconds ago 525MB
Launch this command to test
$ docker run --rm -ti hello-world:1.0
Hello, World!
# Optimise the docker image
The generated docker image have 525MB, let try to optimise this size.
The docker history give as the size of each layer. Here we use grep to get only layer that the size is above or equals 1MB.
The third column of this output show the Dockerfile command that generate this layer.
If we check the Dockerfile we can see that only the first command COPY --chown=demiourgos728
is present.
The other command come from the openjdk:8
base image
$ docker history hello-world:1.0 | grep MB
c4658e3633a0 10 minutes ago /bin/sh -c #(nop) COPY --chown=demiourgos728… 13.9MB
6cedfea72886 2 weeks ago /bin/sh -c set -eux; dpkgArch="$(dpkg --pr… 206MB
<missing> 2 weeks ago /bin/sh -c set -eux; apt-get update; apt-g… 11.1MB
<missing> 2 weeks ago /bin/sh -c apt-get update && apt-get install… 146MB
<missing> 2 weeks ago /bin/sh -c set -ex; if ! command -v gpg > /… 17.5MB
<missing> 2 weeks ago /bin/sh -c apt-get update && apt-get install… 16.5MB
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:f086177965196842a… 114MB
In fact openjdk:8
base image have 510MB as size
$ docker image ls | grep openjdk | grep "8 "
openjdk 8 6cedfea72886 2 weeks ago 510MB
Below a list of some openjdk image and we can see that adoptopenjdk
jre version the smallest one
$ docker image ls | grep openjdk | grep "jdk" | sort -h -k7
adoptopenjdk 11.0.7_10-jre-hotspot 015147d37319 2 weeks ago 227MB
openjdk 11.0.7-jre faf75cb9edb2 2 weeks ago 285MB
adoptopenjdk 11.0.7_10-jdk-hotspot 6995882bb329 2 weeks ago 420MB
openjdk jre 6b23d41384f9 13 months ago 479MB
openjdk 8 6cedfea72886 2 weeks ago 510MB
openjdk 11.0.7 f5de33dc9079 2 weeks ago 627MB
The Docker Plugin
provide an option to override the base image.
Add this line in your build.sbt
dockerBaseImage := "adoptopenjdk:11.0.7_10-jre-hotspot"
and run again
sbt docker:publishLocal
Verify that the base image has changed into Dockerfile
$ more target/docker/stage/Dockerfile | grep FROM
FROM adoptopenjdk:11.0.7_10-jre-hotspot as stage0
FROM adoptopenjdk:11.0.7_10-jre-hotspot as mainstage
The new generated docker image is 54% smaller
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world 1.0 7f8b21413d44 10 seconds ago 241MB
<none> <none> e2585c330c0c 52 minutes ago 525MB
run
docker image prune -f
to remove all dangling images.
Check that application still working
$ docker run --rm -ti hello-world:1.0
Hello, World!
By default, sbt Native Packager will create a daemon user named demiourgos728.
To change name of the user, add this line in your build.sbt
daemonUser in Docker := "daemon"
# Source code
https://github.com/mohamedelhabib/hello-world-scala-docker (opens new window)
# References
See more options into :