Aller au contenu

Dockeriser une application Spring Boot avec Tomcat

29 septembre 2023

Kulwinder Billen

Résumé : Étape 1. Création du Dockerfile, Étape 2. Mission accomplie!

Mon équipe voulait rendre son API REST Spring Boot compatible avec Docker pour déployer l’image Docker sur Kubernetes. Nous voulions qu’elle soit évolutive horizontalement, portable et facilement gérée par une solution d’orchestration de conteneurs. Ajoutez tous les mots à la mode que j’aurais pu oublier.

Alors, prêt à dockeriser une application Spring Boot? C’est parti!

Au programme :

  • Création de votre premier Dockerfile (si applicable)
  • Exploitation de builds multi-étapes dans votre Dockerfile
  • Configuration de Tomcat pour exécuter votre application
  • Configuration de volumes locaux pour tester votre nouvelle image
  • Lancement en local de votre conteneur Docker!
  • Déploiement

Avant de plonger dans l’univers de Docker, voici ce dont vous avez besoin pour dockeriser votre application :

  • Le code source de l’application Java Spring Boot
  • Maven, un outil d’automatisation de build qui vous permettra de construire votre projet

Généralement, les applications Spring Boot sont déployées sur une machine virtuelle dotée de Tomcat. Le fichier WAR (généré par Maven) est ensuite transféré à l’emplacement approprié et le serveur Tomcat est activé.

Au départ, nous pensions simplement intégrer le fichier WAR généré par nos outils d’intégration continue (CI) et construire la partie Tomcat dans notre image Docker.

Finalement, nous avons privilégié une approche où tout se fait en une seule opération. Cette approche simplifiée élimine la nécessité de configurer un emplacement spécifique pour les fichiers WAR et de configurer l’image Docker pour les récupérer. Tout ça grâce aux builds multi-étapes.

Phase de construction

Création de votre premier Dockerfile

Il existe bien sûr une tonne de ressources à ce sujet sur Internet. Mais vous êtes ici pour apprendre à dockeriser une application Spring Boot, n’est-ce pas? Si vous n’avez jamais touché à un Dockerfile, soyez rassurés, je vais faire un petit tour d’horizon pour vous mettre à niveau.

Les Dockerfiles se basent sur une série de commandes clés pour assembler des images. Je vais me concentrer exclusivement sur celles que nous allons utiliser.

FROM  - Base image that you want to start off with. 
A Dockerfile needs to start with this. (or can only be preceded by ARG - 
in case you want to pass in a version to the FROM tag)

LABEL - This allows you to add labels to your image. (eg. Maintainer name and email)

ADD - This copies files from the host machine to the container filesystem. 
This includes handling of tar and URL handling. 
However, COPY is preferred for moving files between the host and container file system. 
The only reason being that it might do some added steps such as decompress your 
tar automatically, etc. It can still definitely be used as long as you know what it will do.

COPY - Same as ADD, but without the tar and remote url handling

WORKDIR - Sets the working directory for the RUN, CMD, ENTRYPOINT, COPY and ADD 
instructions that are specified after it is used. 
You can run this instruction multiple times within the Dockerfile and the commands 
under it will then use the latest directory specified by WORKDIR.

ARG - These are arguments you might want to pass in during build-time only. 
Such as version of the base image, or perhaps the path of the war file you might want to 
pass in.

ENV - Sets environment variables within the docker container. 
The cool thing about this is that they can be set during runtime from the command line. 

EXPOSE - Essentially lets the docker container know to enable networking for this port 
as it will likely be used by the outside world.

RUN - We use this to run commands during the build phase to add layers onto the image itself. 
Something that you want the image to have. (Eg. Your application code or some form of it)

CMD - We use this to run a command at the end that we always want the image to run. 
(*Note: All CMD specified before are ignored except the last one* ). 
Allows you to override the parameters when you run the docker container via command line.

ENTRYPOINT - Similar to CMD, but doesn't allow you to override the command and parameters 
via the command line.

Maintenant que vous êtes à l’aise avec les instructions de base, nous allons les combiner pour créer un Dockerfile.

Maven et les builds multi-étapes

Les images Docker peuvent vite devenir volumineuses, et nous avons toujours intérêt à les alléger au maximum. Avant, la norme était de créer deux Dockerfiles, un pour le build (Dockerfile.build), et un pour le déploiement de l’application en production (Dockerfile).

Heureusement, grâce aux builds multi-étapes, cette époque est révolue.

Comme annoncé, nous allons compiler le code source avec Maven, notre outil de build préféré, avant d’exécuter l’application avec un serveur web comme Tomcat.

FROM maven:3.6.3 as maven
LABEL COMPANY="ShuttleOps"
LABEL MAINTAINER="support@shuttleops.com"
LABEL APPLICATION="Sample Application"

