Skip to content

Add Dockerfiles: standard JVM, Java 25 AOT, and GraalVM native - #13

Merged
andreasarf merged 4 commits into
mainfrom
claude/practical-dirac-t5uv58
Oct 31, 2026
Merged

andreasarf merged 4 commits into
mainfrom
claude/practical-dirac-t5uv58

Conversation

@andreasarf

@andreasarf andreasarf commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Three multi-stage Dockerfiles, documented in docs/docker.md and linked from the README. In all three, the application is compiled inside Docker with the Maven wrapper, and the final image runs as a non-root user on port 8080.

Dockerfile Dockerfile.aot Dockerfile.native
Runs Spring Boot jar on JRE 25 same jar + Spring AOT + Java 25 AOT cache GraalVM native executable
Base images eclipse-temurin:25-jdk → eclipse-temurin:25-jre same ghcr.io/graalvm/native-image-community:25 → debian:bookworm-slim
Image size ~600 MB ~750 MB ~390 MB
Startup ~5.4 s ~1.4 s ~0.3 s
Memory after start ~300 MB ~320 MB ~80 MB
Build ~1 min ~1 min ~6 min, ~10.7 GB peak
  • Dockerfile: Maven dependencies get their own layer and a BuildKit cache mount. The jar is split into Spring Boot's layers and run with java -jar.
  • Dockerfile.aot: Spring AOT (spring-boot:process-aot, run with -Dspring.aot.enabled=true) plus a Java 25 AOT cache (JEP 483/514/515).
    • Training run. During the image build, on the runtime image itself, the app starts once with -XX:AOTCacheOutput and exits as soon as its context is ready (-Dspring.context.exit=onRefresh).
    • Startup. Containers start with -XX:AOTCache. The cache accounts for most of the speed-up: Spring AOT alone starts in about 4.3 s.
    • Checked. A strict -XX:AOTMode=on run, which refuses to start if the cache can't be used, starts fine.
    • No native profile. The build doesn't use the native profile, so it needs no GraalVM-only downloads.
  • Dockerfile.native: builds the native executable in GraalVM's community image and runs it on debian:bookworm-slim.

How this was tested

Each image was built, started three times (startup is the Spring Boot "Started … in" time), and called: GET /api/v2/projects/1 → 200 with X-Trace-Id, Swagger UI → 200, and on the native image POST …/sync → 202. Each runs as spring.

  • Dockerfile.native, with the official image (after merge). I built it unchanged against the official ghcr.io/graalvm/native-image-community:25 (digest sha256:0d936f32…; Oracle Linux 10.1, glibc 2.39, GraalVM CE 25.0.2) and ran the checks above.
    • glibc. The executable needs at most glibc 2.34, so it runs on debian:bookworm-slim (2.36).
    • Network. Only the build's network flags were added (--network host and the proxy build args).
    • Correction. Before the merge it had only been built with a local stand-in, because this sandbox couldn't reach GHCR then. The earlier note here said the official image was Oracle Linux 9 with glibc 2.34; that was wrong.
  • Dockerfile and Dockerfile.aot. I tested with the official Temurin images from Google's Docker Hub mirror (mirror.gcr.io), because Docker Hub rate-limited this sandbox. For the build only, the JDK image had this sandbox's proxy certificate and settings added. The runtime images were used unmodified.

None of the test setup is in the Dockerfiles.

Test plan

  • Build and run all three images (above)
  • Build Dockerfile.native with the official GraalVM image
  • CI green

🤖 Generated with Claude Code

https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2

- Dockerfile: Maven wrapper build on eclipse-temurin:25-jdk, layered Spring
  Boot jar on eclipse-temurin:25-jre, non-root user
- Dockerfile.aot: same jar with Spring AOT (spring-boot:process-aot) and a
  Java 25 AOT cache created by a training run on the runtime image
  (-XX:AOTCacheOutput / -XX:AOTCache); starts in ~1.4 s instead of ~5.4 s
- Dockerfile.native: GraalVM native-image-community:25 builder, native
  executable on debian:bookworm-slim
- .dockerignore, docs/docker.md (native measurements to follow)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
Native image: ~400 MB, starts in ~0.27 s, ~90 MB after start, ~6 min build
peaking at ~10.6 GB. All three images verified: API, X-Trace-Id, Swagger UI,
async sync (202), non-root user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
spring.aot.enabled must be a JVM system property (an environment variable
is ignored and Spring silently starts without AOT); keep the runtime flags
when overriding the entrypoint; heap limits and another GC keep using the
AOT cache (checked with -XX:AOTMode=on); the training run needs what the
application needs at startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
Explains why the training run needs dedicated settings when the
application connects to a database, Redis or RabbitMQ at startup, shows a
value-only training profile and the alternative of training against real
services. Notes that bean-switching properties are ignored with
spring.aot.enabled (checked: springdoc.api-docs.enabled=false at runtime
had no effect) while profile property files still load (checked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
@andreasarf
andreasarf merged commit c8d5758 into main Oct 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants