Docs menu

Scala on Primcloud

How Primcloud detects a Scala app, resolves the JDK and sbt, runs the build, caches Ivy and Coursier, and launches the packaged output.

On this page

From the files in your repo, Primcloud detects a Scala app, resolves the JDK and sbt versions you asked for, runs sbt compile stage, caches the dependency stores between builds, and hands off to the Procfile line that launches the packaged app. This page walks each of those phases.

Detection#

Primcloud runs the Scala toolchain when the root of your repo contains any of:

  • A file with the .sbt extension, usually build.sbt
  • project/build.properties
  • A .scala file under project/
  • A .scala file under .sbt/

With none of these present, the build stops with a "no language detected" error before it starts.

The JDK comes along whenever an sbt build asks for it, or when a system.properties file at the repo root sets java.runtime.version.

Resolving sbt and the JDK#

sbt. Primcloud reads the sbt version from project/build.properties and runs the build with exactly that version:

project/build.properties
sbt.version=1.11.7

This file is required. Without a sbt.version, the build fails with a "No sbt version defined" error. Supported versions are the 1.x and 2.x series, with one exception: 2.0.0 is rejected, because upstream sbt has a plugin-loading bug in that exact release. Use 2.0.1 or later on the 2.x line.

JDK. The build installs an OpenJDK release. It defaults to the latest LTS, and you pin a version via system.properties:

system.properties
java.runtime.version=25

You can pin a major (25) or an exact patch. See Runtime versions for the accepted value shapes and the sbt version rules in full.

Compile phase#

Primcloud runs:

Terminal
sbt compile stage

compile builds your Scala sources. stage is expected to produce a runnable output. Most apps get stage for free from sbt-native-packager. Play enables it automatically through PlayScala, and any other project can enable it explicitly:

project/plugins.sbt
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.7")
build.sbt
enablePlugins(JavaAppPackaging)

sbt-native-packager writes a runnable app to target/universal/stage/bin/<app-name>, which your Procfile invokes at launch.

Overriding the default tasks. You can prepend tasks, replace them, or clean before building. Each is configurable from system.properties or an environment variable, and system.properties wins when both are set. Both surfaces are documented in Customizing the build.

Caching#

Primcloud caches the parts of a Scala build that are slow to rebuild, so a second deploy doesn't re-download the world:

  • Ivy cache: Ivy's resolver store.
  • Coursier cache: Coursier's resolver store. sbt uses Coursier as its default resolver on 1.3 and later.
  • sbt itself: the launcher and its per-version JARs, refetched when sbt.version changes.
  • JDK: the resolved OpenJDK, its download checksum verified before it's reused.

The build log's Distributing dependencies and Running sbt build sections show which of these were restored and which had to be re-downloaded.

By default these are build-only: they don't ship in the runtime image. If your app genuinely needs to call sbt at runtime (rare, and usually a sign to restructure the build), set sbt.available-at-launch=true in system.properties or SBT_AVAILABLE_AT_LAUNCH=true in the environment, and the layers become part of the runtime image.

How the app launches#

A Procfile is required. The Scala build doesn't guess a launch command for you, so ship a Procfile at the repo root that runs whatever stage produced. For a typical sbt-native-packager build:

Procfile
web: target/universal/stage/bin/my-app -Dhttp.port=${PORT}

Replace my-app with the name from your build.sbt. See Pod Formations for the file format and multi-process patterns. A web process takes traffic, and any other process type you declare runs as a worker.

Port binding. Primcloud sets PORT on the web process only; other process types don't get one. Your web launch command has to bind to it. Play and most sbt-native-packager output take -Dhttp.port=$PORT as shown; Akka HTTP, http4s, and plain servlet apps need the equivalent flag for their own server.

Runtime environment. Primcloud sets these variables on every process at runtime:

Variable Value Purpose
JAVA_HOME Path to the installed OpenJDK So the JVM launcher finds its runtime.
PATH With $JAVA_HOME/bin prepended So java is on PATH.
LD_LIBRARY_PATH Includes $JAVA_HOME/lib For native libraries loaded through JNI.
JAVA_TOOL_OPTIONS Memory defaults sized to the pod The standard JVM hook for passing options without editing the launch command.
PORT Assigned by Primcloud Set on the web process only.

Database URL rewriting. If your app carries a DATABASE_URL in the URI shape postgres://user:pass@host:port/db, Primcloud derives companion JDBC_DATABASE_URL, JDBC_DATABASE_USERNAME, and JDBC_DATABASE_PASSWORD variables from it, handy when your Scala code, or a pool like HikariCP, wants a jdbc:postgresql://... URL directly. Unset DATABASE_URL if you don't want the derived variables.

Anything you set in the Secrets panel is layered on top of these at runtime.