Comme vous pouvez le voir, nous utilisons les mots-clés FROM et LABEL. FROM initialise le build à partir d’une image de base. Dans notre cas, il s’agit de l’image de Maven (version 3.6.3). Notez bien le as maven, qui nous sera utile pour référencer cette étape (maven) plus tard.

LABEL nous permet de tagger l’image avec des paires clé/valeur. Dans cet exemple, nous indiquons le nom de l’entreprise, l’adresse courriel du mainteneur, et le nom de l’application.

WORKDIR /usr/src/app
COPY . /usr/src/app
RUN mvn package 

Ensuite, nous utilisons WORKDIR, COPY et RUN.

WORKDIR définit le répertoire de travail à partir duquel s’exécutent toutes les autres instructions. S’il n’existe pas, Docker le crée pour vous. Ici, nous avons choisi la destination usr/src/app pour nos fichiers.

Les choses sérieuses commencent avec COPY, qui sert à transférer les fichiers source depuis la machine hôte – dans ce cas, le répertoire contenant le Dockerfile. Le point « . » précise que les fichiers doivent être copiés à partir de la racine du « contexte de build », à fournir avec la commande docker build. Dans notre cas, nous indiquons le répertoire qui contient le Dockerfile.

Une fois les fichiers copiés dans /usr/src/app, nous pouvons commencer à construire le projet avec Maven. En plus d’empaqueter l’application dans un fichier WAR, la commande mvn package réalise aussi des tests. Tout ça avec une seule commande de CI, pas mal, non?

Note : Assurez-vous que tous les prérequis sont disponibles, sinon les tests échoueront. Si vous effectuez des tests dans d’autres parties de votre CI, vous pouvez les ignorer avec mvn -Dmaven.test.skip=true package.

Comme nous l’avons mentionné, RUN ajoute une nouvelle couche à l’image Docker, et c’est là que notre fichier WAR sera stocké. Il nous servira lors de la seconde étape de construction avec Tomcat. Une fois la commande mvn package lancée, un répertoire /usr/src/app/target contenant notre fichier WAR verra le jour.

Tomcat et l’aspect « multi » des builds multi-étapes

C’est ici que les builds multi-étapes entrent en jeu! Nous allons partir d’une image Tomcat et configurer les dossiers nécessaires pour faire tourner notre application.

Pour rappel, le signe dièse (#) marque les commentaires dans votre Dockerfile.

FROM tomcat:8.5-jdk15-openjdk-oracle
ARG TOMCAT_FILE_PATH=/docker 
	
#Data & Config - Persistent Mount Point
ENV APP_DATA_FOLDER=/var/lib/SampleApp
ENV SAMPLE_APP_CONFIG=${APP_DATA_FOLDER}/config/
	
ENV CATALINA_OPTS="-Xms1024m -Xmx4096m -XX:MetaspaceSize=512m -	XX:MaxMetaspaceSize=512m -Xss512k"

Cette section du Dockerfile contient quelques éléments intéressants.

On repart avec FROM pour lancer la deuxième étape. En gros, le Dockerfile doit redémarrer « de zéro » avec une nouvelle image : tomcat:8.5-jdk15-openjdk-oracle.

Ensuite, nous utilisons le mot-clé ARG pour indiquer à Docker l’emplacement des fichiers de configuration Tomcat à intégrer à l’image. Attention, les arguments ARG sont seulement pris en compte pendant la phase de construction. Le chemin par défaut /docker dit à Docker de fouiller dans ce dossier du contexte de build sur la machine hôte.

On continue avec ENV pour les variables d’environnement. Elles peuvent être modifiées pendant l’exécution, mais nous avons aussi des valeurs par défaut. Notre application utilise SAMPLE_APP_CONFIG pour localiser les fichiers de configuration, un peu comme vous le feriez avec la variable PATH sur votre machine hôte.

La dernière variable d’environnement à configurer est CATALINA_OPTS, que Tomcat utilise pour définir les seuils de mémoire, l’espace pour les métadonnées, ainsi que la taille de la pile du processus.

Il ne reste plus qu’à déplacer le fichier WAR là où il pourra être exécuté!

#Move over the War file from previous build step
WORKDIR /usr/local/tomcat/webapps/
COPY --from=maven /usr/src/app/target/SampleApp.war /usr/local/tomcat/webapps/api.war

COPY ${TOMCAT_FILE_PATH}/* ${CATALINA_HOME}/conf/

WORKDIR $APP_DATA_FOLDER

EXPOSE 8080
ENTRYPOINT ["catalina.sh", "run"]

Encore une fois, allons-y étape par étape. Nous avons utilisé le mot-clé WORKDIR pour définir le répertoire de travail dans le dossier webapps de Tomcat. C’est ici qu’on va transférer le fichier WAR créé à la première étape du build.

Penchons-nous un peu sur cette commande COPY :

COPY --from=maven /usr/src/app/target/SampleApp.war /usr/local/tomcat/webapps/api.war

Notez l’option --from=maven, mentionnée à la première étape du build. Rappelez-vous, nous avons nommé cette étape avec FROM (FROM maven:3.6.3 as maven). Grâce à ce as maven, nous pouvons l’utiliser comme référence et accéder aux fichiers générés à cette étape. Dans notre cas, il s’agit du fichier WAR enregistré dans /usr/src/app/target/, que nous allons déplacer dans le dossier /usr/local/tomcat/webapps/ et renommer api.war.

Note : Avec ce nouveau nom api.war, l’application sera maintenant accessible… disons à localhost:8080/api au lieu de localhost:8080/.

Ensuite, il faut transférer quelques fichiers de configuration dans le dossier Tomcat correspondant grâce à COPY, et ancrer le répertoire de travail dans le dossier de données.

On arrive à la commande principale que l’image Docker doit exécuter lorsqu’elle démarre un conteneur.

EXPOSE désigne le port pour lequel Docker doit activer la communication réseau et signale qu’il y aura du trafic à cet endroit.

Enfin, ENTRYPOINT déclenche catalina.sh run pour démarrer Tomcat et déployer notre application, la rendant opérationnelle.

Voici à quoi ressemble notre Dockerfile :

FROM maven:3.6.3 as maven
LABEL COMPANY="ShuttleOps"
LABEL MAINTAINER="support@shuttleops.com"
LABEL APPLICATION="Sample Application"

WORKDIR /usr/src/app
COPY . /usr/src/app
RUN mvn package 

FROM tomcat:8.5-jdk15-openjdk-oracle
ARG TOMCAT_FILE_PATH=/docker 
	
#Data & Config - Persistent Mount Point
ENV APP_DATA_FOLDER=/var/lib/SampleApp
ENV SAMPLE_APP_CONFIG=${APP_DATA_FOLDER}/config/
	
ENV CATALINA_OPTS="-Xms1024m -Xmx4096m -XX:MetaspaceSize=512m -	XX:MaxMetaspaceSize=512m -Xss512k"

#Move over the War file from previous build step
WORKDIR /usr/local/tomcat/webapps/
COPY --from=maven /usr/src/app/target/SampleApp.war /usr/local/tomcat/webapps/api.war

COPY ${TOMCAT_FILE_PATH}/* ${CATALINA_HOME}/conf/

WORKDIR $APP_DATA_FOLDER

EXPOSE 8080
ENTRYPOINT ["catalina.sh", "run"]

Pour lancer la construction de l’image Docker, placez le fichier – nommé Dockerfile – à la racine du répertoire contenant le fichier pom.xml et exécutez la commande suivante.

docker build -t kbillen92/sample-api:latest .

L’option -t sert à tagger le build avec namespace/name_of_application:tag. Le point final « . » indique que le contexte de build et le Dockerfile se trouvent dans le répertoire courant.

Une fois l’image construite, vous pouvez la visualiser avec la commande docker images.

Lancement

Félicitations! Vous êtes (presque) prêt·e à lancer votre image Docker Spring Boot!

Je dis « presque », car la plupart des applications ont besoin de stocker des données de façon persistante lorsque le conteneur Docker n’est pas en cours d’exécution, qu’il s’agisse d’images, de documents ou divers types de données. Pour cela, il faut mettre en place un volume.

Vous pouvez passer cette étape si votre application n’en a pas besoin.

Configuration du volume

Exécutez la commande suivante pour créer le volume :

docker volume create --driver local --opt device=/d/DockerVolumes/sampleVolume --opt type=none --opt o=bind sampleVolume

Note : Assurez-vous que le chemin de dossier existe déjà, sinon la commande échouera.

Tout est prêt!

Lancement en local

Maintenant que nous avons construit notre image et créé un volume persistant (si applicable), il est temps de lancer le conteneur.

docker run -d --mount source=sampleVolume,target=/var/lib/SampleApp -p 8080:8080 kbillen92/sample-api:latest

Exécutez docker ps pour vérifier que tout fonctionne comme prévu.

Si vous souhaitez accéder au terminal bash à l’intérieur du conteneur, exécutez la commande suivante : docker exec -it <container_id or name> /bin/bash

​ ​BRAVO, VOTRE APPLICATION SPRING BOOT EST OPÉRATIONNELLE!!

La dernière étape consiste à pousser cette image vers votre dépôt Docker. Si vous avez un compte DockerHub, voici les commandes nécessaires :

docker login -u <username> -p <password> pour vous connecter
docker push kbillen92/sample-api:latest pour pousser l’image vers DockerHub

Note : Cette commande rendra votre image publique, à moins que vous ayez configuré un dépôt privé au préalable.

Déploiement

Vous avez fait des merveilles avec Docker, mais vous devez encore déployer votre conteneur. Pour cela, vous allez probablement utiliser Kubernetes, la plateforme de gestion de conteneurs du moment.

VOUS AVEZ UN PROJET ?