Creating a Docker Container from a Scala/SBT project in TeamCity for Use with Octopus Deploy

I considered creating a series of blog posts about my journey into Scala titled “How much I hate Scala/SBT Part XX”, however i decide not to be that bitter. The language isn’t bad, its just the ecosystem around it sucks, I am more likely to find the source code for something from a google search, rather than a stack overflow post or even documentation.

So here’s where I started, I will assume your Scala project is already Packaging up the project with a TeamCity build and SBT step running compile and your ready to move to a docker container.

So the key things here is the version number for me, I use a custom variable called “Version” that i usually set to something like “1.0.%build.counter%”, for my dot NET projects i use the assembly info patcher and this is then used in the package version, so with your docker containers you can use the tag for the version. Octopus Deploy needs the version on the Tag to work effectively.

If you look internally how TeamCity’s SBT runner runs the comment you will see something like the following:

[11:49:38][Step 2/2] Starting: /usr/java/jdk1.8.0_121/bin/java -Dagent.home.dir=/opt/buildagent -Dagent.name=tt-xxx-2004 -Dagent.ownPort=9090 -Dagent.work.dir=/opt/buildagent/work -Dbuild.number=1.0.12 -Dbuild.vcs.number=411695cf560acb5b7e4b2eb837738660acf0e287 -Dbuild.vcs.number.1=411695cf560acb5b7e4b2eb837738660acf0e287 -Dbuild.vcs.number.Ycs_SupplyApiService_YcsSuppioScala1=411695cf560acb5b7e4b2eb837738660acf0e287 -Djava.io.tmpdir=/opt/buildagent/temp/buildTmp -Dsbt.ivy.home=/opt/buildagent/system/sbt_ivy -Dteamcity.agent.cpuBenchmark=627 -Dteamcity.agent.dotnet.agent_url=http://localhost:9090/RPC2 -Dteamcity.agent.dotnet.build_id=1022735 -Dteamcity.auth.password=******* -Dteamcity.auth.userId=TeamCityBuildId=1022735 -Dteamcity.build.changedFiles.file=/opt/buildagent/temp/buildTmp/changedFiles3407499404619574497.txt -Dteamcity.build.checkoutDir=/opt/buildagent/work/36dd69c049b3f712 -Dteamcity.build.id=1022735 -Dteamcity.build.properties.file=/opt/buildagent/temp/buildTmp/teamcity.build2492188537600221102.properties -Dteamcity.build.tempDir=/opt/buildagent/temp/buildTmp -Dteamcity.build.workingDir=/opt/buildagent/work/36dd69c049b3f712 -Dteamcity.buildConfName=DevelopPushDocker -Dteamcity.buildType.id=FFFGGHHH -Dteamcity.configuration.properties.file=/opt/buildagent/temp/buildTmp/teamcity.config6701741486713268575.properties -Dteamcity.projectName=APIService -Dteamcity.runner.properties.file=/opt/buildagent/temp/buildTmp/teamcity.runner3391739853422434247.properties -Dteamcity.tests.recentlyFailedTests.file=/opt/buildagent/temp/buildTmp/testsToRunFirst5160519002097758075.txt -Dteamcity.version=2017.1 (build 46533) -classpath /opt/buildagent/temp/agentTmp/agent-sbt/bin/sbt-launch.jar:/opt/buildagent/temp/agentTmp/agent-sbt/bin/classes: xsbt.boot.Boot < /opt/buildagent/temp/agentTmp/commands5523308191041557049.file
 I’ve highlighted the one I am after, but you can see that TeamCity is passing a lot of data to the SBT runner, it uses java to run from the command line instead of just running the SBT command itself.
There is something i am missing though, I need to know the branch name, becuase we have a convention that if is not built from the master branch we use a “-branchname” at the end. So to add this in you need to edit your SBT runner step in teamcity and add the below
SBTParametersFromCommandLineTeamCity
From this we can use this variable in our Build.Scala file like so, I also add a value for team that is used later.
val dockerRegistry = "MyprivateDockerReg.company.com"
 val team = "myteam"
val appName = "ycs-supply-api"
 var PROJECT_VERSION = Option(System.getProperty("build.number")).getOrElse("0.0.4")

 val BRANCH = Option(System.getProperty("teamcity.build.branch")).getOrElse("SNAPSHOT")
 if ( BRANCH != "master")
 {
 PROJECT_VERSION = PROJECT_VERSION + "-" + BRANCH.replace("-","").replace("/","")
 }
Now for docker, in you SBT plugins directory make sure you have this line to import the plugin
addSbtPlugin(“se.marcuslonnberg” % “sbt-docker” % “1.4.1”)
Then here is what our Build.sbt looks like
import scala.xml.{Elem, Node}

enablePlugins(JavaAppPackaging)

name := "MyAppBin"

dockerfile in docker := {
val appDir: File = stage.value
val targetDir = s"/opt/$team"

new Dockerfile {
from("java:8-jre-alpine")
maintainer(s"$team")

runRaw(s"mkdir -p $targetDir")
workDir(s"$targetDir")
copy(appDir, targetDir)
expose(80)
env("JAVA_OPTS" -> s"-Dappname=$appName -Dconfig.file=conf/application-qa.conf -Dlog.dir=log/ -Dlogback.configurationFile=conf/logback-test.xml -Xms256m -Xmx256m -server")
entryPoint(s"bin/${name.value}")
}
}

imageNames in docker := Seq(
// SPAPI Sets the latest tag
ImageName(s"$dockerRegistry/$team/$appName:latest"),
// SPAPI Sets a name with a tag that contains the project version
ImageName(s"$dockerRegistry/$team/$appName:${version.value}")
)

mainClass in Compile := Some("Boot")

buildOptions in docker := BuildOptions(
cache = true,
removeIntermediateContainers = BuildOptions.Remove.Always,
pullBaseImage = BuildOptions.Pull.IfMissing
)
This will get us building the container and saving it to the local docker cache. After this we need to push it to our private registry.
There is currently an open issue about SBT-Docker here so I couldn’t get docker login to run from SBT so I created a separate task in TeamCity to handle this.
To do this i want to keep a lot of my settings in the Build.Scala so that the experience on the local will be similar to the build server, but I don’t want to text parse, so what we can do is output some logs for SBT to tell TeamCity what settings to use.
Add these two lines in

println(s"##teamcity[setParameter name='DockerContainerCreated' value='$dockerRegistry/$team/$appName:${version.value}']")
println(s"##teamcity[setParameter name='SbtDockerRegistry' value='$dockerRegistry']")</div>
This Will make SBT output the format that TeamCity Reads to set parameters and allow us to create the next step as a command line step.
TeamCitySetParameterFromSBTCommandLine
Next add these parameters is as empty
EmptyParamterTeamCitySetParameterFromCommandLine
Then we can create a command line step that does the docker login/push
PushContainerToDockerRegistryWithVersionTagFromTeamCity
And we are done! you should see your container in the registry now, and if like us you are using Octopus Deploy you will see the container appear on searches and the version numbers will correct be associated with the containers