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
.sbtextension, usuallybuild.sbt project/build.properties- A
.scalafile underproject/ - A
.scalafile 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:
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:
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:
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:
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.7")
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.versionchanges. - 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:
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.