Contactez-nous

Ecriture d'un `Dockerfile` pour une application Spring Boot

Apprenez à créer des Dockerfiles optimisés pour vos applications Spring Boot, en utilisant les multi-stage builds, les layers Spring Boot et les bonnes pratiques de sécurité.

Pourquoi Dockeriser une application Spring Boot ?

Dockeriser une application Spring Boot consiste à l'empaqueter, avec son environnement d'exécution Java et toutes ses dépendances, dans une image de conteneur Docker. Cette image peut ensuite être utilisée pour créer des conteneurs isolés, portables et reproductibles sur n'importe quelle machine disposant de Docker.

Les avantages de cette approche sont nombreux :

  • Environnement cohérent : L'application s'exécute toujours dans le même environnement (OS, version de Java, dépendances), éliminant les problèmes du type "ça marche sur ma machine".
  • Portabilité : L'image peut être déployée facilement sur différents environnements (développement, test, production, cloud) sans modification.
  • Isolation : Les conteneurs isolent l'application des autres processus tournant sur la machine hôte.
  • Déploiement simplifié : Facilite l'intégration dans des pipelines CI/CD et l'orchestration avec des outils comme Kubernetes ou Docker Compose.
  • Scalabilité : Permet de démarrer rapidement de nouvelles instances de l'application pour répondre à la charge.

Le fichier `Dockerfile` est le plan utilisé par Docker pour construire cette image.

Structure d'un Dockerfile simple

Un `Dockerfile` minimal pour une application Spring Boot packagée en JAR exécutable pourrait ressembler à ceci :

# 1. Choisir une image de base Java (JRE suffit pour l'exécution)
FROM openjdk:17-jre-slim

# 2. Définir un répertoire de travail dans le conteneur
WORKDIR /app

# 3. Copier le fichier JAR de l'application dans le conteneur
# Remplacez 'target/mon-application-*.jar' par le chemin réel vers votre JAR
COPY target/mon-application-*.jar app.jar

# 4. Exposer le port sur lequel l'application écoute (par défaut 8080)
EXPOSE 8080

# 5. Commande pour lancer l'application au démarrage du conteneur
ENTRYPOINT ["java", "-jar", "app.jar"]

Ce `Dockerfile` est fonctionnel mais présente plusieurs inconvénients :

  • Il suppose que le fichier JAR a déjà été construit localement avant de lancer `docker build`.
  • Il crée une image potentiellement volumineuse car elle contient le JAR complet.
  • Il ne tire pas parti du cache de layers Docker de manière optimale.
  • Il exécute l'application en tant qu'utilisateur `root` par défaut, ce qui n'est pas idéal pour la sécurité.

Nous allons voir comment améliorer ce `Dockerfile` en appliquant des techniques plus avancées.

Optimisation 1 : Multi-stage builds

Le multi-stage build est une fonctionnalité essentielle pour créer des images Docker optimisées. Il permet d'utiliser une image pour construire l'application (avec le JDK, Maven ou Gradle) et une autre image, beaucoup plus légère (avec seulement le JRE), pour l'exécution finale. Seuls les artefacts nécessaires (le JAR) sont copiés de la phase de build à la phase d'exécution.

Voici un exemple avec Maven :

# ---- Stage 1: Build ----
# Utilise une image avec JDK et Maven pour construire l'application
FROM maven:3.8-openjdk-17 AS builder

# Définit le répertoire de travail
WORKDIR /build

# Copie les fichiers de description du projet (pom.xml)
COPY pom.xml .
# Télécharge les dépendances (profite du cache Docker si pom.xml ne change pas)
RUN mvn dependency:go-offline

# Copie le code source
COPY src ./src

# Construit l'application et crée le JAR
RUN mvn package -DskipTests

# ---- Stage 2: Runtime ----
# Utilise une image JRE légère pour l'exécution
FROM openjdk:17-jre-slim

WORKDIR /app

# Copie UNIQUEMENT le JAR construit depuis le stage 'builder'
COPY --from=builder /build/target/mon-application-*.jar app.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

Ce `Dockerfile` est bien meilleur :

  • Il gère la construction du JAR à l'intérieur du processus Docker.
  • L'image finale ne contient que le JRE et le JAR de l'application, pas le JDK, Maven, ni le code source.
  • Il tire un peu mieux parti du cache Docker pour les dépendances Maven.

Une approche similaire est possible avec Gradle.

Optimisation 2 : Utiliser les Spring Boot Layers pour le cache Docker

