WarpBuild LogoWarpBuild Docs

Scala with SBT

Best practices for Dockerfile for Scala with SBT

🐳 Annotated Dockerfile for Scala with SBT:

# Stage 1: Build the application
FROM sbtscala/scala-sbt:eclipse-temurin-21.0.6_7_1.10.11_3.6.4 AS builder
 
# Set working directory
WORKDIR /app
 
# Copy SBT build definition files first
COPY build.sbt ./
COPY project/ project/
 
# Cache SBT dependencies using a mount cache
RUN --mount=type=cache,target=/root/.ivy2 \
    --mount=type=cache,target=/root/.cache/coursier \
    --mount=type=cache,target=/root/.sbt \
    sbt update
 
# Copy source code
COPY src/ src/
 
# Build the application
RUN --mount=type=cache,target=/root/.ivy2 \
    --mount=type=cache,target=/root/.cache/coursier \
    --mount=type=cache,target=/root/.sbt \
    sbt "set ThisBuild / test := {}" assembly
 
# Stage 2: Create a minimal runtime image
FROM eclipse-temurin:21-alpine
 
# Create a non-root user
RUN groupadd -r scalauser && useradd -r -g scalauser scalauser
 
# Set working directory
WORKDIR /app
 
# Copy the built fat JAR from the builder stage
COPY --from=builder /app/target/scala-*/app-assembly-*.jar app.jar
 
# Set ownership and permissions
RUN chown -R scalauser:scalauser /app
 
# Switch to non-root user
USER scalauser
 
# Expose application port
EXPOSE 8080
 
# Configure JVM options for containerized environments
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom"
 
# Run the application
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -jar app.jar" ]

🔍 Why these are best practices:

✅ Multi-stage builds

  • Reduces final image size.
  • Separates build environment from runtime environment.
  • Eliminates SBT, Scala compiler, and build tools from production image.

✅ SBT dependency caching

  • Uses Docker's build cache to avoid downloading dependencies repeatedly.
  • Caches Ivy2, Coursier, and SBT directories for faster builds.
  • Dramatically improves build times in CI/CD pipelines.

✅ SBT assembly for fat JAR

  • Creates a single, self-contained JAR with all dependencies.
  • Simplifies deployment with minimal runtime requirements.
  • Ensures consistent behavior across environments.

✅ JRE-only runtime image

  • Uses minimal JRE instead of full JDK for smaller runtime image.
  • Reduces attack surface by excluding development tools.
  • Improves startup times and resource utilization.

✅ Security best practices

  • Runs the application as a non-root user.
  • Follows principle of least privilege for enhanced security.
  • Sets appropriate file ownership and permissions.

🚀 Additional Dockerfile best practices you can adopt:

Skip tests during build

For faster builds when tests are run separately:

# Explicitly skip tests during the build
RUN sbt "set ThisBuild / test := {}" assembly

Optimize JVM for containers

Fine-tune JVM settings for containerized environments:

ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError"

Add health checks

Monitor application health:

HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

Use .dockerignore

Exclude unnecessary files from your Docker build context:

target/
!target/*.jar
.git/
.github/
.bsp/
.idea/
.bloop/
.metals/
project/project/
project/target/
*.log

Configure for different environments

Use build arguments to customize your builds:

ARG SBT_PROFILE=production
 
RUN sbt "set ThisBuild / test := {}" "set ThisBuild / scalacOptions ++= Seq(\"-Xfatal-warnings\")" s"assembly $SBT_PROFILE"

Enable GraalVM native-image for Scala

For Scala 3 projects that support native compilation:

FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY . .
RUN gu install native-image
RUN sbt "nativeImage"
 
FROM scratch
COPY --from=builder /app/target/native-image/app /app
ENTRYPOINT ["/app"]

Implement proper signal handling

Ensure your container responds to orchestration signals:

# Add tini for proper signal handling
RUN apt-get update && apt-get install -y --no-install-recommends tini \
    && rm -rf /var/lib/apt/lists/*
 
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["java", "-jar", "app.jar"]

Use a distroless base image for minimal runtime

Further reduce attack surface with distroless:

FROM gcr.io/distroless/java21-debian12:nonroot
 
COPY --from=builder /app/target/scala-*/app-assembly-*.jar /app.jar
 
ENTRYPOINT ["java", "-jar", "/app.jar"]

By following these practices, you'll create Docker images for your Scala applications that are secure, efficient, and optimized for both development and production environments. These approaches help minimize build times, reduce image sizes, and ensure consistent behavior across different deployment environments, leveraging Scala's JVM foundation for robust containerized applications.

Last updated on