Conteneurs — Application à Java

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

Lecture
Containers
Docker
Optimisation, sécurité et bonnes pratiques pour la conteneurisation Java
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

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


ImportantAnalyse 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


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

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

Réutilisation