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:
@@ -0,0 +1,13 @@
|
|||||||
|
# Copy this file to .env and fill in your values before running docker-compose.native.yml
|
||||||
|
# .env is gitignored — never commit real credentials
|
||||||
|
|
||||||
|
# OpenAI
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
|
|
||||||
|
# AWS S3 (figure storage — leave blank if using local filesystem)
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_REGION=eu-west-1
|
||||||
|
|
||||||
|
# S3 bucket name (if S3 storage enabled)
|
||||||
|
APP_STORAGE_S3_BUCKET=ai-teacher-figures
|
||||||
@@ -14,6 +14,8 @@ Auto-generated from all feature plans. Last updated: 2026-04-07
|
|||||||
- PostgreSQL (sections, figures, messages — unchanged). No new tables needed. (004-rag-retrieval-quality)
|
- PostgreSQL (sections, figures, messages — unchanged). No new tables needed. (004-rag-retrieval-quality)
|
||||||
- Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), Vue 3.4, Pinia 2.1, Axios 1.7 (004-rag-retrieval-quality)
|
- Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), Vue 3.4, Pinia 2.1, Axios 1.7 (004-rag-retrieval-quality)
|
||||||
- PostgreSQL (JPA + Flyway), pgvector (`VectorStore`) (004-rag-retrieval-quality)
|
- PostgreSQL (JPA + Flyway), pgvector (`VectorStore`) (004-rag-retrieval-quality)
|
||||||
|
- Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6, (005-native-image-deployment)
|
||||||
|
- PostgreSQL 16 + pgvector (unchanged) (005-native-image-deployment)
|
||||||
|
|
||||||
- Java 21 (backend), TypeScript / Node 20 (frontend) (001-neuro-rag-learning)
|
- Java 21 (backend), TypeScript / Node 20 (frontend) (001-neuro-rag-learning)
|
||||||
|
|
||||||
@@ -33,9 +35,9 @@ npm test && npm run lint
|
|||||||
Java 21 (backend), TypeScript / Node 20 (frontend): Follow standard conventions
|
Java 21 (backend), TypeScript / Node 20 (frontend): Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 005-native-image-deployment: Added Java 25 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, `native-maven-plugin` 0.10.6,
|
||||||
- 004-rag-retrieval-quality: Added Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), Vue 3.4, Pinia 2.1, Axios 1.7
|
- 004-rag-retrieval-quality: Added Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), Vue 3.4, Pinia 2.1, Axios 1.7
|
||||||
- 004-rag-retrieval-quality: Added Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), pgvector, Vue 3.4, Pinia 2.1
|
- 004-rag-retrieval-quality: Added Java 21 (backend), TypeScript / Node 20 (frontend) + Spring Boot 4.0.5, Spring AI 2.0.0-M4, OpenAI API (chat + embeddings), pgvector, Vue 3.4, Pinia 2.1
|
||||||
- 003-basic-login: Added Java 21 (backend) / TypeScript + Node 20 (frontend) + Spring Boot 4.0.5, Spring Security (already included), Vue 3.4, Vue Router 4.3, Pinia 2.1, Axios 1.7
|
|
||||||
|
|
||||||
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ PictureGroup
|
|||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- **Backend**: Spring Boot 4.0.5 + Spring AI 2.0.0-M4, Java 21, Maven
|
- **Backend**: Spring Boot 4.0.5 + Spring AI 2.0.0-M4, Java 25, Maven
|
||||||
- **Frontend**: Vue.js 3 + Vite + TypeScript + Pinia + Axios
|
- **Frontend**: Vue.js 3 + Vite + TypeScript + Pinia + Axios
|
||||||
- **Database**: PostgreSQL 16 + pgvector extension
|
- **Database**: PostgreSQL 16 + pgvector extension
|
||||||
- **Auth**: HTTP Basic (single shared in-memory user)
|
- **Auth**: HTTP Basic (single shared in-memory user)
|
||||||
@@ -146,7 +146,7 @@ PictureGroup
|
|||||||
|
|
||||||
See [specs/001-neuro-rag-learning/quickstart.md](specs/001-neuro-rag-learning/quickstart.md) for full instructions.
|
See [specs/001-neuro-rag-learning/quickstart.md](specs/001-neuro-rag-learning/quickstart.md) for full instructions.
|
||||||
|
|
||||||
### Local Dev
|
### Local Dev (JVM)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start the database
|
# Start the database
|
||||||
@@ -162,6 +162,55 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Native Image Build
|
||||||
|
|
||||||
|
Produces a GraalVM native binary packaged into a minimal Docker image via Jib.
|
||||||
|
|
||||||
|
**Prerequisite**: GraalVM 25 must be installed and set as `JAVA_HOME`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install GraalVM 25 CE via sdkman (one-time)
|
||||||
|
sdk install java 25-graalce
|
||||||
|
sdk use java 25-graalce
|
||||||
|
|
||||||
|
# Build native executable + Docker image (requires Docker daemon)
|
||||||
|
cd backend
|
||||||
|
mvn -Pnative package jib:dockerBuild -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
The image `ai-teacher-backend:latest` will appear in your local Docker. It starts in under 1 second and uses significantly less memory than the JVM image.
|
||||||
|
|
||||||
|
### Run Native Stack (Docker Compose)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy and fill in secrets
|
||||||
|
cp .env.example .env
|
||||||
|
# edit .env — add OPENAI_API_KEY at minimum
|
||||||
|
|
||||||
|
# Start PostgreSQL + native backend
|
||||||
|
docker compose -f docker-compose.native.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
App available at `http://localhost:8080`.
|
||||||
|
|
||||||
|
### Build Pipeline (Native)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
SRC["Source Code\n(Java 25)"]
|
||||||
|
AOT["Spring Boot AOT\n(process-aot)"]
|
||||||
|
NI["GraalVM native-image\n(native-maven-plugin)"]
|
||||||
|
EXE["Native Executable\ntarget/ai-teacher-backend"]
|
||||||
|
JIB["Jib\n(jib-native-image-extension)"]
|
||||||
|
IMG["Docker Image\nai-teacher-backend:latest\n(distroless base)"]
|
||||||
|
|
||||||
|
SRC --> AOT
|
||||||
|
AOT --> NI
|
||||||
|
NI --> EXE
|
||||||
|
EXE --> JIB
|
||||||
|
JIB --> IMG
|
||||||
|
```
|
||||||
|
|
||||||
### Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
#### Backend
|
#### Backend
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Java build artifacts
|
||||||
|
target/
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ---- Pull Maven from its official image (avoids microdnf under QEMU) ----
|
||||||
|
FROM maven:3.9.9-eclipse-temurin-21 AS maven-dist
|
||||||
|
|
||||||
|
# ---- Build stage: GraalVM 25 + Maven ----
|
||||||
|
ARG TARGETPLATFORM=linux/arm64
|
||||||
|
FROM --platform=$TARGETPLATFORM ghcr.io/graalvm/native-image-community:25 AS build
|
||||||
|
|
||||||
|
# Copy Maven from the official Maven image — no package installation needed
|
||||||
|
COPY --from=maven-dist /usr/share/maven /opt/maven
|
||||||
|
ENV PATH="/opt/maven/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Cache dependency resolution separately from source compilation
|
||||||
|
COPY pom.xml .
|
||||||
|
RUN mvn -Pnative dependency:resolve dependency:resolve-plugins -q
|
||||||
|
|
||||||
|
# Build native executable
|
||||||
|
COPY src ./src
|
||||||
|
RUN mvn -Pnative package -DskipTests
|
||||||
|
|
||||||
|
# ---- Runtime stage: minimal ARM64 distroless ----
|
||||||
|
FROM --platform=$TARGETPLATFORM gcr.io/distroless/base-nossl-debian12
|
||||||
|
COPY --from=build /app/target/ai-teacher-backend /app/ai-teacher-backend
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["/app/ai-teacher-backend"]
|
||||||
@@ -143,12 +143,97 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.graalvm.buildtools</groupId>
|
||||||
|
<artifactId>native-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Jib — package native executable (or fat-jar) into Docker image -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.google.cloud.tools</groupId>
|
||||||
|
<artifactId>jib-maven-plugin</artifactId>
|
||||||
|
<version>3.4.5</version>
|
||||||
|
<configuration>
|
||||||
|
<from>
|
||||||
|
<!-- distroless glibc base — matches GraalVM native binary ABI on Linux x86_64 -->
|
||||||
|
<image>gcr.io/distroless/base-nossl-debian12</image>
|
||||||
|
</from>
|
||||||
|
<to>
|
||||||
|
<!-- override at build time: -Djib.to.image=registry/org/image:tag -->
|
||||||
|
<image>ai-teacher-backend</image>
|
||||||
|
</to>
|
||||||
|
<container>
|
||||||
|
<ports>
|
||||||
|
<port>8080</port>
|
||||||
|
</ports>
|
||||||
|
</container>
|
||||||
|
<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>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>native</id>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
|
||||||
|
<!-- GraalVM native-image compilation -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.graalvm.buildtools</groupId>
|
||||||
|
<artifactId>native-maven-plugin</artifactId>
|
||||||
|
<version>1.0.0</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,ch.qos.logback</buildArg>
|
||||||
|
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
|
||||||
|
<buildArg>--gc=serial</buildArg>
|
||||||
|
<buildArg>-Os</buildArg>
|
||||||
|
<buildArg>-H:+RemoveUnusedSymbols</buildArg>
|
||||||
|
<buildArg>-H:-EnableLoggingFeature</buildArg>
|
||||||
|
<buildArg>-R:MaxHeapSize=128m</buildArg>
|
||||||
|
<buildArg>-R:MinHeapSize=32m</buildArg>
|
||||||
|
</buildArgs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.aiteacher;
|
package com.aiteacher;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
|
import com.aiteacher.config.NativeHintsConfig;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
|
@ImportRuntimeHints(NativeHintsConfig.class)
|
||||||
public class AiTeacherApplication {
|
public class AiTeacherApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.aiteacher.config;
|
||||||
|
|
||||||
|
import org.springframework.aot.hint.MemberCategory;
|
||||||
|
import org.springframework.aot.hint.RuntimeHints;
|
||||||
|
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||||
|
import org.springframework.aot.hint.TypeReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GraalVM native-image runtime hints for third-party libraries that use reflection
|
||||||
|
* or classpath resource scanning not covered by Spring Boot's AOT processor.
|
||||||
|
*
|
||||||
|
* Registered via @ImportRuntimeHints on AiTeacherApplication.
|
||||||
|
*/
|
||||||
|
public class NativeHintsConfig implements RuntimeHintsRegistrar {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||||
|
// PDFBox — font and encoding resources loaded via classpath scanning at runtime
|
||||||
|
hints.resources().registerPattern("org/apache/pdfbox/resources/*");
|
||||||
|
hints.resources().registerPattern("org/apache/pdfbox/resources/afm/*");
|
||||||
|
hints.resources().registerPattern("org/apache/pdfbox/resources/cmap/*");
|
||||||
|
hints.resources().registerPattern("org/apache/pdfbox/resources/glyphlist/*");
|
||||||
|
hints.resources().registerPattern("org/apache/pdfbox/resources/icc/*");
|
||||||
|
hints.resources().registerPattern("org/apache/pdfbox/resources/ttf/*");
|
||||||
|
hints.resources().registerPattern("org/apache/pdfbox/resources/version.properties");
|
||||||
|
|
||||||
|
// PDFBox — font encoding classes instantiated via reflection
|
||||||
|
hints.reflection().registerType(
|
||||||
|
org.apache.pdfbox.pdmodel.font.encoding.GlyphList.class,
|
||||||
|
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
|
||||||
|
MemberCategory.INVOKE_PUBLIC_METHODS
|
||||||
|
);
|
||||||
|
hints.reflection().registerType(
|
||||||
|
org.apache.pdfbox.pdmodel.font.encoding.WinAnsiEncoding.class,
|
||||||
|
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS
|
||||||
|
);
|
||||||
|
hints.reflection().registerType(
|
||||||
|
org.apache.pdfbox.pdmodel.font.encoding.MacRomanEncoding.class,
|
||||||
|
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS
|
||||||
|
);
|
||||||
|
hints.reflection().registerType(
|
||||||
|
org.apache.pdfbox.pdmodel.font.encoding.MacExpertEncoding.class,
|
||||||
|
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS
|
||||||
|
);
|
||||||
|
hints.reflection().registerType(
|
||||||
|
org.apache.pdfbox.pdmodel.font.encoding.StandardEncoding.class,
|
||||||
|
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS
|
||||||
|
);
|
||||||
|
|
||||||
|
// JPA / Hibernate — array types used in entity mappings
|
||||||
|
hints.reflection().registerType(java.util.UUID[].class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
|
||||||
|
|
||||||
|
// JBoss Logging — message logger implementations generated by annotation processor.
|
||||||
|
// JBoss Logging uses reflection to look up the generated *_$logger class by name.
|
||||||
|
registerJBossLogger(hints, "org.hibernate.jpa.internal.JpaLogger_$logger");
|
||||||
|
registerJBossLogger(hints, "org.hibernate.internal.CoreMessageLogger_$logger");
|
||||||
|
registerJBossLogger(hints, "org.hibernate.internal.EntityManagerMessageLogger_$logger");
|
||||||
|
|
||||||
|
// AWS SDK v2 — HTTP client and SdkPojo serialization
|
||||||
|
hints.resources().registerPattern("software/amazon/awssdk/global/handlers/execution.interceptors");
|
||||||
|
hints.resources().registerPattern("software/amazon/awssdk/services/s3/execution.interceptors");
|
||||||
|
hints.resources().registerPattern("codegen-resources/s3/*");
|
||||||
|
hints.reflection().registerType(
|
||||||
|
software.amazon.awssdk.services.s3.S3Client.class,
|
||||||
|
MemberCategory.INVOKE_PUBLIC_METHODS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerJBossLogger(RuntimeHints hints, String className) {
|
||||||
|
hints.reflection().registerType(
|
||||||
|
TypeReference.of(className),
|
||||||
|
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
|
||||||
|
MemberCategory.INVOKE_PUBLIC_METHODS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ spring:
|
|||||||
index-type: HNSW
|
index-type: HNSW
|
||||||
initialize-schema: false
|
initialize-schema: false
|
||||||
openai:
|
openai:
|
||||||
api-key: ${OPENAI_API_KEY}
|
api-key: ${OPENAI_API_KEY:}
|
||||||
chat:
|
chat:
|
||||||
options:
|
options:
|
||||||
model: gpt-4o-mini
|
model: gpt-4o-mini
|
||||||
@@ -62,8 +62,8 @@ app:
|
|||||||
endpoint: https://s3.immich-ad.ovh
|
endpoint: https://s3.immich-ad.ovh
|
||||||
region: garage
|
region: garage
|
||||||
bucket: ${S3_BUCKET:aiteacher}
|
bucket: ${S3_BUCKET:aiteacher}
|
||||||
access-key-id: ${S3_ACCESS_KEY_ID}
|
access-key-id: ${S3_ACCESS_KEY_ID:}
|
||||||
secret-access-key: ${S3_SECRET_ACCESS_KEY}
|
secret-access-key: ${S3_SECRET_ACCESS_KEY:}
|
||||||
min-image-size-px: 100
|
min-image-size-px: 100
|
||||||
embedding:
|
embedding:
|
||||||
batch-size: 20
|
batch-size: 20
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: pgvector/pgvector:pg16
|
||||||
|
container_name: aiteacher-postgres-native
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: aiteacher
|
||||||
|
POSTGRES_USER: aiteacher
|
||||||
|
POSTGRES_PASSWORD: aiteacher
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- pgdata_native:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U aiteacher -d aiteacher"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: ai-teacher-backend:latest
|
||||||
|
container_name: aiteacher-backend-native
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/aiteacher
|
||||||
|
SPRING_DATASOURCE_USERNAME: aiteacher
|
||||||
|
SPRING_DATASOURCE_PASSWORD: aiteacher
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata_native:
|
||||||
@@ -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.
|
||||||
@@ -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>
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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 (T003–T006 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 (T003–T007) — **CRITICAL, do not skip T007**
|
||||||
|
3. Complete Phase 3: US1 (T008–T011)
|
||||||
|
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`
|
||||||
Reference in New Issue
Block a user