Environnement de développement
Dans cette section, nous allons voir comment j’ai conçu un environnement de développement avec Docker. Pour cela, j’ai créée deux Dockerfile (un pour la partie api, un autre pour la partie client)et un docker-compose.yml pour gérer l’orchestration des conteneurs.
Dockerfile
Back-end
Voici le Dockerfile pour le back-end :
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY gradle/ gradle/COPY settings.gradle.kts build.gradle.kts gradlew ./
RUN ./gradlew dependencies --no-daemon
COPY src/ src/
EXPOSE 8080 35729
ENTRYPOINT ["./gradlew", "bootRun"]
Expliquons un peu ce qu’il se passe ici :
- Image de base :
On utilise l’image eclipse-temurin:21-jdk-alpine
, qui contient un JDK (Java Development Kit), nécessaire pour compiler et exécuter notre application Java. L’option alpine
est une version allégée de l’image JDK, ce qui réduit la taille finale de l’image, et la version 21 est exploitée ici puisqu’il s’agit de la dernière version stable de Java au moment de l’écriture de ce document.
- Répertoire de travail :
La commande WORKDIR
définit le répertoire de travail dans le conteneur. Toutes les commandes suivantes y seront exécutées. Ici, nous définissons /app
, qui est le répertoire racine de notre application.
- Copie des fichiers de configuration Gradle :
- gradle/ : contient les fichiers de configuration de Gradle.
- settings.gradle.kts, build.gradle.kts, gradlew : fichiers de configuration et script Gradle pour lancer les tâches.
Ces deux étapes auraient pu aussi bien s’écrire en plusieurs autres, comme ceci :
COPY gradle/ gradle/COPY settings.gradle.kts .COPY build.gradle.kts .COPY gradlew .
Le rendu est plus clair, mais la première méthode est plus optimisée puisqu’à chaque instruction COPY
, Docker crée une nouvelle couche dans l’image. Et plus il y a de couches, plus l’image est lourde.
- Téléchargement des dépendances :
La commande ./gradlew dependencies --no-daemon
télécharge les dépendances nécessaires à l’application. Le paramètre --no-daemon
empêche l’utilisation du démon Gradle, dont on a pas besoin ici. Cette étape est importante pour éviter de télécharger à nouveau les dépendances à chaque reconstruction de l’image .
- Copie du code source :
Ensuite, nous copions le répertoire src/
qui contient le code source de l’application, les fichiers Java, les ressources, etc.
- Exposition des ports :
On expose les ports 8080
et 35729
pour permettre la communication avec l’application. Le premier est celui de Spring Boot par défaut, et le second est utilisé pour le rechargement automatique des fichiers lors de modifications, grâce à un mécanisme de live-reload. Cependant, il est important de noter que l’exposition des ports dans le Dockerfile est sans effet sur la connectivité entre les conteneurs ou entre les conteneurs et l’hôte. Cela ne fait que documenter les ports utilisés par l’application .
- Commande d’entrée :
Enfin, la commande ENTRYPOINT
définit l’action à exécuter lorsque le conteneur démarre. Ici, nous exécutons ./gradlew bootRun
pour démarrer l’application Spring Boot. Cela permet de lancer l’application à partir du conteneur sans avoir besoin d’interaction supplémentaire.
Front-end
Voici le Dockerfile pour le front-end :
FROM node:22-alpine
WORKDIR /app
COPY package*.json angular.json tsconfig.json ./
RUN npm install
COPY . .
EXPOSE 4200
ENTRYPOINT ["npx", "ng", "serve", "--host", "0.0.0.0", "--poll", "1000"]
- Image de base :
On utilise l’image node:22-alpine
, qui contient Node.js et npm pour gérer les dépendances et exécuter l’application Angular. L’option alpine
est une version allégée de l’image Node.js, ce qui réduit la taille du conteneur.
- Répertoire de travail :
La commande WORKDIR
définit le répertoire de travail dans le conteneur. Toutes les commandes suivantes y seront exécutées. Ici, nous définissons /app
, qui est le répertoire racine de notre application.
-
Copie des fichiers de configuration :
- package.json : contient les dépendances de l’application.
- package-lock.json : verrouille les versions des dépendances.
- angular.json : fichier de configuration de l’application Angular.
- tsconfig.json : fichier de configuration de TypeScript.
-
Installation des dépendances :
La commande npm install
installe les dépendances définies dans le fichier package.json
nécessaires à l’application. Cette étape est importante pour éviter de télécharger à nouveau les dépendances à chaque reconstruction de l’image . Grâce au cache de Docker, si aucun changement n’est apporté au fichier package.json ou package-lock.json, l’installation des dépendances ne sera pas refaite.
- Copie du code source :
on copie tout le code source de l’application dans le conteneur à l’aide de la commande COPY . .
. Cette commande doit être exécutée après l’installation des dépendances pour éviter de les re-télécharger à chaque modification du code source.
- Exposition du port :
On expose le port 4200
pour permettre la communication avec l’application Angular. Cela correspond au port par défaut que le framework utilise pour servir l’application en mode développement. Cependant, il est important de noter que l’exposition des ports dans le Dockerfile est sans effet sur la connectivité entre les conteneurs ou entre les conteneurs et l’hôte. Cela ne fait que documenter les ports utilisés par l’application .
- Commande d’entrée :
Enfin, la commande ENTRYPOINT
définit l’action à exécuter lorsque le conteneur démarre. Ici, on exécute npx ng serve --host 0.0.0.0 --poll 1000
pour démarrer l’application Angular. Cela permet de lancer l’application à partir du conteneur sans avoir besoin d’interaction supplémentaire.
Plus en détail, la commande est composée comme suit :
npx
: permet d’exécuter des commandes npm sans les installer globalement.ng serve
: commande pour lancer le serveur de développement Angular.--host 0.0.0.0
: permet de rendre le serveur accessible depuis l’extérieur du conteneur.--poll 1000
: permet de surveiller les fichiers pour détecter les modifications et recharger la page automatiquement. La valeur1000
représente le délai entre chaque vérification des fichiers (en millisecondes).
Docker Compose
Pour orchestrer les conteneurs, j’ai créé un fichier docker-compose-dev.yml
à la racine du projet :
services:
database: container_name: db image: postgres:17 restart: unless-stopped environment: POSTGRES_DB: quizine POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres TZ: Europe/Paris volumes: - postgres_data:/var/lib/postgresql/data ports: - "5433:5432"
api: container_name: api build: context: ./api dockerfile: Dockerfile-dev restart: unless-stopped depends_on: - database volumes: - ./api:/app - gradle_cache:/root/.gradle ports: - "8080:8080" - "35729:35729" environment: SPRING_DATASOURCE_URL: jdbc:postgresql://database:5432/quizine SPRING_DATASOURCE_USERNAME: postgres SPRING_DATASOURCE_PASSWORD: postgres SPRING_PROFILES_ACTIVE: dev TZ: Europe/Paris
client: container_name: client build: context: ./client dockerfile: Dockerfile-dev restart: unless-stopped depends_on: - api volumes: - ./client:/app - node_cache:/app/node_modules ports: - "4200:4200" environment: CHOKIDAR_INTERVAL: 1000 CHOKIDAR_USEPOLLING: true TZ: Europe/Paris
volumes: postgres_data: gradle_cache: node_cache:
Ce fichier définit trois services :
- database : conteneur PostgreSQL pour la base de données.
- api : conteneur pour l’application back-end Spring Boot.
- client : conteneur pour l’application front-end Angular.
Chaque service a ses propres configurations, telles que le nom du conteneur, l’image à utiliser, les dépendances, les volumes, les ports, les variables d’environnement, etc.
Détaillons un peu tout ça !
Base de données
database: container_name: db image: postgres:17 restart: unless-stopped environment: POSTGRES_DB: quizine POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres TZ: Europe/Paris volumes: - postgres_data:/var/lib/postgresql/data ports: - "5433:5432"
Le service database utilise l’image postgres:17
pour créer un conteneur PostgreSQL.
Voici les configurations principales :
container_name
: nom du conteneur.image
: image à utiliser pour le conteneur.restart
: politique de redémarrage du conteneur.environment
: variables d’environnement pour la base de données.volumes
: volumes pour stocker les données de la base de données.ports
: ports à exposer pour la communication avec la base de données.
Le conteneur reçoit un nom très simple db
pour le distinguer des autres conteneurs plus facilement.
L’image postgres:17
est basée sur l’image officielle PostgreSQL disponible sur le Docker Hub. On n’utilise pas la version alpine
ici, car elle ne supporte pas les locales nécessaires pour la configuration de la base de données, ce qui peut poser des problèmes lors de son initialisation ou des incohérences avec des caractères spéciaux ou des langues spécifiques.
J’utilise la version 17 puisque c’est la dernière version stable de PostgreSQL au moment de l’écriture de ce document.
La politique de redémarrage unless-stopped
garantit que le conteneur redémarre automatiquement après un crash ou un redémarrage de l’hôte, sauf s’il a été arrêté manuellement.
Les variables d’environnement POSTGRES_DB
, POSTGRES_USER
et POSTGRES_PASSWORD
permettent de définir respectivement le nom de la base de données, l’utilisateur et le mot de passe. La variable TZ
définit le fuseau horaire pour la base de données.
Le volume nommé postgres_data
est utilisé pour stocker les données de la base de données de manière persistante. Cela permet de conserver les données même si le conteneur est supprimé ou redémarré. Le volume est monté dans le répertoire /var/lib/postgresql/data
du conteneur, comme indiqué dans la documentation de l’image PostgreSQL sur le Docker Hub.
Enfin, le port 5432
du conteneur est exposé sur le port 5433
de la machine hôte pour permettre la communication avec la base de données. Cela permet d’accéder à la base de données depuis l’extérieur du conteneur. Le port 5432
est le port par défaut de PostgreSQL, mais sur ma machine, il est exceptionellement placé sur le port 5433
.
API
api: container_name: api build: context: ./api dockerfile: Dockerfile-dev restart: unless-stopped depends_on: - database volumes: - ./api:/app - gradle_cache:/root/.gradle ports: - "8080:8080" - "35729:35729" environment: SPRING_DATASOURCE_URL: jdbc:postgresql://database:5432/quizine SPRING_DATASOURCE_USERNAME: postgres SPRING_DATASOURCE_PASSWORD: postgres SPRING_PROFILES_ACTIVE: dev TZ: Europe/Paris
Le service api utilise un Dockerfile spécifique pour construire l’image du conteneur. Le fichier Dockerfile-dev
se trouve dans le répertoire api/
du projet.
Voici les configurations principales :
container_name
: nom du conteneur.build
: définition du contexte et du fichier Dockerfile pour construire l’image.restart
: politique de redémarrage du conteneur.depends_on
: dépendance du service par rapport à un autre service.volumes
: volumes pour monter les fichiers de l’application et le cache de Gradle.ports
: ports à exposer pour la communication avec l’application.environment
: variables d’environnement pour l’application.
Le conteneur reçoit un nom api
pour le distinguer des autres conteneurs plus facilement.
Comme il s’agit d’une image personnalisée, nous devons définir le contexte et le fichier Dockerfile à utiliser pour construire l’image. Le contexte est le répertoire dans lequel se trouve le Dockerfile que l’on souhaite utiliser. Ici, le contexte est ./api
et le Dockerfile est Dockerfile-dev
.
La politique de redémarrage unless-stopped
garantit que le conteneur redémarre automatiquement après un crash ou un redémarrage de l’hôte, sauf s’il a été arrêté manuellement.
Le service api dépend du service database pour garantir que la base de données est lancée avant de démarrer l’application. Cela permet d’éviter les problèmes de connexion à la base de données lors du démarrage de l’application.
Les volumes ./api:/app
et gradle_cache:/root/.gradle
sont utilisés pour monter respectivement les fichiers de l’application et le cache de Gradle dans le conteneur. Le premier volume permet de synchroniser les fichiers de l’application entre l’hôte et le conteneur, ce qui sous entends que les modifications apportées au code source sur l’hôte sont immédiatement prises en compte dans le conteneur . Tandis que le second volume permet de conserver les dépendances téléchargées par Gradle entre les reconstructions de l’image, ce qui constitue un gain de temps et de performances considérable.
Les ports 8080
et 35729
du conteneur sont exposés sur les mêmes ports de la machine hôte pour permettre la communication avec l’application Spring Boot. Le port 8080
est celui par défaut de Spring Boot, tandis que le 35729
est utilisé pour le rechargement automatique des fichiers lors de modifications, grâce à un mécanisme de live-reload.
Les variables d’environnement SPRING_DATASOURCE_URL
, SPRING_DATASOURCE_USERNAME
et SPRING_DATASOURCE_PASSWORD
permettent de définir respectivement l’URL de connexion à la base de données, le nom d’utilisateur et le mot de passe. Il s’agit des mêmes valeurs que celles définies pour le service database.
La variable SPRING_PROFILES_ACTIVE
définit le profil actif de l’application, ici dev
pour l’environnement de développement. Cette mécanique permet de charger les configurations spécifiques à l’environnement de développement, comme les propriétés de connexion à la base de données, les logs, etc.
La variable TZ
définit le fuseau horaire pour l’application.
Client
client: container_name: client build: context: ./client dockerfile: Dockerfile-dev restart: unless-stopped depends_on: - api volumes: - ./client:/app - node_cache:/app/node_modules ports: - "4200:4200" environment: CHOKIDAR_INTERVAL: 1000 CHOKIDAR_USEPOLLING: true TZ: Europe/Paris
Le service client utilise également un Dockerfile spécifique pour construire l’image du conteneur. Le fichier Dockerfile-dev
se trouve dans le répertoire client/
du projet.
Voici les configurations principales :
container_name
: nom du conteneur.build
: définition du contexte et du fichier Dockerfile pour construire l’image.restart
: politique de redémarrage du conteneur.depends_on
: dépendance du service par rapport à un autre service.volumes
: volumes pour monter les fichiers de l’application et le cache de Node.js.ports
: ports à exposer pour la communication avec l’application.environment
: variables d’environnement pour l’application.
Le conteneur reçoit un nom client
pour le distinguer des autres conteneurs plus facilement.
Comme pour le service api, nous devons définir le contexte et le fichier Dockerfile à utiliser pour construire l’image. Le contexte est le répertoire dans lequel se trouve le Dockerfile que l’on souhaite utiliser. Ici, le contexte est ./client
et le Dockerfile est Dockerfile-dev
.
La politique de redémarrage unless-stopped
garantit que le conteneur redémarre automatiquement après un crash ou un redémarrage de l’hôte, sauf s’il a été arrêté manuellement.
Le service client dépend du service api pour garantir que l’application back-end est lancée avant de démarrer l’application front-end.
Les volumes ./client:/app
et node_cache:/app/node_modules
sont utilisés pour monter respectivement les fichiers de l’application et le cache de Node.js dans le conteneur. Le premier volume permet de synchroniser les fichiers de l’application entre l’hôte et le conteneur, ce qui sous entends que les modifications apportées au code source sur l’hôte sont immédiatement prises en compte dans le conteneur . Tandis que le second volume permet de conserver les dépendances téléchargées par Node.js entre les reconstructions de l’image, ce qui constitue un gain de temps et de performances considérable.
Le port 4200
du conteneur est exposé sur le même port de la machine hôte pour permettre la communication avec l’application Angular. Cela correspond au port par défaut que le framework utilise pour servir l’application en mode développement.
Les variables d’environnement CHOKIDAR_INTERVAL
et CHOKIDAR_USEPOLLING
permettent de configurer le mécanisme de live-reload pour détecter les modifications des fichiers et recharger la page automatiquement. Plus précisément, CHOKIDAR_INTERVAL
définit le délai entre chaque vérification des fichiers (en millisecondes), tandis que CHOKIDAR_USEPOLLING
active le mode de polling pour surveiller les fichiers.
La variable TZ
définit le fuseau horaire pour l’application.
Volumes
volumes: postgres_data: gradle_cache: node_cache:
Enfin, la section volumes
définit les volumes utilisés par les services pour stocker les données de manière persistante. Les volumes sont créés automatiquement par Docker lors du lancement des conteneurs s’ils n’existent pas déjà. Ils permettent de conserver les données même si les conteneurs sont supprimés ou redémarrés.
postgres_data
: volume pour stocker les données de la base de données PostgreSQL.gradle_cache
: volume pour stocker le cache de Gradle, donc les dépendances de l’api.node_cache
: volume pour stocker le cache de Node.js, donc les dépendances du client.
Dans le cadre de volume nommé, il faut les déclarer ainsi pour qu’ils soient persistants. Si on ne les déclare pas, les données seront stockées dans un volume anonyme qui sera supprimé lors de l’arrêt du conteneur.