sha256:1dc734cacffb667447e0413fb5f0ad819e628c68accb24504a5e3ca470b3076e
Construire des images Docker Java prêtes pour l’orchestration
2026-01-28
Jupyter Kernel: bash
🖥️ Env Ubuntu 24.04.3 LTS / x86_64 • ☕ Java 25.0.1 (openjdk) • 🎯 Maven 3.9.12 • 🐳 Docker Client 29.1.5 / Server 29.1.5
Ce support a été généré par Quarto : les cellules sont exécutées par le noyau indiqué lors du rendu.
Certaines parties ont été rédigées avec l’assistance d’un modèle de langage ; le contenu a été relu et validé par l’auteur.
Comment transformer une application Java en image Docker exploitable en production.
À l’issue, vous devez comprendre :
Important
Ce chapitre ne vise pas à maîtriser toutes les techniques, mais à comprendre les trajectoires possibles.
✅ Updated existing repository: ebpro/notebook-containers-intro-sample-java-helloworld
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.01.mavenimage
# ------------------------------------------------------------
# Stage: build (single-stage)
# Purpose: compile the application using Maven and package the artifact
# Base image: maven:3.9.12-eclipse-temurin-21-noble
# Maven profile: -Pprod
# Artifact: target/hello-world-0.0.1-SNAPSHOT.jar
# Notes: single-stage image includes build tools; consider multi-stage to reduce final image size
# ------------------------------------------------------------
FROM maven:3.9.12-eclipse-temurin-21-noble
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - Full Maven image"
LABEL version="0.1.0-SNAPSHOT"
LABEL license="MIT"
WORKDIR /app
# Copy wrapper
COPY mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw
# Copy POM first to leverage Docker cache
COPY pom.xml ./
RUN ./mvnw --batch-mode dependency:resolve
# Copy source code and build the application
COPY src ./src
RUN ./mvnw --batch-mode -Pprod -DskipTests clean package
# Copy entrypoint script and make it executable
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh || true
# Prepare a canonical runtime artifact path (/app/app.jar) if build produced a jar
# Use wildcard to match the built jar (SNAPSHOT or versioned)
RUN cp target/*.jar /app/app.jar 2>/dev/null || true
# Copy runtime dependency jars (produced by -Pprod) so manifest Class-Path 'libs/' resolves
RUN cp -r target/libs /app/libs 2>/dev/null || true
# Create non-root user (only for running)
RUN groupadd -r appuser && useradd -r -g appuser -m appuser \
&& chown -R appuser:appuser /app
USER appuser
ENV HOME=/home/appuser
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75"
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]sha256:1dc734cacffb667447e0413fb5f0ad819e628c68accb24504a5e3ca470b3076e
14:12:17.830 INFO fr.univtln.bruno.demos.docker.App - Java Vendor: Eclipse Adoptium | Version: 21.0.9
14:12:17.834 INFO fr.univtln.bruno.demos.docker.App - Démarrage de l'application (Iterations: 10000)...
14:12:17.873 INFO fr.univtln.bruno.demos.docker.App - Traitement terminé. Éléments filtrés : 8934 | Temps : 39 ms
Fin du programme. (Éléments traités: 8934)
ANALYSE DE L'IMAGE : javahello:mavenimage • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ❌ Polluants (Maven ou /src) • Runtime : ☕ Standard (JRE) • Java Opts : ✅ -XX:MaxRAMPercentage=75 • Poids : 288MB
Analyse critique
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.02.mavenimagestage
# ------------------------------------------------------------
# Stage: build
# Purpose: compile the application using Maven in the build stage
# Base image: maven:3.9.12-eclipse-temurin-21-noble
# Maven profile: -Pprod
# Artifact: target/hello-world-*-SNAPSHOT.jar
# Notes: use cache mounts for /root/.m2 to speed up dependency resolution
# ------------------------------------------------------------
FROM maven:3.9.12-eclipse-temurin-21-noble AS stage-build
WORKDIR /app
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - multi-stage build"
LABEL version="0.1.0-SNAPSHOT"
LABEL license="MIT"
# Copy wrapper
COPY mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw
# Copy POM first to leverage Docker cache for dependencies
COPY pom.xml ./
RUN ./mvnw --batch-mode dependency:resolve
# Copy source code and build the application
COPY src ./src
RUN ./mvnw --batch-mode -Pprod -DskipTests clean package
# ------------------------------------------------------------
# Stage: runtime
# Purpose: provide a minimal runtime image with Temurin JRE
# Base image: eclipse-temurin:21.0.9_10-jre-noble
# Copies: /app/app.jar (from build stage) and /app/libs if provided
# Notes: run the application as a non-root user; keep runtime image minimal
# ------------------------------------------------------------
FROM eclipse-temurin:21.0.9_10-jre-noble
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - multi-stage runtime"
WORKDIR /app
# Copy only the built jar from the build stage
COPY --from=stage-build /app/target/hello-world-*-SNAPSHOT.jar /app/app.jar
# Copy the libs directory (containing dependencies) if needed
COPY --from=stage-build /app/target/libs /app/libs
# Install entrypoint script (exec form) and make it executable
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh || true
# Create a non-root user for security and ensure app files are owned by that user
RUN groupadd -r appuser && useradd -r -u 1001 -g appuser -m appuser \
&& chown -R appuser:appuser /app
USER appuser
ENV HOME=/home/appuser
# Configure Java options for container environment (default, overridable)
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Use exec-form entrypoint script to preserve signals and expand JAVA_OPTS
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]sha256:f74ad4d189a902ad3ae11d0e5296b71408cddbcd25bd5abaa272d13644181e48
ANALYSE DE L'IMAGE : javahello:mavenimagestage • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : ☕ Standard (JRE) • Java Opts : ✅ -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 • Poids : 97MB
Pour être orchestrée (Kubernetes, Compose), une image doit respecter des règles d’hygiène :
appuser. Cela empêche une faille applicative de compromettre l’hôte.ENTRYPOINT ["/script.sh"] permet à la JVM de recevoir le SIGTERM pour un arrêt propre.-XX:MaxRAMPercentage permet à la JVM d’être “Container Aware”.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.03.dockercache
# ------------------------------------------------------------
# Stage: build
# Purpose: compile the application using Maven with cache mounts enabled
# Base image: maven:3.9.12-eclipse-temurin-21-noble
# Maven profile: -Pprod
# Artifact: target/hello-world-*-SNAPSHOT.jar
# Notes: uses --mount=type=cache for /root/.m2 to speed up builds; cache is not persisted in final image
# ------------------------------------------------------------
FROM maven:3.9.12-eclipse-temurin-21-noble AS stage-build
WORKDIR /app
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - multi-stage build"
LABEL version="0.1.0-SNAPSHOT"
LABEL license="MIT"
# Copy wrapper
COPY mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw
# Copy POM first to leverage Docker cache for dependencies
COPY pom.xml ./
RUN --mount=type=cache,target=/root/.m2 ./mvnw --batch-mode dependency:resolve
# Copy source code and build the application
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 ./mvnw --batch-mode -Pprod -DskipTests clean package
# ------------------------------------------------------------
# Stage: runtime
# Purpose: minimal runtime with Temurin JRE containing the packaged application
# Base image: eclipse-temurin:21.0.9_10-jre-noble
# Copies: /app/app.jar and /app/libs from build stage
# Notes: create a non-root user for security and set JAVA_OPTS appropriately
# ------------------------------------------------------------
FROM eclipse-temurin:21.0.9_10-jre-noble
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - multi-stage runtime"
WORKDIR /app
# Copy only the built jar from the build stage
COPY --from=stage-build /app/target/hello-world-*-SNAPSHOT.jar /app/app.jar
# Copy the libs directory (containing dependencies) if needed
COPY --from=stage-build /app/target/libs /app/libs
# Create a non-root user for security and ensure app files are owned by that user
RUN groupadd -r appuser && useradd -r -u 1001 -g appuser -m appuser \
&& chown -R appuser:appuser /app
USER appuser
ENV HOME=/home/appuser
# Configure Java options for container environment
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Install generic entrypoint script
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh || true
# Use generic exec-form entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]sha256:cd5f208294b41a2ee09515c7c48f064c6a7958bd1d6a3519811125435e7ac601
ANALYSE DE L'IMAGE : javahello:dockercache • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : ☕ Standard (JRE) • Java Opts : ✅ -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 • Poids : 97MB
Note
L’instruction --mount=type=cache,target=/root/.m2 est une fonctionnalité BuildKit qui transforme radicalement l’expérience de CI/CD.
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.05.manual
# ------------------------------------------------------------
# Stage: build
# Purpose: manual build using SDKMAN to install Java and Maven, then compile the app
# Base image: ubuntu:jammy (SDKMAN will install Java/Maven)
# Maven profile: -Pprod
# Artifact: target/hello-world-*-SNAPSHOT.jar
# Notes: BUILDER_UID/BUILDER_GID control file ownership; use --mount=type=cache for /home/builder/.m2
# ------------------------------------------------------------
FROM ubuntu:jammy AS stage-build
ARG JAVA_VERSION="21.0.2-tem"
ARG MAVEN_VERSION="3.9.6"
ARG BUILDER_UID=2000
ARG BUILDER_GID=2000
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - multi-stage build (SDKMAN)"
LABEL version="0.1.0-SNAPSHOT"
LABEL license="MIT"
# Install dependencies (as root)
RUN apt-get update && \
apt-get install --yes --quiet --no-install-recommends \
ca-certificates \
curl \
unzip \
zip \
bash && \
rm -rf /var/lib/apt/lists/*
# Create builder user with fixed 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"]
# Install SDKMAN + Java + Maven (as builder)
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
# Copy wrapper
COPY --chown=builder:builder mvnw ./
COPY --chown=builder:builder .mvn .mvn
# Copy POM first
COPY --chown=builder:builder pom.xml ./
# Resolve dependencies using cache (correct UID/GID)
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
# Copy source
COPY --chown=builder:builder src ./src
# Build
RUN --mount=type=cache,target=/home/builder/.m2,uid=${BUILDER_UID},gid=${BUILDER_GID} \
source "$SDKMAN_DIR/bin/sdkman-init.sh" && \
./mvnw --batch-mode -Pprod -DskipTests clean package
# ------------------------------------------------------------
# Stage: runtime
# Purpose: provide runtime using Temurin JRE with same UID/GID as build user
# Base image: eclipse-temurin:21.0.9_10-jre-noble
# Copies: /app/app.jar and /app/libs from build stage
# Notes: create runtime user with same UID/GID to avoid permission issues
# ------------------------------------------------------------
FROM eclipse-temurin:21.0.9_10-jre-noble AS stage-runtime
ARG BUILDER_UID=2000
ARG BUILDER_GID=2000
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World Application - multi-stage runtime"
# Create same user in runtime (same UID/GID)
RUN groupadd -g ${BUILDER_GID} appuser \
&& useradd -m -u ${BUILDER_UID} -g appuser -s /bin/bash appuser
WORKDIR /app
COPY --from=stage-build /app/target/hello-world-*-SNAPSHOT.jar /app/app.jar
COPY --from=stage-build /app/target/libs /app/libs
RUN chown -R appuser:appuser /app
# Install generic entrypoint script and make executable
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh || true
USER appuser
ENV HOME=/home/appuser
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Use generic exec-form entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]sha256:8fd8977801620a8cea2705ef28d7f60e1b4214da380e12e99c88c640f4b521ba
ANALYSE DE L'IMAGE : javahello:manual • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : ☕ Standard (JRE) • Java Opts : ✅ -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 • Poids : 96MB
jlink permet de créer un runtime Java sur mesure contenant uniquement les modules nécessaires.
Avertissement
Nécessite une application modularisée (JPMS) et une analyse fine des dépendances.
Dockerfile.06.jlink
# ------------------------------------------------------------
# Stage: build
# Purpose: compile the application and produce a custom runtime via jlink
# Base image: maven:3.9.12-eclipse-temurin-21-noble
# Maven profile: -Pjlink
# Artifact: target/maven-jlink/classifiers/runtime-image (copied to /jre)
# Notes: maven-jlink-plugin produces a modular runtime image and a launcher; verify launcher name
# ------------------------------------------------------------
FROM maven:3.9.12-eclipse-temurin-21-noble AS build
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World - jlink build stage"
LABEL license="MIT"
WORKDIR /app
# Maven Wrapper
COPY mvnw ./
COPY .mvn .mvn
RUN chmod +x mvnw
# Pré-chargement des dépendances (cache Docker)
COPY pom.xml ./
RUN --mount=type=cache,target=/root/.m2 \
./mvnw --batch-mode dependency:resolve
# Sources + build jlink
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
./mvnw --batch-mode -Pjlink clean package
# Ensure jlink output is placed in a stable location inside the build image
# Some maven-jlink-plugin configurations produce the runtime image under
# target/maven-jlink/classifiers/runtime-image; copy its contents to /app/jre
RUN mkdir -p /app/jre \
&& if [ -d target/maven-jlink/classifiers/runtime-image ]; then \
cp -a target/maven-jlink/classifiers/runtime-image/* /app/jre/ ; \
fi
# ------------------------------------------------------------
# Stage: runtime
# Purpose: minimal runtime built from the jlink-generated runtime-image
# Base image: debian:bookworm-slim
# Copies: /jre (runtime image) from build stage to /jre in final image
# Notes: jlink runtime is custom and contains only required modules; run the launcher at /jre/bin/hello
# ------------------------------------------------------------
FROM debian:bookworm-slim
LABEL maintainer="Emmanuel Bruno <emmanuel.bruno@univ-tln.fr>"
LABEL description="Java Hello World - jlink runtime"
LABEL version="0.1.0"
# Répertoire applicatif
WORKDIR /app
# Runtime Java custom généré par jlink
COPY --from=build /app/jre /jre
# Utilisateur non-root (bonne pratique)
# Create a dedicated group 'appuser' and add the user to it, then set ownership on /jre
RUN groupadd -g 1001 appuser \
&& useradd --system --uid 1001 -g appuser -s /usr/sbin/nologin appuser \
&& chown -R appuser:appuser /jre \
&& chmod -R go-w /jre
USER appuser
# Exécution via le launcher jlink
# Prefer generic entrypoint: it will detect and exec the jlink launcher (/jre/bin/hello)
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh || true
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]sha256:27416777c61c9424a0b6a3f2c12f4ef9aaafc5a46cb7f6a778b155408b0aebab
ANALYSE DE L'IMAGE : javahello:jlink • Sécurité : ✅ appuser • Surface : ⚠️ Shell présent • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : 🧩 JLink (Launcher 'hello') • Java Opts : ❌ Manquante • Poids : 77MB
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.
Avertissement
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:
Inconvénients:
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.
GraalVM compile Java en binaire natif.
Dockerfile.07.graalVM
# ------------------------------------------------------------
# Stage: build
# Purpose: build a static native binary using GraalVM native-image
# Base image: ghcr.io/graalvm/native-image-community:25-muslib
# Maven profile: -Pgraalvm
# Artifact: target/app (native binary)
# Notes: install zlib-static and use musl toolchain; verify binary is truly static before final image
# ------------------------------------------------------------
FROM ghcr.io/graalvm/native-image-community:25-muslib AS builder
# Installation de zlib-static (indispensable pour le flag --static)
RUN microdnf install -y gcc make binutils zlib-static && microdnf clean all
WORKDIR /app
# IMPORTANT : On force l'utilisation du compilateur musl pour les tests de fonctionnalités
ENV CC=/usr/local/musl/bin/gcc
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN chmod +x mvnw
# On télécharge les dépendances
RUN --mount=type=cache,target=/root/.m2 ./mvnw dependency:go-offline -Pgraalvm
COPY src ./src
# On lance la compilation via Maven
# Le flag --libc=musl dans le POM fera le reste
RUN --mount=type=cache,target=/root/.m2 ./mvnw package -Pnative -DskipTests
# Place any produced native binary in a stable location (/app/app) so final stage can copy it
RUN mkdir -p /app \
&& candidate=$(find target -type f \( -name app -o -name "*app" -o -name "*native*" -o -name "*-runner" -o -name "*.exe" -o -name "*.bin" -o -name "*.so" \) 2>/dev/null | head -n 1 || true) \
&& if [ -n "$candidate" ]; then cp "$candidate" /app/app; chmod +x /app/app || true; else echo "No native artifact found under target/" >&2; fi
# ------------------------------------------------------------
# Stage: runtime
# Purpose: minimal final image for the static native binary
# Base image: gcr.io/distroless/static-debian12
# Copies: /app/app (native binary) from build stage
# Notes: distroless static image contains no shell; ensure user exists or set numeric UID and that binary is static
# ------------------------------------------------------------
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/app /app/app
# Use numeric UID/GID to avoid relying on username existing in distroless
USER 65532:65532
ENTRYPOINT ["/app/app"]sha256:ca304cc0df56859a8f232dd1977b77d7115b0910ecd8341c139ad019e8a12eb5
ANALYSE DE L'IMAGE : javahello:graalvm • Sécurité : ✅ 65532:65532 • Surface : ⚠️ Shell présent • Structure : ✅ Artefact pur (Multi-stage OK) • Runtime : 🚀 Natif (GraalVM app) • Java Opts : ✅ N/A (Binaire natif) • Poids : 10MB
| 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 |
Points clés pour la mise en production
stdout/stderr (laissé à la charge de l’orchestrateur).Prochaine étape : orchestrer ces images avec Docker Compose.
E. Bruno - Conteneurs — Application à Java