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
This commit is contained in:
Adrien
2026-04-09 12:05:02 +02:00
parent aee6a9dfba
commit d8bcdce879
17 changed files with 1285 additions and 6 deletions
@@ -0,0 +1,47 @@
# Build Contract: Native Image
**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07
## Overview
This contract defines what the native build produces and how consumers (developers, CI, docker-compose)
interact with it. The REST API contract is **unchanged** — all existing endpoints remain identical.
## Build Inputs
| Input | Required | Description |
|-------|----------|-------------|
| GraalVM JDK 25 | Yes (native profile only) | `JAVA_HOME` must point to GraalVM 25 |
| `DOCKER_HOST` / Docker daemon | Optional | Required for `jib:dockerBuild`; not needed for `jib:build` |
| `jib.to.image` Maven property | Optional | Override target image name; defaults to `ai-teacher-backend` |
## Build Outputs
| Profile | Command | Output |
|---------|---------|--------|
| Default (JVM) | `mvn package` | `target/ai-teacher-backend-*.jar` |
| Native | `mvn -Pnative package` | `target/ai-teacher-backend` (native executable) |
| Native + Docker | `mvn -Pnative package jib:dockerBuild` | Local Docker image `ai-teacher-backend:latest` |
| Native + Registry | `mvn -Pnative package jib:build` | Remote Docker image (configured via properties) |
## Docker Image Contract
| Property | Value |
|----------|-------|
| Base image | `gcr.io/distroless/base-nossl-debian12` |
| Entrypoint | `/app/ai-teacher-backend` (native executable) |
| Exposed port | `8080` |
| Architecture | `linux/amd64` (matches build host) |
| Required env vars | Same as JVM mode (`SPRING_DATASOURCE_URL`, `OPENAI_API_KEY`, etc.) |
## REST API Contract
Unchanged — the native image exposes the same HTTP API as the JVM image:
- `GET /api/v1/books`
- `POST /api/v1/books` (multipart PDF upload)
- `DELETE /api/v1/books/{id}`
- `POST /api/v1/chat`
- `GET /api/v1/chat/history`
All endpoints use the same request/response schemas.
@@ -0,0 +1,33 @@
# Data Model: Native Image Deployment
**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07
## Summary
This feature introduces **no new data entities**. It is a build and packaging change only.
The existing data model (books, sections, figures, messages, pgvector embeddings) is unchanged.
All Flyway migrations run identically in native mode as in JVM mode.
## Impact on Existing Entities
| Entity | Change |
|--------|--------|
| Books | None |
| Sections | None |
| Figures | None |
| Messages | None |
| pgvector embeddings | None |
## Configuration Properties (new/changed)
The following new or updated configuration properties are added for native deployment:
| Property | Description | Default |
|----------|-------------|---------|
| `jib.to.image` | Target Docker image name/tag | (set at build time) |
| `jib.to.auth.username` | Docker registry username | (CI env var) |
| `jib.to.auth.password` | Docker registry password | (CI env var) |
All runtime configuration (database URL, OpenAI API key, S3 credentials) remains in
`application.properties` / environment variables — unchanged.
+213
View File
@@ -0,0 +1,213 @@
# Implementation Plan: Native Image Deployment
**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/005-native-image-deployment/spec.md`
## Summary
Add a `native` Maven profile to the backend that compiles the Spring Boot 4.0.5 application to a
GraalVM 25 native executable, then packages it into a minimal Docker image via Jib. The JVM build
(default profile + existing Dockerfile) remains unchanged. A companion `docker-compose.native.yml`
enables local full-stack testing with the native image. README.md is updated with build
instructions and an updated architecture diagram.
## Technical Context
**Language/Version**: Java 25 (backend), TypeScript / Node 20 (frontend)
**Primary Dependencies**: Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6,
`jib-maven-plugin` 3.4.5, `jib-native-image-extension-maven` 0.1.0
**Storage**: PostgreSQL 16 + pgvector (unchanged)
**Testing**: Spring Boot Test / JUnit 5 (unchanged); native integration test via smoke-test on
produced Docker image
**Target Platform**: Linux x86_64 container (Docker), GraalVM 25 CE or Oracle GraalVM 25
**Project Type**: Web application — backend API + frontend client (Option 2 structure)
**Performance Goals**: Backend container starts in < 1 s; idle RSS < 150 MB
**Constraints**: Native profile is opt-in; JVM mode unchanged; no new runtime dependencies;
cross-compilation out of scope
**Scale/Scope**: Single backend deployable unit (1 Docker image); frontend unchanged
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. KISS | ✅ Pass | Build-time change only; no new runtime layers or abstractions. |
| II. Easy to Change | ✅ Pass | `native` profile is additive; default JVM profile unchanged. Native hints isolated in one `NativeHintsConfig` class. |
| III. Web-First Architecture | ✅ Pass | REST API contract unchanged. |
| IV. Documentation as Architecture | ⚠️ Required | README.md MUST be updated with native build instructions and updated Mermaid diagram showing the build pipeline. |
| Technology Constraints | ✅ Pass | Still 1 backend + 1 frontend deployable unit; no new services. |
**Post-design re-check**: After Phase 1 — Principle IV gate requires README update in same PR.
## Project Structure
### Documentation (this feature)
```text
specs/005-native-image-deployment/
├── plan.md # This file
├── spec.md # Feature specification
├── research.md # Phase 0 — toolchain & compatibility decisions
├── data-model.md # Phase 1 — no new entities (confirmed)
├── quickstart.md # Phase 1 — developer build guide
├── contracts/
│ └── build-contract.md # Build inputs/outputs & unchanged REST API
└── tasks.md # Phase 2 output (/speckit.tasks — NOT created here)
```
### Source Code (repository root)
```text
backend/
├── pom.xml # +native profile, +Jib plugin, +NativeHintsConfig
├── src/
│ └── main/
│ └── java/com/aiteacher/
│ └── config/
│ └── NativeHintsConfig.java # NEW — RuntimeHints for PDFBox, AWS SDK gaps
└── src/
└── main/
└── resources/
└── META-INF/
└── native-image/ # Optional: manual JSON hints if AOT gaps remain
docker-compose.yml # UNCHANGED (JVM mode)
docker-compose.native.yml # NEW — native image + postgres full stack
README.md # UPDATED — build instructions + Mermaid diagram
```
**Structure Decision**: Follows Option 2 (web application). Only backend is modified; frontend
is untouched. New files are minimal: one Java config class, one docker-compose file.
## Complexity Tracking
> No constitution violations. No entry required.
---
## Implementation Phases
### Phase 1: Maven `native` Profile + Native Maven Plugin
**Goal**: `mvn -Pnative package` produces a native executable `target/ai-teacher-backend`.
**Tasks**:
1. Add `<profile id="native">` to `backend/pom.xml`:
- Wire `spring-boot-maven-plugin` `process-aot` goal to `prepare-package` phase.
- Add `native-maven-plugin` 0.10.6 with `add-reachability-metadata` and `compile-no-fork`
executions.
- Set `<imageName>ai-teacher-backend</imageName>` and add essential build args:
`-H:+ReportExceptionStackTraces`, `--initialize-at-build-time=org.slf4j`.
2. Create `backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java`:
- Implements `RuntimeHintsRegistrar`.
- Registers PDFBox classpath resources: `org/apache/pdfbox/resources/**`.
- Registers PDFBox reflection entries for font/encoding classes.
- Registers AWS SDK `SdkPojo` subtypes used by S3 (discovered iteratively).
- Annotate main application class or a `@Configuration` class with
`@ImportRuntimeHints(NativeHintsConfig.class)`.
3. Validate: run `mvn -Pnative package -DskipTests` and confirm executable is produced.
**Acceptance**: `target/ai-teacher-backend` exists, `./target/ai-teacher-backend --help`
exits cleanly, basic startup reaches DB connection stage.
---
### Phase 2: Jib Plugin — Package Native Image into Docker
**Goal**: `mvn -Pnative package jib:dockerBuild` produces a local Docker image.
**Tasks**:
1. Add `jib-maven-plugin` 3.4.5 to the `<build><plugins>` section (outside native profile,
but configured to package the native executable when the profile is active):
- Add `jib-native-image-extension-maven` 0.1.0 as a plugin dependency.
- Configure `<from><image>gcr.io/distroless/base-nossl-debian12</image></from>`.
- Set `<to><image>ai-teacher-backend</image></to>` (overridable via `-Djib.to.image`).
- Add `<pluginExtensions>` pointing to `JibNativeImageExtension`.
- Set `<container><ports><port>8080</port></ports></container>`.
2. Verify Jib picks up the native executable from `target/ai-teacher-backend` and sets it
as entrypoint.
3. Run `mvn -Pnative package jib:dockerBuild` and confirm:
- Image exists in local Docker: `docker images | grep ai-teacher-backend`.
- Container starts: `docker run --rm -e SPRING_DATASOURCE_URL=... ai-teacher-backend`.
**Acceptance**: Image starts, logs show "Started AiTeacherApplication in < 1.0 seconds",
and `GET /api/v1/books` returns 200 (with DB connected).
---
### Phase 3: Integration Smoke Test
**Goal**: All existing features work in native mode.
**Tasks**:
1. Start PostgreSQL locally (or via existing docker-compose.yml).
2. Start the native container with full environment variables.
3. Verify:
- Flyway migrations run successfully at startup.
- `POST /api/v1/books` (PDF upload + embedding) succeeds.
- `POST /api/v1/chat` (RAG chat) returns a non-empty answer.
- Spring Security HTTP Basic auth is enforced.
4. Fix any `MissingResourceException` or `ClassNotFoundException` by adding entries to
`NativeHintsConfig` and rebuilding.
**Acceptance**: All 5 REST API endpoints respond correctly with native image (FR-005).
---
### Phase 4: Docker Compose for Native Stack
**Goal**: `docker compose -f docker-compose.native.yml up` starts the full stack.
**Tasks**:
1. Create `docker-compose.native.yml` at repo root:
- `postgres` service: same as `docker-compose.yml` (copy).
- `backend` service: uses `ai-teacher-backend:latest` (the Jib-built native image).
- Wires all required env vars via `.env` file reference or inline secrets.
- Adds `depends_on` with health check for postgres.
2. Document the `.env` file format in `quickstart.md`.
**Acceptance**: `docker compose -f docker-compose.native.yml up` → all services healthy →
app responds at `http://localhost:8080/api/v1/books`.
---
### Phase 5: README Update (Constitution IV Gate)
**Goal**: README.md reflects the new build pipeline and updated deployment diagram.
**Tasks**:
1. Add "Native Image Build" section to README.md with:
- Prerequisite: GraalVM 25 install instructions (sdkman command).
- Build command: `mvn -Pnative package jib:dockerBuild`.
- Run command: `docker compose -f docker-compose.native.yml up`.
2. Update the Mermaid architecture diagram in README.md to show:
- Build pipeline: Maven native profile → GraalVM native-image → Jib → Docker image.
- Keep the runtime diagram unchanged (same components, just a lighter container).
**Acceptance**: README.md Mermaid diagram renders correctly on GitHub; build section
contains all commands from `quickstart.md`.
---
## Risk Log
| Risk | Likelihood | Mitigation |
|------|------------|------------|
| Spring AI 2.0.0-M4 missing native hints for some AI response types | Medium | Iterative hint discovery during Phase 3 smoke test |
| PDFBox font loading fails at runtime in native | Medium | Register full `org/apache/pdfbox/resources/**` in `NativeHintsConfig` |
| AWS SDK S3 reflection errors | Medium | Switch to `url-connection-client` (simpler); lazy-init S3Client |
| GraalVM 25 availability in CI | Low | Documented prerequisite; CI pipeline config out of scope (can be added as follow-up) |
| Native build time > 5 min | Low | Acceptable for a first build; not a blocker for POC |
@@ -0,0 +1,81 @@
# Quickstart: Native Image Build & Deploy
**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07
## Prerequisites
- GraalVM JDK 25 CE or Oracle GraalVM 25 installed and set as `JAVA_HOME`
- Docker daemon running (for `jib:dockerBuild`)
- Maven 3.9+
```bash
# Install GraalVM 25 CE via sdkman
sdk install java 25-graalce
sdk use java 25-graalce
# Verify
java -version # should show GraalVM 25
native-image --version # should show GraalVM 25
```
## Build Native Docker Image (local)
```bash
cd backend
# Build native executable AND package into local Docker image
mvn -Pnative package jib:dockerBuild
# The image is now available locally
docker images | grep ai-teacher-backend
```
## Run the Full Stack (native)
```bash
# From repo root — starts PostgreSQL + native backend
docker compose -f docker-compose.native.yml up
```
Access the app at `http://localhost:8080`.
## Build and Push to Registry (CI)
```bash
mvn -Pnative package jib:build \
-Djib.to.image=ghcr.io/your-org/ai-teacher-backend:native-latest \
-Djib.to.auth.username=$REGISTRY_USER \
-Djib.to.auth.password=$REGISTRY_TOKEN
```
## JVM Build (unchanged)
```bash
# Default profile — no GraalVM required
cd backend
mvn package -DskipTests
java -jar target/ai-teacher-backend-*.jar
```
## Environment Variables (both JVM and native)
| Variable | Description |
|----------|-------------|
| `SPRING_DATASOURCE_URL` | PostgreSQL JDBC URL |
| `SPRING_DATASOURCE_USERNAME` | DB user |
| `SPRING_DATASOURCE_PASSWORD` | DB password |
| `OPENAI_API_KEY` | OpenAI API key |
| `AWS_ACCESS_KEY_ID` | S3 access key (if S3 storage enabled) |
| `AWS_SECRET_ACCESS_KEY` | S3 secret key |
| `AWS_REGION` | S3 region |
## Troubleshooting
**Build fails with "ImageGenerationFailed"**: Ensure `native-image` is on `PATH` and
`JAVA_HOME` points to GraalVM 25, not a regular JDK.
**Missing resource at runtime**: Add the resource pattern to `NativeHintsConfig` and rebuild.
**ClassNotFoundException at runtime**: Register the class in `NativeHintsConfig` with
`MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS` and rebuild.
@@ -0,0 +1,262 @@
# 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**:
```xml
<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**:
```java
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
```bash
# 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
```xml
<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>
```
+138
View File
@@ -0,0 +1,138 @@
# Feature Specification: Native Image Deployment
**Feature Branch**: `005-native-image-deployment`
**Created**: 2026-04-07
**Status**: Draft
**Input**: Prepare the application for deployment using GraalVM native image for the backend, built into a Docker image via Jib.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Build Native Docker Image (Priority: P1)
A developer or CI pipeline runs a single Maven command to produce a Docker image containing the
native-compiled backend binary. The image starts in under one second and uses significantly less
memory than the JVM fat-jar image.
**Why this priority**: This is the core deliverable — without a working native Docker image the
entire feature has no value.
**Independent Test**: Run `mvn -Pnative jib:dockerBuild` (or equivalent), start the resulting
container, hit `GET /api/v1/books`, and receive a valid response. Startup log must show the server
is ready in under 1 second.
**Acceptance Scenarios**:
1. **Given** a clean checkout with GraalVM installed, **When** the developer runs the native build
command, **Then** a Docker image is produced without manual steps.
2. **Given** the produced Docker image, **When** the container is started with the required
environment variables, **Then** the backend starts, connects to PostgreSQL, and serves HTTP
requests correctly.
3. **Given** the produced Docker image, **When** a RAG chat request is made, **Then** it responds
correctly (OpenAI call, pgvector retrieval, and Flyway migrations all work in native mode).
---
### User Story 2 - JVM Mode Preserved (Priority: P2)
A developer who has not installed GraalVM can still build and run the backend the same way as
today (fat-jar / existing Dockerfile), without any change to their workflow.
**Why this priority**: Preserving the developer experience for non-native builds ensures the team
can keep iterating without a GraalVM prerequisite.
**Independent Test**: Run `mvn package -DskipTests` and start the resulting jar with `java -jar`
the application starts and serves requests identically to before this feature.
**Acceptance Scenarios**:
1. **Given** a standard JDK (no GraalVM), **When** `mvn package` is run, **Then** a fat-jar is
produced and runs normally.
2. **Given** the existing Dockerfile, **When** `docker build` is run, **Then** a JVM-mode image
is produced and works exactly as before.
---
### User Story 3 - Docker Compose Full Stack (Priority: P3)
A developer can bring up the complete stack (PostgreSQL + native backend) with a single
`docker compose up` command for local integration testing.
**Why this priority**: Makes it easy to validate the native image against the real database before
pushing to any environment.
**Independent Test**: Run `docker compose up` (with native image tag), wait for healthy status,
make a RAG request — all services communicate correctly.
**Acceptance Scenarios**:
1. **Given** the docker-compose.yml, **When** `docker compose up` is run with the native backend
image, **Then** all services start, health checks pass, and the application is reachable.
---
### Edge Cases
- What happens when a reflection-heavy library (PDFBox, AWS SDK) is called at runtime without
proper native hints? → Must be caught at build time via native-image test or detected early
via integration smoke test.
- What if GraalVM is not installed in CI? → Build must fail with a clear error message, not
silently produce a JVM image.
- What if an environment variable required at runtime is missing? → Container must fail fast with
a meaningful error (not a NullPointerException from missing reflection).
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The project MUST provide a `native` Maven profile that compiles the backend to a
standalone native executable using GraalVM.
- **FR-002**: The native profile MUST include Spring Boot AOT processing to generate the
reflection, serialization, and proxy hints required at compile time.
- **FR-003**: The Jib Maven plugin MUST be configured to package the native executable into a
Docker image without requiring Docker daemon access during the build.
- **FR-004**: The resulting Docker image MUST start the backend in under 1 second on modern
hardware (2+ CPU cores, 2 GB RAM).
- **FR-005**: All existing REST endpoints MUST function correctly in native mode (book upload,
RAG chat, Flyway migrations, pgvector retrieval, Spring Security).
- **FR-006**: The JVM fat-jar build (default profile) MUST continue to work unchanged.
- **FR-007**: The README.md MUST be updated with native build instructions and updated
architecture/deployment diagram.
- **FR-008**: The `docker-compose.yml` MUST be updated (or a companion file added) to support
running the native Docker image alongside PostgreSQL.
### Key Entities
- **Native Profile**: Maven build profile (`native`) that activates AOT processing and
GraalVM native compilation.
- **Docker Image**: OCI image produced by Jib, containing the native executable and a minimal
base OS layer.
- **AOT Hints**: Compile-time metadata (reflection, serialization, proxy) that replace runtime
reflection for third-party libraries.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: The native Docker image starts and serves its first HTTP request in under 1 second
on a machine with 2+ CPU cores and 2 GB RAM.
- **SC-002**: The native container's idle memory footprint is at least 40% lower than the
equivalent JVM container running the same application.
- **SC-003**: All existing API endpoints return correct responses in native mode (0 regressions).
- **SC-004**: A developer with GraalVM installed can build the native Docker image with a single
command documented in README.md.
- **SC-005**: The default JVM build and Dockerfile continue to work without any changes to the
developer workflow.
## Assumptions
- GraalVM 25 (CE or Oracle) will be available in CI and on developer machines that want to build
native images.
- The native build is performed on Linux x86_64; cross-compilation for other architectures is out
of scope for this feature.
- Spring Boot 4.0.5 and Spring AI 2.0.0-M4 have sufficient native image support for the features
used (OpenAI chat/embedding, pgvector, Flyway, Spring Security, Spring Data JPA).
- The Docker registry target and image name/tag will be configured via Maven properties or
environment variables, not hardcoded.
- Frontend deployment is out of scope for this feature; only the backend native image is addressed.
- The existing Dockerfile is kept as a fallback JVM image build; the Jib-built native image is
the new primary artifact.
+189
View File
@@ -0,0 +1,189 @@
# Tasks: Native Image Deployment
**Input**: Design documents from `/specs/005-native-image-deployment/`
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/ ✅
**Tests**: No test tasks — this feature has no unit/integration test requirements in the spec.
Smoke testing is part of US1 implementation (T010, T011).
**Organization**: Tasks grouped by user story for independent implementation and verification.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no incomplete-task dependencies)
- **[Story]**: Which user story this task belongs to (US1, US2, US3)
- Paths use `backend/` and repo root (web app structure per constitution)
---
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Verify prerequisites and baseline before touching any build files.
- [ ] T001 Verify GraalVM 25 CE or Oracle GraalVM 25 is installed: run `native-image --version` and confirm output shows GraalVM 25; document install command (`sdk install java 25-graalce`) if missing
- [ ] T002 [P] Verify baseline JVM build passes: run `mvn package -DskipTests` inside `backend/` and confirm `target/ai-teacher-backend-*.jar` is produced
**Checkpoint**: GraalVM available, existing JVM build green — safe to modify build files
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Add AOT processing and native compilation support to the Maven build. These changes
are prerequisites for all user stories.
**⚠️ CRITICAL**: US1 cannot start until T007 confirms the native executable is produced.
- [ ] T003 Add `<profile id="native">` skeleton to `backend/pom.xml` and wire `spring-boot-maven-plugin` `process-aot` goal to the `prepare-package` phase inside that profile
- [ ] T004 Add `native-maven-plugin` 0.10.6 to the `native` profile in `backend/pom.xml` with executions `add-reachability-metadata` and `compile-no-fork` (phase `package`), image name `ai-teacher-backend`, and build args `--initialize-at-build-time=org.slf4j` and `-H:+ReportExceptionStackTraces`
- [ ] T005 [P] Create `backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java` implementing `RuntimeHintsRegistrar`: register resource pattern `org/apache/pdfbox/resources/**` and reflection for `org.apache.pdfbox.pdmodel.font.encoding.GlyphList` with `MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS`
- [ ] T006 Add `@ImportRuntimeHints(NativeHintsConfig.class)` annotation to the main `@SpringBootApplication` class in `backend/src/main/java/com/aiteacher/`
- [ ] T007 Run `mvn -Pnative package -DskipTests` inside `backend/` and confirm `target/ai-teacher-backend` native executable is produced; fix any AOT compilation errors before proceeding
**Checkpoint**: `target/ai-teacher-backend` exists and exits cleanly — US1 implementation can begin
---
## Phase 3: User Story 1 — Build Native Docker Image (Priority: P1) 🎯 MVP
**Goal**: One command (`mvn -Pnative package jib:dockerBuild`) produces a local Docker image
containing the native backend. Container starts in < 1 s and all REST endpoints work.
**Independent Test**: Start native container with DB env vars → `GET /api/v1/books` returns 200 →
startup log shows ready in under 1 second.
### Implementation for User Story 1
- [ ] T008 [US1] Add `jib-maven-plugin` 3.4.5 to `backend/pom.xml` `<build><plugins>` section (default build, not inside `native` profile): configure `<from><image>gcr.io/distroless/base-nossl-debian12</image></from>`, `<to><image>ai-teacher-backend</image></to>`, exposed port `8080`, and `<pluginExtensions>` referencing `com.google.cloud.tools.jib.maven.extension.nativeimage.JibNativeImageExtension`; add `jib-native-image-extension-maven` 0.1.0 as plugin `<dependency>`
- [ ] T009 [US1] Run `mvn -Pnative package jib:dockerBuild -DskipTests` inside `backend/` and confirm `docker images | grep ai-teacher-backend` shows the produced image
- [ ] T010 [US1] Start the native container with required env vars (`SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD`, `OPENAI_API_KEY`) pointing to a local PostgreSQL; verify startup log shows "Started … in < 1.0 seconds" and `GET /api/v1/books` returns HTTP 200
- [ ] T011 [US1] Run full smoke test against the running native container: `POST /api/v1/books` (PDF upload), `POST /api/v1/chat` (RAG query), `GET /api/v1/chat/history`, and HTTP Basic auth enforcement; for each `MissingResourceException` or `ClassNotFoundException` found, add the corresponding entry to `backend/src/main/java/com/aiteacher/config/NativeHintsConfig.java` and rebuild (T009 → T010 loop) until all pass
**Checkpoint**: Native Docker image starts < 1 s, all 5 endpoints work — US1 complete ✅
---
## Phase 4: User Story 2 — JVM Mode Preserved (Priority: P2)
**Goal**: Default Maven build and existing Dockerfile continue to work with no changes required
from developers who do not have GraalVM installed.
**Independent Test**: `mvn package -DskipTests` produces a fat-jar; `docker build` using
`backend/Dockerfile` produces a working JVM container.
### Implementation for User Story 2
- [ ] T012 [P] [US2] Verify `mvn package -DskipTests` (no `-Pnative`) inside `backend/` still produces `target/ai-teacher-backend-*.jar`; confirm the jar starts correctly with `java -jar`
- [ ] T013 [P] [US2] Verify `docker build -t ai-teacher-backend-jvm backend/` using the existing `backend/Dockerfile` succeeds and the resulting JVM container starts and serves `GET /api/v1/books` correctly
**Checkpoint**: JVM path fully unchanged — US2 complete ✅
---
## Phase 5: User Story 3 — Docker Compose Full Stack (Priority: P3)
**Goal**: `docker compose -f docker-compose.native.yml up` starts PostgreSQL + native backend
together so developers can run the complete native stack locally with a single command.
**Independent Test**: `docker compose -f docker-compose.native.yml up` → all services healthy →
`GET http://localhost:8080/api/v1/books` returns 200.
### Implementation for User Story 3
- [ ] T014 [US3] Create `docker-compose.native.yml` at repo root: include `postgres` service (same image and config as `docker-compose.yml`), add `backend` service using image `ai-teacher-backend:latest` with all required env vars sourced from `.env`, `depends_on` postgres with health-check condition, and port mapping `8080:8080`
- [ ] T015 [P] [US3] Create `.env.example` at repo root listing all env vars needed by the native stack (`SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD`, `OPENAI_API_KEY`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`) with placeholder values and inline comments
**Checkpoint**: Full native stack starts via docker compose — US3 complete ✅
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Constitution IV gate (README must be updated in the same PR as architectural change)
and final documentation alignment.
- [ ] T016 Update `README.md` at repo root: add a "Native Image Build" section with GraalVM install command (`sdk install java 25-graalce`), build command (`mvn -Pnative package jib:dockerBuild`), and run command (`docker compose -f docker-compose.native.yml up`); update the Mermaid architecture diagram to show the native build pipeline (Maven native profile → GraalVM native-image → Jib → Docker image)
- [ ] T017 [P] Update `specs/005-native-image-deployment/quickstart.md` with any corrections or additions discovered during implementation (env var names, exact commands, troubleshooting entries)
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies — start immediately
- **Foundational (Phase 2)**: Requires Phase 1 completion — **BLOCKS all user stories**
- **US1 (Phase 3)**: Requires Phase 2 (T007 must pass) — primary deliverable
- **US2 (Phase 4)**: Requires Phase 2 (T003T006 must not break JVM path) — can run in parallel with US1 after Phase 2
- **US3 (Phase 5)**: Requires US1 completion (T009 must produce the Docker image)
- **Polish (Phase 6)**: Requires US1, US2, US3 all complete
### User Story Dependencies
- **US1 (P1)**: Unblocked after Foundational — critical path
- **US2 (P2)**: Unblocked after Foundational — can run in parallel with US1 (different verification commands, no file conflicts)
- **US3 (P3)**: Depends on US1 (needs the Docker image to exist for docker-compose)
### Within US1
T008 (Jib config) → T009 (build image) → T010 (verify startup) → T011 (smoke test + hint fixes, may loop back to T009)
### Parallel Opportunities
- T001 and T002 (Phase 1): both can run in parallel
- T003/T004 (pom.xml edits) are sequential (same file); T005 (new Java file) can run in parallel with T003/T004
- T012 and T013 (US2 verification): parallel, different commands
- T014 and T015 (US3 docker-compose + .env.example): parallel, different files
- T016 and T017 (Polish): parallel, different files
- US1 (Phase 3) and US2 (Phase 4) can run in parallel after Foundational completes
---
## Parallel Example: Foundational Phase
```bash
# In parallel — different files, no dependencies between them:
Task T005: "Create NativeHintsConfig.java in backend/src/main/java/com/aiteacher/config/"
# Sequential — same file (pom.xml):
Task T003: "Add native profile skeleton with spring-boot-maven-plugin process-aot"
Task T004: "Add native-maven-plugin 0.10.6 to native profile" # after T003
```
## Parallel Example: After Foundational Completes
```bash
# US1 and US2 can start simultaneously:
Developer A → Phase 3 (US1): T008 → T009 → T010 → T011
Developer B → Phase 4 (US2): T012, T013 (parallel)
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup (T001, T002)
2. Complete Phase 2: Foundational (T003T007) — **CRITICAL, do not skip T007**
3. Complete Phase 3: US1 (T008T011)
4. **STOP and VALIDATE**: Native image starts < 1 s, all endpoints work
5. This alone satisfies FR-001 through FR-006 and SC-001 through SC-003
### Incremental Delivery
1. Phase 1 + Phase 2 → build toolchain ready
2. Phase 3 (US1) → native Docker image working (**MVP**)
3. Phase 4 (US2) → JVM fallback confirmed
4. Phase 5 (US3) → full native stack via docker compose
5. Phase 6 (Polish) → README updated (constitution IV gate — **required before merge**)
---
## Notes
- [P] tasks operate on different files or independent commands — safe to run concurrently
- US2 (T012, T013) can be verified at any time after Phase 2; they are quick sanity checks
- The hint-fix loop in T011 is the most likely source of iteration — budget extra time
- Constitution IV gate (README update, T016) is **mandatory** — PR cannot merge without it
- `.env.example` (T015) should be committed; actual `.env` must be in `.gitignore`