Contents

Building Spring boot app for the cloud

Short reference for building a Spring boot application Docker container with Java 22.

Why being small in the cloud matters

With the cloud era and tens to hundreds of services within a company, we aim in reducing the costs of CPU and especially RAM usage of our applications, to be as efficient as possible and thus reducing the price.

Memory usage
If an application that was initially 500Mb would get reduced to 250Mb, it would cut company cloud costs by 2x. A lot of environments will increase or decrease the number of these applications to handle a load increase or reduction in order to handle costs, so scaling up a 500mb app 4 times ends up being ~2GB of RAM where it could be just ~1GB and so on.

There are some times when applications are not even used or minimal, we don’t want to have a 2GB service sitting there handling only a couple of requests, we want to have a minimal one that we can automatically scale when we get an increase.

Building the jar

It’s important to keep in mind, because we are building a Java app, that we need to have an OS on which we run the JVM on which we load our application. That makes our resulting Docker image “fat” with the size of ~750MB (Spring boot that includes postgresql and flyway), which is huge. This is of course because we put everything on the pile that the app will use or not.

How can we get it smaller?

We use the following

  1. multi-stage builds so that we first build the app in one image and then have just the runtime in the production image that runs the app.
  2. Layering Docker images. This layering is designed to separate code based on how likely it is to change between application builds.
  3. jlink, we can create our own, small JRE that contains only the relevant classes that we want to use, without wasting memory, and as a result, we’ll see increased performance. Here we use an existing custom alpine Docker image for Java 22 eclipse-temurin:22.0.2_9-jre-alpine.

Here is the Dockerfile that contains everything.

# Build jar
FROM maven:3.9.9-eclipse-temurin-22-jammy AS maven
RUN mkdir /application
WORKDIR /application
COPY src ./src
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn clean package -DskipTests

