commit 0d624137c2557c6eeb87020749e4977b821c2b5c Author: Adrien <adrien.cesaro@proton.me> Date: Thu Apr 9 11:55:22 2026 +0200 backend native image setup
9.0 KiB
Research: Native Image Deployment
Branch: 005-native-image-deployment | Date: 2026-04-07
Decision Log
1. GraalVM Version and Java 25 Support
Decision: Use GraalVM JDK 25 (Oracle GraalVM or GraalVM CE 25).
Rationale: Oracle GraalVM 25 ships with Java 25 support and includes the
native-image tool. GraalVM CE 25 is available via sdkman (sdk install java 25-graalce).
Both support the native-maven-plugin 0.10.x workflow.
Alternatives considered:
- GraalVM 21 with Java 21: would require downgrading
java.versionin pom.xml — rejected because pom.xml already targets Java 25. - Mandrel (Red Hat distribution): Red Hat Mandrel 25 may lag behind Oracle GraalVM 25 release; acceptable fallback but Oracle GraalVM 25 is preferred.
2. Native Image Build Toolchain
Decision: Use native-maven-plugin (0.10.x) inside a Maven native profile, combined
with Spring Boot's AOT processing via spring-boot-maven-plugin process-aot execution.
Rationale: This is the canonical Spring Boot 4.x native image approach:
mvn -Pnative packageruns AOT → generates source undertarget/spring-aot/native:compilecompiles the AOT-generated sources + app into a native executable.
The native-maven-plugin coordinates with GraalVM native-image CLI automatically.
Alternatives considered:
- Paketo Buildpacks (
spring-boot:build-image -Pnative): does not use Jib; rejected because the user explicitly wants Jib. - Manual
native-imageCLI: fragile, no Maven lifecycle integration — rejected.
3. Jib + Native Image Integration
Decision: Use jib-maven-plugin (3.4.x) with the jib-native-image-extension-maven
extension (0.1.0).
Rationale: The Jib native image extension packages a pre-built native executable into a Docker image without requiring a Docker daemon. The workflow is:
mvn -Pnative native:compile # step 1: build native executable
mvn -Pnative jib:dockerBuild # step 2: package into Docker image
Or combined (with process-aot wired in native profile):
mvn -Pnative package jib:dockerBuild
Jib uses the extension to locate the native executable under target/ and set it as the
container entrypoint.
Key configuration:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.5</version>
<configuration>
<from>
<!-- distroless for security; or cgr.dev/chainguard/static for even smaller -->
<image>gcr.io/distroless/base-nossl-debian12</image>
</from>
<pluginExtensions>
<pluginExtension>
<implementation>
com.google.cloud.tools.jib.maven.extension.nativeimage.JibNativeImageExtension
</implementation>
</pluginExtension>
</pluginExtensions>
</configuration>
<dependencies>
<dependency>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-native-image-extension-maven</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>
</plugin>
Alternatives considered:
- Custom Dockerfile for native image: more control but manual, diverges from Jib workflow — rejected in favor of Jib as specified.
jib:build(push to registry) vsjib:dockerBuild(local daemon): default tojib:dockerBuildfor dev;jib:buildfor CI with registry configured.
4. Docker Base Image for Native Executable
Decision: gcr.io/distroless/base-nossl-debian12 (Debian 12 glibc variant).
Rationale: GraalVM native images compiled on Linux link against glibc by default. Distroless provides glibc without a shell, minimizing attack surface.
Important: the native executable compiled on the host must target the same glibc ABI as the base image. Since the build is on Linux x86_64 this matches Debian 12's glibc 2.36.
Alternatives considered:
alpine(musl libc): requires--staticor--libc=muslnative-image flag; more complex — rejected for simplicity (KISS).gcr.io/distroless/java-base: includes JVM libs we don't need — rejected (pure native).scratch: too minimal; requires fully static binary with all libs included — rejected.
5. Spring AI 2.0.0-M4 Native Image Compatibility
Decision: Rely on Spring AI's built-in AOT hints; add manual RuntimeHints only for gaps
found during build.
Rationale: Spring AI 2.0.0-M4 includes @NativeHint registrations for the OpenAI
integration (HTTP client, response DTOs) and pgvector store. Spring Boot's AOT processor
picks these up automatically. M4 milestone has known native image improvements.
Known gaps (require manual RuntimeHints):
spring-ai-pdf-document-reader: PDFBox font/resource loading usesClass.forName()and classpath resource scanning. Need to register font resources and PDFBox parser classes.spring-ai-advisors-vector-store: generally AOT-compatible via Spring AI hints.
Approach: Add a NativeHintsConfig class annotated with @ImportRuntimeHints that
registers PDFBox fonts and any other gaps found during a -Ob (quick) native build.
6. PDFBox Reflection Hints
Decision: Register PDFBox font resources and parser classes via a RuntimeHintsRegistrar.
Rationale: PDFBox 3.x loads fonts from classpath resources (/org/apache/pdfbox/resources/)
and uses reflection for CMap/encoding classes. Without hints the native build fails at runtime
with ClassNotFoundException or missing resource errors.
Minimum hints needed:
hints.resources().registerPattern("org/apache/pdfbox/resources/**");
hints.reflection().registerType(org.apache.pdfbox.pdmodel.font.encoding.GlyphList.class,
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
// Additional classes as discovered during smoke tests
7. AWS SDK v2 Native Image Support
Decision: Add software.amazon.awssdk:aws-crt-client is NOT needed; instead add
software.amazon.awssdk:netty-nio-client or use URL connection client.
Add AWS SDK v2 native image support via aws-sdk-java-v2-native-bridge if available,
otherwise register reflection manually.
Rationale: AWS SDK v2 2.30.x provides native image support via:
software.amazon.awssdk:apache-clientorurl-connection-client(simpler, fewer reflection needs than netty)- The SDK's
SdkPojointerfaces generate reflection entries; Spring Boot AOT may not cover all.
Minimum approach: Switch S3 HTTP client from default (netty) to url-connection-client
for simplicity in native mode, register SdkPojo subtypes via RuntimeHints.
Alternative: If S3 is used infrequently and only for figure uploads, mark S3Client as a conditional bean initialized lazily — reflection issues then surface only on first use (not startup), making native hints easier to discover iteratively.
8. Flyway in Native Mode
Decision: No special configuration needed; Flyway 10.x has native image support.
Rationale: Spring Boot 4.x AOT includes Flyway native hints. PostgreSQL JDBC driver and Flyway 10 have been tested with GraalVM native. No manual hints required.
9. Spring Security in Native Mode
Decision: No special configuration needed.
Rationale: Spring Security 7.x (bundled with Spring Boot 4.x) includes AOT-compatible configuration. HTTP Basic auth does not require dynamic proxies at runtime.
10. Build Command Summary
# Install GraalVM 25
sdk install java 25-graalce
# Build native executable + Docker image (local daemon)
cd backend
mvn -Pnative package jib:dockerBuild
# Or push to registry (CI)
mvn -Pnative package jib:build \
-Djib.to.image=ghcr.io/your-org/ai-teacher-backend:native-latest \
-Djib.to.auth.username=$CI_USER \
-Djib.to.auth.password=$CI_TOKEN
11. Maven Profile Structure
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<!-- AOT processing -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>process-aot</id>
<goals><goal>process-aot</goal></goals>
</execution>
</executions>
</plugin>
<!-- Native compile -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.6</version>
<executions>
<execution>
<id>add-reachability-metadata</id>
<goals><goal>add-reachability-metadata</goal></goals>
</execution>
<execution>
<id>compile</id>
<goals><goal>compile-no-fork</goal></goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<imageName>ai-teacher-backend</imageName>
<buildArgs>
<buildArg>--initialize-at-build-time=org.slf4j</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>