Compiling Spring Boot into a Native Executable and Containerizing It
One of Spring Boot’s latest features is the ability to perform native compilation. This generates an application with a smaller binary size and a reduced memory footprint. This is particularly beneficial for deployments in containerized environments such as the OpenShift Container Platform.
Let’s start by installing GraalVM in our environment. I am using Fedora, so the installation process might differ for other operating systems.
$ wget https://github.com/graalvm/mandrel/releases/download/mandrel-24.2.2.0-Final/mandrel-java24-linux-amd64-24.2.2.0-Final.tar.gz $ sudo mkdir -p /opt/mandrel $ sudo tar -xzf mandrel-java24-linux-amd64-24.2.2.0-Final.tar.gz -C /opt/mandrel/ $ sudo ln -s /opt/mandrel/mandrel-java24-24.2.2.0-Final /opt/mandrel/current $ export GRAALVM_HOME=/opt/mandrel/current
Next, we create a Maven pom.xml file,
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.edw</groupId>
<artifactId>spring-boot-postgresql</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.9</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-bom</artifactId>
<version>2.15.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.7</version>
</dependency>
<!-- OpenTelemetry -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>JAR</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
We also need the following Java files,
package com.edw;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
package com.edw.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Entity
@Table(name = "t_customer")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Customer implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
@Column(name = "customer_id")
private Long customerId;
@Column(name = "customer_name")
private String customerName;
}
package com.edw.repository;
import com.edw.model.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {
}
package com.edw.service;
import com.edw.model.Customer;
import com.edw.repository.CustomerRepository;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Transactional
public class CustomerService {
private final CustomerRepository customerRepository;
public CustomerService(@Autowired CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public List<Customer> findAll(){
return customerRepository.findAll(Sort.by(Sort.Direction.ASC, "customerId"));
}
}
package com.edw.controller;
import com.edw.service.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CustomerController {
private final CustomerService customerService;
public CustomerController(@Autowired CustomerService customerService) {
this.customerService = customerService;
}
@GetMapping("/")
public ResponseEntity findAll () {
return ResponseEntity.ok(customerService.findAll());
}
}
Now, build the native application using the following command,
$ mvn clean package -Pnative native:compile ...... Finished generating 'spring-boot-postgresql' in 1m 30s. [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 01:41 min [INFO] Finished at: 2026-01-13T11:44:54+07:00 [INFO] ------------------------------------------------------------------------
If we look closely at the pom.xml provided above, we might notice something interesting. There is no profile named native explicitly defined in the file. Yet, the mvn command works perfectly.
The reason is that “spring-boot-starter-parent” comes with a pre-configured native profile out of the box. When we run Maven with the -Pnative flag, it activates this inherited profile, which automatically sets up the necessary configuration and AOT processing for GraalVM.
Create a file named Dockerfile.native-ubi9,
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7
WORKDIR /work
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY target/spring-boot-postgresql /work/application
COPY target/*.so /work/
ENV LD_LIBRARY_PATH=/work
EXPOSE 8080
USER 1001
CMD ["./application"]
Then, build the container image,
$ podman build -t default-route-openshift-image-registry.apps-crc.testing/api/spring-boot-postgresql:native-ubi9 -f Dockerfile.native-ubi9 .
After deploying to OpenShift, we can see a significant resource difference between a traditional Spring Boot app and a native one
$ oc adm top pod -n api spring-boot-postgresql-jvm-64b5ff9967-5pp76 NAME CPU(cores) MEMORY(bytes) spring-boot-postgresql-jvm-64b5ff9967-5pp76 1m 237Mi $ oc adm top pod -n api spring-boot-postgresql-native-85db64b4cd-2cpt9 NAME CPU(cores) MEMORY(bytes) spring-boot-postgresql-native-85db64b4cd-2cpt9 1m 81Mi
The code for this project can be found in this repository,
https://github.com/edwin/spring-boot-postgresql









