Conteneurs — Application à Java

Construire des images Docker Java prêtes pour l’orchestration

Université de Toulon

LIS UMR CNRS 7020

2026-01-28

Environnement et reproductibilite

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.

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).

Important

Ce chapitre ne vise pas à maîtriser toutes les techniques, mais à comprendre les trajectoires possibles.

Exemples pratiques

✅ Updated existing repository: ebpro/notebook-containers-intro-sample-java-helloworld

Détails
git clone -b develop https://github.com/ebpro/notebook-containers-intro-sample-java-helloworld

Image 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.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"]
docker image build --quiet \
  --tag javahello:mavenimage \
  --file Dockerfile.01.mavenimage .
sha256:1dc734cacffb667447e0413fb5f0ad819e628c68accb24504a5e3ca470b3076e
docker container run --rm javahello:mavenimage
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

  • 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.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"]
docker image build --quiet \
  --tag javahello:mavenimagestage \
  --file Dockerfile.02.mavenimagestage .
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


Prêt pour l’orchestration (Production-Ready)

Pour être orchestrée (Kubernetes, Compose), une image doit respecter des règles d’hygiène :

  1. Non-Root : Vos Dockerfiles utilisent un appuser. Cela empêche une faille applicative de compromettre l’hôte.
  2. Signaux (Exec Form) : L’usage de ENTRYPOINT ["/script.sh"] permet à la JVM de recevoir le SIGTERM pour un arrêt propre.
  3. Mémoire : L’usage de -XX:MaxRAMPercentage permet à 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   f74ad4d189a9   406MB     7 seconds ago
javahello    mavenimage        1dc734cacffb   928MB     27 seconds 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.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"]
docker image build --quiet \
  --tag javahello:dockercache \
  --file Dockerfile.03.dockercache .
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.

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.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"]
docker image build --quiet \
  --tag javahello:manual \
  --file Dockerfile.05.manual .
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


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


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.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


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

Points clés pour la mise en production

  • 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.