Files
Adrien d8bcdce879 Squashed commit of the following:
commit 0d624137c2557c6eeb87020749e4977b821c2b5c
Author: Adrien <adrien.cesaro@proton.me>
Date:   Thu Apr 9 11:55:22 2026 +0200

    backend native image setup
2026-04-09 12:05:02 +02:00

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.version in 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:

  1. mvn -Pnative package runs AOT → generates source under target/spring-aot/
  2. native:compile compiles 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-image CLI: 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) vs jib:dockerBuild (local daemon): default to jib:dockerBuild for dev; jib:build for 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 --static or --libc=musl native-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 uses Class.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-client or url-connection-client (simpler, fewer reflection needs than netty)
  • The SDK's SdkPojo interfaces 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>