# Extract jar layers
FROM eclipse-temurin:22-jre-ubi9-minimal AS builder
WORKDIR /builder
COPY --from=maven /application/target/*.jar application.jar
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted

# Runtime
FROM eclipse-temurin:22.0.2_9-jre-alpine
WORKDIR /application

COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./

RUN addgroup --system appuser \
    && adduser -S -s /usr/sbin/nologin -G appuser appuser

USER appuser

ENTRYPOINT ["java","-XX:MaxRAMPercentage=70", "-jar","application.jar"]

And then build it with the Docker command

docker build -t myapp .

We can see that the resulting docker image has the size of 362MB, which is 2x smaller than the starting one! Spring boot effective Docker images reference.

Native

There is also a possibility to build native images for Spring boot which can reduce the application memory size even more (2-3x) BUT, I just gave up on making Graalvm work on Spring boot app. It’s just unstable and incredibly time-consuming. This is just no expected to be easy with Spring as that framework was made in 2003. and there is too much reflection abuse and questionable framework design that doesn’t fare well in today’s standards.

If you plan on sticking and trying to set up Spring boot for native, expect to face following errors:

2.083 [INFO] Building commandrunner 0.0.1-SNAPSHOT
2.083 [INFO] --------------------------------[ jar ]---------------------------------
2.251 [INFO] 
2.251 [INFO] --- maven-clean-plugin:3.3.2:clean (default-clean) @ commandrunner ---
2.264 [INFO] 
2.264 [INFO] --- native-maven-plugin:0.10.2:add-reachability-metadata (add-reachability-metadata) @ commandrunner ---
2.267 Downloading from central: https://repo.maven.apache.org/maven2/org/graalvm/buildtools/graalvm-reachability-metadata/0.10.2/graalvm-reachability-metadata-0.10.2-repository.zip
Downloaded from central: https://repo.maven.apache.org/maven2/org/graalvm/buildtools/graalvm-reachability-metadata/0.10.2/graalvm-reachability-metadata-0.10.2-repository.zip (251 kB at 2.8 MB/s)
2.362 [INFO] Downloaded GraalVM reachability metadata repository from file:/root/.m2/repository/org/graalvm/buildtools/graalvm-reachability-metadata/0.10.2/graalvm-reachability-metadata-0.10.2-repository.zip
2.460 [INFO] [graalvm reachability metadata repository for com.zaxxer:HikariCP:5.1.0]: Configuration directory not found. Trying latest version.
2.460 [INFO] [graalvm reachability metadata repository for com.zaxxer:HikariCP:5.1.0]: Configuration directory is com.zaxxer/HikariCP/5.0.1
2.462 [INFO] [graalvm reachability metadata repository for org.hibernate.orm:hibernate-core:6.5.2.Final]: Configuration directory not found. Trying latest version.
2.462 [INFO] [graalvm reachability metadata repository for org.hibernate.orm:hibernate-core:6.5.2.Final]: Configuration directory is org.hibernate.orm/hibernate-core/6.5.0.Final
2.463 [INFO] [graalvm reachability metadata repository for org.jboss.logging:jboss-logging:3.5.3.Final]: Configuration directory not found. Trying latest version.
2.463 [INFO] [graalvm reachability metadata repository for org.jboss.logging:jboss-logging:3.5.3.Final]: Configuration directory is org.jboss.logging/jboss-logging/3.5.0.Final
2.464 [INFO] [graalvm reachability metadata repository for org.glassfish.jaxb:jaxb-runtime:4.0.5]: Configuration directory not found. Trying latest version.
2.464 [INFO] [graalvm reachability metadata repository for org.glassfish.jaxb:jaxb-runtime:4.0.5]: Configuration directory is org.glassfish.jaxb/jaxb-runtime/3.0.2
2.466 [INFO] [graalvm reachability metadata repository for ch.qos.logback:logback-classic:1.5.6]: Configuration directory not found. Trying latest version.
2.466 [INFO] [graalvm reachability metadata repository for ch.qos.logback:logback-classic:1.5.6]: Configuration directory is ch.qos.logback/logback-classic/1.4.9
2.467 [INFO] [graalvm reachability metadata repository for org.hibernate.validator:hibernate-validator:8.0.1.Final]: Configuration directory not found. Trying latest version.
2.467 [INFO] [graalvm reachability metadata repository for org.hibernate.validator:hibernate-validator:8.0.1.Final]: Configuration directory is org.hibernate.validator/hibernate-validator/7.0.4.Final
2.467 [INFO] [graalvm reachability metadata repository for org.hibernate.validator:hibernate-validator:8.0.1.Final]: Configuration directory not found. Trying latest version.
2.467 [INFO] [graalvm reachability metadata repository for org.hibernate.validator:hibernate-validator:8.0.1.Final]: Latest version not found!
2.467 [INFO] [graalvm reachability metadata repository for org.hibernate.validator:hibernate-validator:8.0.1.Final]: missing.
2.468 [INFO] [graalvm reachability metadata repository for org.apache.tomcat.embed:tomcat-embed-core:10.1.26]: Configuration directory not found. Trying latest version.
2.468 [INFO] [graalvm reachability metadata repository for org.apache.tomcat.embed:tomcat-embed-core:10.1.26]: Configuration directory is org.apache.tomcat.embed/tomcat-embed-core/10.0.20
2.470 [INFO] [graalvm reachability metadata repository for com.fasterxml.jackson.core:jackson-databind:2.17.2]: Configuration directory not found. Trying latest version.
2.470 [INFO] [graalvm reachability metadata repository for com.fasterxml.jackson.core:jackson-databind:2.17.2]: Configuration directory is com.fasterxml.jackson.core/jackson-databind/2.15.2
2
2.471 [INFO] [graalvm reachability metadata repository for org.flywaydb:flyway-core:10.10.0]: Configuration directory is org.flywaydb/flyway-core/10.10.0
2.471 [INFO] [graalvm reachability metadata repository for org.flywaydb:flyway-database-postgresql:10.10.0]: Configuration directory is org.flywaydb/flyway-database-postgresql/10.10.0
2.472 [INFO] [graalvm reachability metadata repository for org.postgresql:postgresql:42.7.3]: Configuration directory not found. Trying latest version.
2.472 [INFO] [graalvm reachability metadata repository for org.postgresql:postgresql:42.7.3]: Configuration directory is org.postgresql/postgresql/42.3.4
2.476 [INFO] 
2.476 [INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ commandrunner ---
2.501 [INFO] Copying 1 resource from src/main/resources to target/classes
2.506 [INFO] Copying 2 resources from src/main/resources to target/classes
2.506 [INFO] 
2.506 [INFO] --- maven-compiler-plugin:3.13.0:compile (default-compile) @ commandrunner ---
2.540 [INFO] Recompiling the module because of changed source code.
2.543 [INFO] Compiling 24 source files with javac [debug parameters release 22] to target/classes
2.605 [INFO] ------------------------------------------------------------------------
2.605 [INFO] BUILD FAILURE
2.605 [INFO] ------------------------------------------------------------------------
2.606 [INFO] Total time:  2.191 s
2.606 [INFO] Finished at: 2024-09-08T14:05:23Z
2.606 [INFO] ------------------------------------------------------------------------
2.606 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile (default-compile) on project commandrunner: Fatal error compiling: error: release version 22 not supported -> [Help 1]
2.606 [ERROR] 
2.606 [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
2.606 [ERROR] Re-run Maven using the -X switch to enable full debug logging.
2.606 [ERROR] 
2.606 [ERROR] For more information about the errors and possible solutions, please read the following articles:
2.607 [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
------
Dockerfile:15
--------------------
  13 |     COPY pom.xml .
  14 |     
  15 | >>> RUN --mount=type=cache,target=/root/.m2 mvn clean package -Pnative
  16 |     
  17 |     # Second stage: Lightweight debian-slim image
--------------------
ERROR: failed to solve: process "/bin/sh -c mvn clean package -Pnative" did not complete successfully: exit code: 1

Summary

Always keep in mind the goal of your product/software and the time budget, the comparison with Graalvm and JVM is that with Spring Graalvm is incredibly time-consuming and may not work always, JVM is more than capable, and you probably don’t need anything better, especially since it’s evolving rapidly for the cloud and is easier to test and debug.

If small memory footprint is required, and you want to use Java, I suggest checking out Quarkus orMicronaut. Otherwise, be sure to check out Go as well (with it the above application in Docker with DB connection would probably be ~25MB).