docker image build \
--tag javahello:mavenimage \
--file Dockerfile.10.maven .Conteneurs — Application à Java
Construire des images Docker Java prêtes pour l’orchestration
Objectifs
Comment transformer une application Java en image Docker exploitable en production.
À l’issue, vous devez comprendre :
- L’impact de l’ordre des instructions sur le cache de build.
- Pourquoi séparer l’environnement de build du runtime (Multi-stage).
- Comment rendre une image orchestrable (sécurité non-root, signaux PID 1).
Ce chapitre ne vise pas à maîtriser toutes les techniques, mais à comprendre les trajectoires possibles.
Exemples pratiques
Les exemples suivants sont accessibles dans le dépôt :
✅ : ebpro/notebook-containers-intro-sample-java-helloworld
Détails
- URL: https://github.com/ebpro/notebook-containers-intro-sample-java-helloworld
- Branch: develop
- Local Path:
/home/jovyan/work/examples/github/ebpro/notebook-containers-intro-sample-java-helloworld - Latest Commit: a21605e - fix: native-image heap cap must use JAVA_TOOL_OPTIONS (real JVM var)
- Timestamp: 2026-09-30T07:03:15Z
Clone Command:
git clone -b develop https://github.com/ebpro/notebook-containers-intro-sample-java-helloworldImage Java simple (Maven embarqué)
La méthode la plus directe consiste à utiliser une image Maven officielle. C’est l’approche “tout-en-un” où l’on compile et exécute au même endroit.
Dockerfile.10.maven
# ============================================================
# Stage: Build & Run (Single-stage Maven)
# Purpose: Baseline development environment for Java 25
# ============================================================
FROM maven:3.9.16-eclipse-temurin-25-noble
# ------------------------------------------------------------
# Build Environment Configuration
# ------------------------------------------------------------
WORKDIR /app
ENV HOME=/home/appuser
ENV MAVEN_CONFIG=/home/appuser/.m2
ENV MAVEN_OPTS="-Dmaven.repo.local=/home/appuser/.m2"
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - Full Maven baseline"
# ------------------------------------------------------------
# Security: Unprivileged user creation
# ------------------------------------------------------------
RUN groupadd -r appgroup \
&& useradd -r -g appgroup -m -d /home/appuser appuser \
&& mkdir -p /home/appuser/.m2 \
&& chown -R appuser:appgroup /home/appuser \
&& chown -R appuser:appgroup /app
# ------------------------------------------------------------
# Layer 1: Dependency Caching
# ------------------------------------------------------------
# Copying only the wrapper and POM allows Docker to cache
# dependencies independently of source code changes.
COPY --chown=appuser:appgroup .mvn/ .mvn
COPY --chown=appuser:appgroup mvnw pom.xml ./
USER appuser
# Pre-resolve project dependencies to optimize build time
RUN ./mvnw dependency:resolve
# ------------------------------------------------------------
# Layer 2: Source Compilation
# ------------------------------------------------------------
COPY --chown=appuser:appgroup src ./src
# Build artifact using the 'jvm' profile (configured for thin JAR)
RUN ./mvnw clean package -DskipTests -Pjvm
# ------------------------------------------------------------
# Execution
# ------------------------------------------------------------
# Configure JVM container ergonomics for resource management
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Explicitly invoke the JAR. The 'jvm' profile's Manifest
# automatically links to external dependencies in the /libs folder.
ENTRYPOINT ["java", "-jar", "/app/target/hello-world-0.1.0-SNAPSHOT.jar"]docker container run --rm javahello:mavenimagePicked up JAVA_TOOL_OPTIONS: -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0
07:04:08.564 INFO fr.univtln.bruno.demos.docker.App - Java Vendor: Eclipse Adoptium | Version: 25.0.4.1
07:04:08.572 INFO fr.univtln.bruno.demos.docker.App - Démarrage de l'application (Iterations: 10000)...
07:04:08.655 INFO fr.univtln.bruno.demos.docker.App - Traitement terminé. Éléments filtrés : 8926 | Temps : 82 ms
Fin du programme. (Éléments traités: 8926)
ANALYSE DE L'IMAGE : javahello:mavenimage • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ❌ Polluants (Maven ou /src) • Runtime : ☕ Standard (JRE) • Java Opts : ❌ Manquante • Poids : 509MB
- Sécurité : L’image contient le code source, Maven et le JDK complet. C’est une surface d’attaque inutile.
- Poids : L’image dépasse souvent les 800MB pour une application simple.
Image Java optimisée (multi-stage build)
Le Multi-stage build est la stratégie de référence. On utilise une image “lourde” pour compiler, puis on ne transfère que le .jar et les dépendances (lib/*.jar) vers une image JRE légère pour l’exécution.
Dockerfile.20.stage
# ============================================================
# Stage 1: Build environment
# ============================================================
# Uses the full Maven SDK to compile the source code into a thin JAR
FROM maven:3.9.16-eclipse-temurin-25-noble AS stage-build
WORKDIR /app
# Cache dependency layer: Copying only the wrapper and POM
# allows Docker to skip downloading dependencies if they haven't changed.
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
# Copy source code and perform a clean build
COPY src ./src
# Build using the 'jvm' profile (configured to output a thin JAR)
RUN ./mvnw clean package -DskipTests -Pjvm
# ============================================================
# Stage 2: Runtime environment
# ============================================================
# Uses the JRE-only base image to minimize the security attack surface
FROM eclipse-temurin:25-jre-noble
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - multi-stage"
# Security: Create an unprivileged user/group to run the application
RUN groupadd -r appgroup && useradd -r -g appgroup appuser && \
mkdir -p /app && chown -R appuser:appgroup /app
# Copy artifacts from the builder stage:
# Only the thin JAR and external dependencies are required at runtime.
COPY --from=stage-build --chown=appuser:appgroup /app/target/libs /app/libs
COPY --from=stage-build --chown=appuser:appgroup /app/target/hello-world-*.jar /app/app.jar
USER appuser
# Configure JVM ergonomics for containerized environments:
# JAVA_TOOL_OPTIONS is natively parsed by the JVM at startup,
# ensuring memory limits are respected without needing shell script injection.
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Execute the application
ENTRYPOINT ["java", "-jar", "/app/app.jar"]docker image build \
--tag javahello:mavenimagestage \
--file Dockerfile.20.stage .ANALYSE DE L'IMAGE : javahello:mavenimagestage • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : ☕ Standard (JRE) • Java Opts : ❌ Manquante • Poids : 298MB
Prêt pour l’orchestration (Production-Ready)
Pour être orchestrée (Kubernetes, Compose), une image doit respecter des règles d’hygiène :
- Non-Root : Vos Dockerfiles utilisent un
appuser. Cela empêche une faille applicative de compromettre l’hôte. - Signaux (Exec Form) : L’usage de
ENTRYPOINT ["/script.sh"]permet à la JVM de recevoir leSIGTERMpour un arrêt propre. - Mémoire : L’usage de
-XX:MaxRAMPercentagepermet à la JVM d’être “Container Aware”.
Affichage des images créées
docker image ls \
--filter "reference=javahello" \
--format "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.Size}}\t{{.CreatedSince}}"REPOSITORY TAG IMAGE ID SIZE CREATED
javahello mavenimagestage c0415d4244c7 312MB 8 seconds ago
javahello mavenimage f21f79c68234 533MB About a minute ago
Accélérer les builds (Cache BuildKit)
Docker permet de monter des caches persistants pour le répertoire ~/.m2. Cela évite de retélécharger les dépendances à chaque modification du code source.
Dockerfile.30.cache
# ============================================================
# Stage 1: Build Environment
# Purpose: Leverage BuildKit caching to accelerate Maven builds
# ============================================================
FROM maven:3.9.16-eclipse-temurin-25-noble AS stage-build
WORKDIR /app
# ------------------------------------------------------------
# Layer 1: Maven Wrapper and Project Metadata
# ------------------------------------------------------------
# Copying the wrapper and POM first allows Docker to cache the
# dependency resolution layer, skipping downloads if the project
# structure hasn't changed.
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN chmod +x mvnw
# ------------------------------------------------------------
# Layer 2: Dependency Caching (BuildKit)
# ------------------------------------------------------------
# Using BuildKit's cache mount persists the local M2 repository
# across builds, significantly reducing network overhead.
RUN --mount=type=cache,target=/root/.m2 \
./mvnw --batch-mode dependency:go-offline
# ------------------------------------------------------------
# Layer 3: Application Build
# ------------------------------------------------------------
COPY src ./src
# Compile the project using the 'jvm' profile.
# The persistent cache ensures that even if sources change,
# dependencies do not need to be re-downloaded.
RUN --mount=type=cache,target=/root/.m2 \
./mvnw --batch-mode clean package -DskipTests -Pjvm
# ============================================================
# Stage 2: Runtime Environment
# ============================================================
# Using a JRE-only base image minimizes the attack surface
# and keeps the final image size optimized for production.
FROM eclipse-temurin:25-jre-noble
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - BuildKit cache optimized"
# Security: Create an unprivileged user/group to adhere to the principle of least privilege
RUN groupadd -r appgroup && useradd -r -g appgroup appuser && \
mkdir -p /app && chown -R appuser:appgroup /app
# ------------------------------------------------------------
# Deployment: Copy artifacts from builder stage
# ------------------------------------------------------------
# We only copy the thin JAR and external dependencies required at runtime.
COPY --from=stage-build --chown=appuser:appgroup /app/target/libs /app/libs
COPY --from=stage-build --chown=appuser:appgroup /app/target/hello-world-*.jar /app/app.jar
USER appuser
# ------------------------------------------------------------
# Runtime Execution
# ------------------------------------------------------------
# Configure JVM container ergonomics. JAVA_TOOL_OPTIONS is parsed
# natively by the JVM, avoiding the need for shell script wrappers.
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Execute the application. The JAR manifest is configured to
# resolve external dependencies from the /app/libs/ directory.
ENTRYPOINT ["java", "-jar", "/app/app.jar"]docker image build \
--tag javahello:dockercache \
--file Dockerfile.30.cache .ANALYSE DE L'IMAGE : javahello:dockercache • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : ☕ Standard (JRE) • Java Opts : ❌ Manquante • Poids : 298MB
L’instruction --mount=type=cache,target=/root/.m2 est une fonctionnalité BuildKit qui transforme radicalement l’expérience de CI/CD.
Construction personnalisée (SDKMAN) — Avancé
L’usage de SDKMAN permet un contrôle granulaire sur les versions exactes du JDK et des outils de build, indépendamment des images Maven officielles.
Dockerfile.40.manual
# ============================================================
# Stage 1: Build Environment (Manual SDKMAN Toolchain)
# Purpose: Demonstrate manual environment provisioning and
# fixed-UID/GID file ownership for high-security CI.
# ============================================================
FROM ubuntu:noble AS stage-build
# Toolchain versioning for reproducible builds
ARG JAVA_VERSION="25.0.3-tem"
ARG MAVEN_VERSION="3.9.16"
ARG BUILDER_UID=2000
ARG BUILDER_GID=2000
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - SDKMAN build"
# ------------------------------------------------------------
# OS Layer: Provision essential tools for SDKMAN
# ------------------------------------------------------------
RUN apt-get update && \
apt-get install --yes --quiet --no-install-recommends \
ca-certificates curl unzip zip bash && \
rm -rf /var/lib/apt/lists/*
# ------------------------------------------------------------
# Security: Create dedicated builder user with deterministic UID/GID
# ------------------------------------------------------------
RUN groupadd -g ${BUILDER_GID} builder \
&& useradd -m -u ${BUILDER_UID} -g builder -s /bin/bash builder
USER builder
ENV HOME=/home/builder
ENV SDKMAN_DIR="$HOME/.sdkman"
ENV PATH="$SDKMAN_DIR/bin:$SDKMAN_DIR/candidates/java/current/bin:$SDKMAN_DIR/candidates/maven/current/bin:$PATH"
SHELL ["/bin/bash", "-c"]
# ------------------------------------------------------------
# Provisioning: Install Java/Maven via SDKMAN
# ------------------------------------------------------------
RUN curl -s "https://get.sdkman.io" | bash && \
source "$SDKMAN_DIR/bin/sdkman-init.sh" && \
sdk install java "$JAVA_VERSION" && \
sdk install maven "$MAVEN_VERSION" && \
rm -rf "$SDKMAN_DIR/archives/*" "$SDKMAN_DIR/tmp/*"
WORKDIR /app
# ------------------------------------------------------------
# Layer: Dependency Resolution (BuildKit Cached)
# ------------------------------------------------------------
COPY --chown=builder:builder mvnw ./
COPY --chown=builder:builder .mvn .mvn
COPY --chown=builder:builder pom.xml ./
RUN --mount=type=cache,target=/home/builder/.m2,uid=${BUILDER_UID},gid=${BUILDER_GID} \
source "$SDKMAN_DIR/bin/sdkman-init.sh" && \
./mvnw --batch-mode dependency:resolve
# ------------------------------------------------------------
# Layer: Build
# ------------------------------------------------------------
COPY --chown=builder:builder src ./src
RUN --mount=type=cache,target=/home/builder/.m2,uid=${BUILDER_UID},gid=${BUILDER_GID} \
source "$SDKMAN_DIR/bin/sdkman-init.sh" && \
./mvnw --batch-mode -Pjvm -DskipTests clean verify
# ============================================================
# Stage 2: Runtime Environment
# ============================================================
# Distribute using a slim JRE-only image. Note: We do not require
# the UID to match the builder; we only need a non-root user.
FROM eclipse-temurin:25-jre-noble AS stage-runtime
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - multi-stage runtime"
RUN groupadd -r appgroup && useradd -r -g appgroup appuser && \
mkdir -p /app && chown -R appuser:appgroup /app
# Deploy minimal runtime artifacts from the build stage
COPY --from=stage-build --chown=appuser:appgroup /app/target/hello-world-*-SNAPSHOT.jar /app/app.jar
COPY --from=stage-build --chown=appuser:appgroup /app/target/libs /app/libs
USER appuser
ENV HOME=/home/appuser
# JVM Container Ergonomics: JAVA_TOOL_OPTIONS is natively parsed by the
# JVM, enabling container-aware resource management.
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
ENTRYPOINT ["java", "-jar", "/app/app.jar"]docker image build \
--tag javahello:manual \
--file Dockerfile.40.manual .ANALYSE DE L'IMAGE : javahello:manual • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : ☕ Standard (JRE) • Java Opts : ❌ Manquante • Poids : 298MB
Image minimale avec jlink (JPMS) — Expert
jlink permet de créer un runtime Java sur mesure contenant uniquement les modules nécessaires.
Nécessite une application modularisée (JPMS) et une analyse fine des dépendances.
Dockerfile.50.jlink
# ============================================================
# Stage 1: Build Environment
# Purpose: Leverage Maven to compile code and "slice" a
# custom JRE using the jlink toolchain.
# ============================================================
FROM maven:3.9.16-eclipse-temurin-25-noble AS builder
WORKDIR /workspace
# ------------------------------------------------------------
# Layer 1: Build Infrastructure
# ------------------------------------------------------------
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN chmod +x mvnw
# ------------------------------------------------------------
# Layer 2: Dependency Caching
# ------------------------------------------------------------
# Warm the Maven cache to speed up subsequent builds
RUN --mount=type=cache,target=/root/.m2 \
./mvnw -q dependency:go-offline
# ------------------------------------------------------------
# Layer 3: Application Build & JRE Slicing
# ------------------------------------------------------------
COPY src ./src
# The 'jlink' profile triggers the maven-jlink-plugin, which
# inspects the module-info and builds a minimal, custom runtime
# image that contains only the required JDK modules.
RUN --mount=type=cache,target=/root/.m2 \
./mvnw clean verify -Pjlink -DskipTests
# ============================================================
# Stage 2: Runtime Environment (Ubuntu Noble)
# Purpose: Deploy the custom, modularized Java runtime.
# ============================================================
FROM ubuntu:noble
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application with Custom JRE via jlink"
# Security: Create a non-privileged user to run the application
RUN groupadd -r appgroup && useradd -r -g appgroup appuser && \
mkdir -p /jre && chown -R appuser:appgroup /jre
# ------------------------------------------------------------
# Deployment: Copy the custom runtime
# ------------------------------------------------------------
# The custom JRE includes the application logic, dependencies,
# and the native launcher ('app') generated during the build.
COPY --from=builder --chown=appuser:appgroup /workspace/target/maven-jlink/classifiers/runtime-image/ /jre/
USER appuser
# JVM Container Ergonomics: JAVA_TOOL_OPTIONS is natively parsed by
# the JRE, ensuring resource limits are applied to the custom runtime.
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Execution: Invoke the native binary created by jlink.
# No 'java -jar' command is needed, as the application is now a
# self-contained modular executable.
ENTRYPOINT ["/jre/bin/app"]ANALYSE DE L'IMAGE : javahello:jlink • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : ☕ Standard (JRE) • Java Opts : ❌ Manquante • Poids : 121MB
jlink-alpine — Minimalité et compromis
jlink-alpine propose un runtime Java construit sur Alpine (musl) contenant uniquement les modules nécessaires. C’est une excellente option pédagogique pour montrer l’impact du runtime sur la taille d’image, mais elle comporte des limites pratiques.
Alpine utilise musl (libc alternative). Les composants natifs (JNI), certaines bibliothèques précompilées ou des outils attendus par glibc peuvent ne pas fonctionner. Testez systématiquement avant production.
Avantages:
- Image très compacte (gain de plusieurs dizaines à centaines de MB).
- Idéale pour démonstrations et environnements contrôlés.
Inconvénients:
- Compatibilité native réduite (JNI, agents natifs, certains drivers).
- Parfois plus difficile à déboguer (outils absents dans l’image).
Commandes utiles (exemples étudiants) :
Recommandation : pour un déploiement robuste, préférez jlink (Debian/distroless) sauf si vous maîtrisez l’ensemble des dépendances natives.
Exécutable natif avec GraalVM — Culture
GraalVM compile Java en binaire natif.
- Avantage : Démarrage instantané (< 100ms) et RAM dérisoire.
- Inconvénient : Build complexe et perte de certaines capacités dynamiques de la JVM.
Dockerfile.60.graalvm
# syntax=docker/dockerfile:1
# Layer 1 — GraalVM Native Image toolchain
# ============================================================================
# GraalVM + native-image (the "native" toolchain, installed on top of the JDK)
ARG GRAALVM_VERSION=25
FROM ghcr.io/graalvm/native-image-community:${GRAALVM_VERSION} AS builder
WORKDIR /workspace
# Maven wrapper + POM first (re-uses the /root/.m2 cache for dependency download)
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN chmod +x mvnw
RUN --mount=type=cache,target=/root/.m2 \
./mvnw -q -Pnative dependency:go-offline
# Cap the native-image build heap so the CI render stays within the runner
# memory budget (large pool: 24 Gi cgroup limit). JAVA_TOOL_OPTIONS is read by
# every JVM in the build (maven + native-image analysis); -Xmx8G leaves headroom
# for the native compiler + GC + runtime within the limit.
ENV JAVA_TOOL_OPTIONS=-Xmx8G
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
./mvnw -Pnative -DskipTests package
# Layer 2 — minimal runtime
FROM gcr.io/distroless/base-debian12:nonroot AS runtime
WORKDIR /workspace
# Copy the compiled native executable from the builder stage
COPY --from=builder /workspace/target/*-runner /app
# The native executable is self-contained (no JVM needed at runtime).
# Run as the nonroot user (uid 65532) provided by the distroless base image.
USER nonroot:nonroot
ENTRYPOINT ["/app"]
EXPOSE 8080
# -----------------------------------------------------------------------------
# Layer 3 — optional: multi-stage variant (for the "advanced" lesson)
# -----------------------------------------------------------------------------
# In a real project you'd split build + runtime more aggressively. Here we show
# the same idea with a single GraalVM image used for both stages, to keep the
# lesson focused on the toolchain rather than image optimisation.
FROM ghcr.io/graalvm/native-image-community:${GRAALVM_VERSION} AS build-and-run
WORKDIR /workspace
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN chmod +x mvnw
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
./mvnw -Pnative -DskipTests package
CMD ["./target/*-runner"]ANALYSE DE L'IMAGE : javahello:graalvm • Sécurité : ✅ nonroot:nonroot • Surface : ✅ Minimaliste / Distroless • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : 🚀 Natif (GraalVM app) • Java Opts : ✅ N/A (Binaire natif) • Poids : 20MB
Comparaison des stratégies
| Image Tag | Build Cold | Size | Peak RAM | Surface d’Attaque |
|---|---|---|---|---|
| mavenimage | 28s | 928MB | 86.01 MiB | Maximale |
| mavenimagestage | 24s | 406MB | 71.97 MiB | Moyenne |
| jlink-alpine | 30s | 163MB | 55.83 MiB | Faible |
| graalvm | 57s | 46MB | 5.02 MiB | Minimale |
Conclusion
- Multi-stage build : Séparation build / runtime.
- Utilisateur non-root : Sécurité de l’hôte.
- Gestion des ressources : Paramétrage JVM pour les limites de conteneur.
- Logs : Toujours sur
stdout/stderr(laissé à la charge de l’orchestrateur).
Prochaine étape : orchestrer ces images avec Docker Compose.