Depuis Spring Boot 2.3, le plugin Maven/Gradle peut créer des JAR "layerisés" (layered JARs). Le JAR est structuré en plusieurs couches distinctes (dépendances, ressources Spring Boot loader, ressources de l'application, code de l'application). Cette structure, combinée à un `Dockerfile` adapté, permet de maximiser l'utilisation du cache de layers Docker.

Etape 1 : Activer les layers dans le build (Maven) :



    
        
            org.springframework.boot
            spring-boot-maven-plugin
            
                
                    true
                
            
        
    

Etape 2 : Adapter le Dockerfile pour extraire et copier les layers :

# ---- Stage 1: Build (similaire à avant) ----
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
# S'assurer que le JAR est layerisé lors du build
RUN mvn package -DskipTests

# ---- Stage 2: Extract Layers ----
# Etape intermédiaire pour extraire les couches du JAR
FROM builder AS extractor
WORKDIR /app
# Copie le JAR depuis le stage précédent
COPY --from=builder /build/target/mon-application-*.jar app.jar
# Extrait les couches en utilisant le lanceur Spring Boot
RUN java -Djarmode=layertools -jar app.jar extract

# ---- Stage 3: Runtime ----
FROM openjdk:17-jre-slim
WORKDIR /app

# Copie les couches extraites DANS L'ORDRE (des moins fréquentes aux plus fréquentes)
COPY --from=extractor /app/dependencies/ ./ # Couche des dépendances externes
COPY --from=extractor /app/spring-boot-loader/ ./ # Couche du lanceur Spring
COPY --from=extractor /app/snapshot-dependencies/ ./ # Couche des dépendances SNAPSHOT (si applicable)
COPY --from=extractor /app/application/ ./ # Couche du code de l'application

EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
# Note: L'Entrypoint change car on lance via les classes extraites

Avec cette structure, si seule la couche `application` (votre code) change, Docker n'aura qu'à reconstruire cette dernière couche, réutilisant les couches précédentes (dépendances, etc.) depuis le cache. Cela accélère considérablement les builds lors du développement.

Bonne pratique : Exécuter en tant qu'utilisateur non-root

Exécuter des conteneurs en tant qu'utilisateur `root` est une mauvaise pratique de sécurité. Il est préférable de créer un utilisateur dédié avec des privilèges limités à l'intérieur du conteneur.

Intégrons cela dans notre `Dockerfile` multi-stage avec layers :

# ---- Stage 1: Build (inchangé) ----
FROM maven:3.8-openjdk-17 AS builder
# ... (build steps comme avant) ...

# ---- Stage 2: Extract Layers (inchangé) ----
FROM builder AS extractor
# ... (extraction steps comme avant) ...

# ---- Stage 3: Runtime ----
FROM openjdk:17-jre-slim
WORKDIR /app

# Créer un groupe et un utilisateur dédiés
RUN groupadd -r spring && useradd -r -g spring springuser

# Copier les couches extraites
COPY --from=extractor --chown=springuser:spring /app/dependencies/ ./
COPY --from=extractor --chown=springuser:spring /app/spring-boot-loader/ ./
COPY --from=extractor --chown=springuser:spring /app/snapshot-dependencies/ ./
COPY --from=extractor --chown=springuser:spring /app/application/ ./

# Changer l'utilisateur pour l'exécution
USER springuser

EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

Ici, nous avons ajouté :

  • RUN groupadd ... useradd ... pour créer le groupe et l'utilisateur `springuser`.
  • L'option --chown=springuser:spring lors des `COPY` pour que les fichiers appartiennent à notre nouvel utilisateur.
  • USER springuser pour indiquer à Docker d'exécuter les commandes suivantes (notamment l'ENTRYPOINT) avec cet utilisateur.

Construire et exécuter l'image

Pour construire l'image Docker en utilisant le `Dockerfile` placé à la racine de votre projet Spring Boot, exécutez la commande suivante depuis cette racine :

docker build -t mon-app-image:latest .
# '-t mon-app-image:latest' donne un nom et un tag à l'image
# '.' indique que le contexte de build est le répertoire courant

Une fois l'image construite, vous pouvez lancer un conteneur :

docker run -p 8080:8080 --name mon-conteneur mon-app-image:latest
# '-p 8080:8080' mappe le port 8080 de l'hôte au port 8080 du conteneur
# '--name mon-conteneur' donne un nom au conteneur en cours d'exécution

Vous pouvez passer des variables d'environnement Spring Boot (profils, configuration externe) à la commande `docker run` :

docker run -p 8080:8080 \
  -e "SPRING_PROFILES_ACTIVE=prod" \
  -e "DATABASE_URL=jdbc:postgresql://dbhost:5432/mydb" \
  --name mon-conteneur-prod mon-app-image:latest