Skip to content

Instantly share code, notes, and snippets.

@ammbra
Last active October 18, 2025 08:33
Show Gist options
  • Select an option

  • Save ammbra/59aa7cdb776145a227469730020aa5a4 to your computer and use it in GitHub Desktop.

Select an option

Save ammbra/59aa7cdb776145a227469730020aa5a4 to your computer and use it in GitHub Desktop.
Example Dockerfiles with Two-Step AOT Workflow
version: '3.8'
services:
aotgen:
build:
context: .
dockerfile: Dockerfile.aotgen
args:
JAR_FILE: app.jar
environment:
AOT_DIR: /cache
volumes:
- ./cache:/cache
# run once to produce ./cache/app.aot and exit
restart: "no"
petclinic:
build:
context: .
dockerfile: Dockerfile.deploy
environment:
AOT_DIR: /cache
volumes:
- ./cache:/cache
depends_on:
aotgen:
condition: service_completed_successfully
ports:
- "8080"
FROM container-registry.oracle.com/java/openjdk:25-oraclelinux9 as builder
ARG MODULES=java.base,java.compiler,java.desktop,java.instrument,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql.rowset,jdk.jfr,jdk.management,jdk.management.agent,jdk.management.jfr,jdk.jcmd,jdk.net,jdk.unsupported
RUN $JAVA_HOME/bin/jlink \
--add-modules ${MODULES} \
--no-man-pages \
--no-header-files \
--compress=zip-9 \
--output /javaruntime
WORKDIR /builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
RUN $JAVA_HOME/bin/java -Djarmode=tools -jar app.jar extract --layers --destination extracted
# AOT generator image
FROM container-registry.oracle.com/os/oraclelinux:9-slim
ENV JAVA_HOME=/usr/java/openjdk-25
ENV PATH=$JAVA_HOME/bin:$PATH
ENV AOT_DIR=/cache
ENV AOT_CACHE=${AOT_DIR}/app.aot
COPY --from=builder /javaruntime $JAVA_HOME
WORKDIR /application
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./
# Continue with training run and assembly phase
RUN groupadd -r appuser && useradd -r -g appuser appuser && chown -R appuser:appuser /application
USER appuser
# Run once to generate the AOT cache into the mounted host directory, then exit
CMD ["/bin/sh","-c","mkdir -p \"${AOT_DIR}\" && java -XX:AOTCacheOutput=\"${AOT_CACHE}\" -Dspring.context.exit=onRefresh -jar app.jar"]
FROM container-registry.oracle.com/java/openjdk:25-oraclelinux9 as runtime-build
ARG MODULES=java.base,java.compiler,java.desktop,java.instrument,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql.rowset,jdk.jfr,jdk.management,jdk.management.agent,jdk.management.jfr,jdk.jcmd,jdk.net,jdk.unsupported
RUN $JAVA_HOME/bin/jlink \
--add-modules ${MODULES} \
--no-man-pages \
--no-header-files \
--compress=zip-9 \
--output /javaruntime
FROM container-registry.oracle.com/os/oraclelinux:9-slim
ENV JAVA_HOME /usr/java/openjdk-25
ENV PATH $JAVA_HOME/bin:$PATH
COPY --from=runtime-build /javaruntime $JAVA_HOME
ARG JAR_FILE=target/*.jar
ENV AOT_DIR=cache
COPY ${JAR_FILE} app.jar
# Continue with training run and assembly phase
RUN mkdir ${AOT_DIR} && chmod 755 ${AOT_DIR} \
&& java -XX:AOTCacheOutput=${AOT_DIR}/app.aot -Dspring.context.exit=onRefresh -jar app.jar \
&& groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# Deployment run
CMD java -XX:AOTCache=${AOT_DIR} -jar app.jar
FROM container-registry.oracle.com/java/openjdk:25-oraclelinux9 as builder
ARG MODULES=java.base,java.compiler,java.desktop,java.instrument,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql.rowset,jdk.jfr,jdk.management,jdk.management.agent,jdk.management.jfr,jdk.jcmd,jdk.net,jdk.unsupported
RUN $JAVA_HOME/bin/jlink \
--add-modules ${MODULES} \
--no-man-pages \
--no-header-files \
--compress=zip-9 \
--output /javaruntime
WORKDIR /builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
RUN $JAVA_HOME/bin/java -Djarmode=tools -jar app.jar extract --layers --destination extracted
# Runtime image
FROM container-registry.oracle.com/os/oraclelinux:9-slim
ENV JAVA_HOME=/usr/java/openjdk-25
ENV PATH=$JAVA_HOME/bin:$PATH
ENV AOT_DIR=/cache
ENV AOT_CACHE=${AOT_DIR}/app.aot
COPY --from=builder /javaruntime $JAVA_HOME
WORKDIR /application
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./
# Non-root runtime
RUN groupadd -r appuser && useradd -r -g appuser appuser && chown -R appuser:appuser /application
USER appuser
CMD java -XX:AOTCache="${AOT_CACHE}" -jar app.jar
FROM container-registry.oracle.com/java/openjdk:25-oraclelinux9 as builder
ARG MODULES=java.base,java.compiler,java.desktop,java.instrument,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql.rowset,jdk.jfr,jdk.management,jdk.management.agent,jdk.management.jfr,jdk.jcmd,jdk.net,jdk.unsupported
RUN $JAVA_HOME/bin/jlink \
--add-modules ${MODULES} \
--no-man-pages \
--no-header-files \
--compress=zip-9 \
--output /javaruntime
WORKDIR /builder
# This points to the built jar file in the target folder
# Adjust this to 'build/libs/*.jar' if you're using Gradle
ARG JAR_FILE=target/*.jar
# Copy the jar file to the working directory and rename it to application.jar
COPY ${JAR_FILE} app.jar
# Extract the jar file using an efficient layout
RUN $JAVA_HOME/bin/java -Djarmode=tools -jar app.jar extract --layers --destination extracted
FROM container-registry.oracle.com/os/oraclelinux:9-slim
ENV JAVA_HOME /usr/java/openjdk-25
ENV PATH $JAVA_HOME/bin:$PATH
ENV AOT_DIR=/cache
COPY --from=builder /javaruntime $JAVA_HOME
WORKDIR /application
# Copy the extracted jar contents from the builder container into the working directory in the runtime container
# Every copy step creates a new docker layer
# This allows docker to only pull the changes it really needs
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./
# Continue with training run and assembly phase
RUN mkdir ${AOT_DIR} && chmod 755 ${AOT_DIR} \
&& java -XX:AOTCacheOutput=${AOT_DIR}/app.aot -Dspring.context.exit=onRefresh -jar app.jar \
&& groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# Deployment run
CMD java -XX:AOTCache=${AOT_DIR}/app.aot -jar app.jar
@ammbra
Copy link
Author

ammbra commented Sep 29, 2025

Different Dockerfile Configurations for Working with AOT Cache

Below are some Dockerfile configurations tried for working with AOT Cache and Spring Boot based applications.

Note

If you build your .jar without enabling layering, you may remove the extra copy commands for dependencies, spring-boot-loader and snapshot-dependencies.

Basic Dockerfile with JDK Tools Only

The Dockerfile.basic utilizes the following approach:

  1. The runtime-build phase creates a minimal runtime based on jdeps output.
jdeps --ignore-missing-deps -q --recursive  --multi-release 25  --print-module-deps  -classpath 'classpath' app.jar
  1. As application dependencies change, I preferred to store the modules in an argument that can be changed at image build time (if needed).

  2. The next Dockerfile stage uses only the operating system as base image thus minimizing the total image size.
    It will use the previously built runtime to go through training run + assembly phase and deployment run.

  3. Location of the application jar file is given via ARG JAR_FILE so it can be overwritten at image build time (docker build -f Dockerfile.basic --build-arg JAR_FILE=/another/path/to/your.jar -t app-basic:local .).

  4. Location of the AOT cache is provided via ENV AOT_CACHE and it can be overwritten at container run time.

Commands to build:

docker build -f Dockerfile.basic -t app-basic:local . --no-cache

Command to run:

docker run -p 8080:8080 --name app-basic-container app-basic:local

Basic Dockerfile with JDK Tools and Layering

The Dockerfile.layer extends the previous approach and add layering via -Djarmode=tools:

  1. The builder phase creates a minimal runtime but also extracts the layers from the .jar file.

  2. The next Dockerfile stage uses only the operating system as base image thus minimizing the total image size.
    It will copy the previously built runtime, but also the artifacts from /builder directory.

  3. Location of the AOT cache is provided via ENV AOT_CACHE and it can be overwritten at container run time.

Commands to build:

docker build -f Dockerfile.layer -t app-layered:local . --no-cache

Command to run:

docker run -p 8080:8080 --name app-layered-container app-layered:local

Observed startup time:

Separate Dockerfiles for AOT Generation and Run

Commands to build and generate the AOT cache:

docker build -f Dockerfile.aotgen --build-arg JAR_FILE=app.jar -t app-aotgen:local .
docker run --name aotgen --rm -e AOT_DIR=/cache -v "$PWD/cache:/cache" app-aotgen:local

Command to run the application with the AOT cache:

docker build -f ops/Dockerfile.deploy -t app-deploy:local .
docker run -d --name app-deploy -e AOT_DIR=/cache -v "$PWD/cache:/cache" -p 8080 app-deploy:local

Or simply run docker-compose up -f compose.yml --build to achieve the same result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment