Skip to content

Découvrir Docker

Objectifs

Bonnes pratiques

Tout au long du TP vous allez créer des fichiers liés a docker ou docker-compose ou liés a du code que nous allons utiliser. Pour vous y retrouver, créez une structure de fichiers appropriée, 1 dossier par image buildée par exemple.

Application cible

Application trois tiers:

  • Serveur HTTP (simple revers proxy dans un premier temps)
  • API Backend
  • Base de données

Pour chacune de ces applications, nous suivrons le même processus : choisir l'image de base Docker appropriée, créer et configurer cette image, ajouter les spécificités de notre application et, à un moment donné, la faire fonctionner. Notre objectif final est d'avoir une application trois tiers qui tourne.

Images de base

Ces images seront celles utilisées comme point de départ pour builder vos images custom qui feront tourner votre application.

Pour éviter le rate limit de Docker Hub nous vous fournissons ces même image dans notre registry public :

Base de données

Fondamentaux

Nous utiliserons l'image : postgres:14.1-alpine.

Faisons fonctionner un simple serveur PostgreSQL. Voici ce qui serait un Dockerfile minimal :

FROM registry.takima.io/school/proxy/postgres:14.1-alpine

ENV POSTGRES_DB=db \
   POSTGRES_USER=usr \
   POSTGRES_PASSWORD=pwd

Construisez cette image et démarrez un conteneur correctement, vous devriez pouvoir accéder à votre base de données en fonction du port de publication que vous choisissez : localhost:PORT.

Note

Ici les variable ENV sont indiquer pour être prise par défault. Il est ensuite conseiller de les override lors du lancement du conteneur avec l'option -e

Votre base de données PostgreSQL devrait être opérationnelle. Connectez-vous à votre base de données et vérifiez que tout fonctionne correctement. N'oubliez pas de nommer votre image Docker et votre conteneur.

Tip

Si vous avez des difficultés, revenez à la partie Construire l'image et Exécuter votre image sur TD01 - Docker (TD 1 Découverte de Docker).

Relancez votre base de données et Adminer avec --network app-network pour permettre la communication Adminer/Base de données. Nous utilisons -–network pour placer adminer et postgres dans le même réseau, au lieu de -–link car ce dernier est obsolète.

Tip

N'oubliez pas de créer votre réseau

docker network create app-network
Si vous n'avez pas placé le conteneur dans le bon réseau vous pouvez le connecter avec docker network connect my_net my_container

Est-il correct d'avoir des mots de passe écrits en clair dans un fichier ? Vous pouvez plutôt définir ces paramètres d'environnement lors de l'exécution de l'image en utilisant l'option -e.

Tip

Pourquoi exécutons-nous le conteneur avec une option -e pour fournir les variables d'environnement ?

Initialisation de la base de données

parfois il peut être pratique d'avoir notre structure de base de données initialisée à l'intérieur même de l'image Docker ainsi que quelques données initiales. Tout script SQL trouvé dans /docker-entrypoint-initdb.d sera exécuté dans l'ordre alphabétique, ajoutons donc quelques scripts à notre image :

01-CreateScheme.sql

CREATE TABLE public.departments
(
 id      SERIAL      PRIMARY KEY,
 name    VARCHAR(20) NOT NULL
);

CREATE TABLE public.students
(
 id              SERIAL      PRIMARY KEY,
 department_id   INT         NOT NULL REFERENCES departments (id),
 first_name      VARCHAR(20) NOT NULL,
 last_name       VARCHAR(20) NOT NULL
);

02-InsertData.sql

INSERT INTO departments (name) VALUES ('IT');
INSERT INTO departments (name) VALUES ('RH');
INSERT INTO departments (name) VALUES ('Administration');


INSERT INTO students (department_id, first_name, last_name) VALUES (1, 'Eli', 'Copter');
INSERT INTO students (department_id, first_name, last_name) VALUES (2, 'Emma', 'Carena');
INSERT INTO students (department_id, first_name, last_name) VALUES (2, 'Jack', 'Uzzi');
INSERT INTO students (department_id, first_name, last_name) VALUES (3, 'Aude', 'Javel');

Modifiez votre dockerfile en ajoutant ces scripts dans /docker-entrypoint-initdb.d, reconstruisez votre image et vérifiez que vos scripts ont été exécutés au démarrage et que les données sont présentes dans votre conteneur.

Tip

Lorsque nous parlons de /docker-entrypoint-initdb.d, il s'agit de l'intérieur du conteneur, vous devez donc copier le contenu de votre répertoire dans le répertoire du conteneur.

Tip

N'oubliez pas de lancer adminer :

    docker run \
    -p "8090:8080" \
    --net=app-network \
    --name=adminer \
    -d \
    registry.takima.io/school/proxy/adminer

Persistez les données

Vous avez peut-être remarqué, en effctuant des modifications de données sur votre base avec adminer, que si votre conteneur de base de données est détruit, toutes vos données sont réinitialisées. Une base de données doit persister durablement les données : c'est sont rôle. Utilisez des volumes pour persister les données sur le disque de l'hôte.

-v /my/own/datadir:/var/lib/postgresql/data

Vérifiez que les données survivent lorsque votre conteneur est détruit.

Question

Pourquoi avons-nous besoin d'un volume attaché à notre conteneur PostgreSQL ?

API Backend

Fondamentaux

Pour commencer, nous allons simplement exécuter une classe Java "Hello World" dans nos conteneurs, et seulement après nous exécuterons un fichier JAR. Dans les deux cas, choisissez l'image appropriée en gardant à l'esprit que nous n'avons besoin que d'un environnement d'exécution Java.

Voici une implémentation de Hello World en Java :

Main.java

public class Main {

   public

 static void main(String[] args) {
       System.out.println("Hello World!");
   }
}

1- Compilez avec votre version cible de Java : javac Main.java. 2- Rédigez le Dockerfile.

FROM   # TODO: Choisissez une version de Java JRE
# TODO:  Ajoutez le code Java (bytecode, .class)
# TODO: Exécutez le code Java avec la commande : "java Main"

3- Maintenant, pour lancer l'application, vous devez faire la même chose que dans l'étape de base 1.

Ici, vous avez un premier aperçu de votre application Backend.

Dans l'étape suivante, nous enrichirons le build (en utilisant Maven au lieu de javac minimaliste) et exécuterons un fichier JAR au lieu d'un simple .class.

→ Si cela réussit, vous devriez voir "Hello World" dans votre console.

Build multistage

Dans la section précédente, nous avons construit du code Java sur notre machine pour le faire fonctionner dans un conteneur Docker. Ne serait-il pas formidable que Docker gère également le build ? Vous avez probablement remarqué que les images Docker openjdk par défaut contiennent... Eh bien... un JDK ! Le problème du JDK c'est qu'il contient beaucoup de librairy et d'outil qui ne sont pas nécéssaire pour le run. Pour le run on préfère avoir un JRE qui est bien moins lourd. Or rappelons que le but des images Docker est d'être le plus léger possible. Cela tombe bien le multistage build sert à cela : utiliser une image pour builder (JDK par exemple), puis transmettre le build dans une autre image qui servira au run (JRE par exemple).

Créez un build multistage en utilisant le multistage.

Votre Dockerfile devrait ressembler à ceci :

FROM registry.takima.io/school/proxy/openjdk:17
# Construire Main.java
# TODO : dans les prochaines étapes (pas maintenant)

FROM registry.takima.io/school/proxy/eclipse-temurin:17-jre-alpine
# Copiez les ressources de l'étape précédente
COPY --from=0 /usr/src/Main.class .
# Exécutez le code Java avec le JRE
# TODO : dans les prochaines étapes (pas maintenant)

Ne remplissez pas le Dockerfile maintenant, nous devrons le faire dans les prochaines étapes.

API Backend simple

Nous déploierons une application Spring Boot fournissant une API simple avec un seul point d'accès de publication.

Créez votre application Spring Boot sur : Spring Initializer.

Utilisez la configuration suivante :

  • Projet : Maven
  • Langage : Java 17
  • Spring Boot : 2.7.5
  • Emballage : Jar
  • Dépendances : Spring Web

Générez le projet et créez une classe GreetingController simple :

package fr.takima.training.simpleapi.controller;

import org.springframework.web.bind.annotation.*;

import java.util.concurrent.atomic.AtomicLong;

@RestController
public class GreetingController {

   private static final String template = "Hello, %s!";
   private final AtomicLong counter = new AtomicLong();

   @GetMapping("/")
   public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
       return new Greeting(counter.incrementAndGet(), String.format(template, name));
   }

   class Greeting {

       private final long id;
       private final String content;

       public Greeting(long id, String content) {
           this.id = id;
           this.content = content;
       }

       public long getId() {
           return id;
       }

       public String getContent() {
           return content;
       }
   }
}

Vous pouvez maintenant construire et démarrer votre application. Bien sûr, vous aurez besoin de Maven et d'un JDK-17.

Que diriez-vous d'avoir un conteneur pour construire et exécuter notre API simpliste ?

Oh, attendez, nous avons Docker, voici comment vous pourriez construire et exécuter votre application avec Docker :

# Build
FROM registry.takima.io/school/proxy/maven:3.8.5-openjdk-17-slim AS myapp-build
ENV MYAPP_HOME /opt/myapp
WORKDIR $MYAPP_HOME
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests

# Run
FROM registry.takima.io/school/proxy/openjdk:17-slim
ENV MYAPP_HOME /opt/myapp
WORKDIR $MYAPP_HOME
COPY --from=myapp-build $MYAPP_HOME/target/*.jar $MYAPP_HOME/myapp.jar

ENTRYPOINT java -jar myapp.jar

Question

Pourquoi avons-nous besoin d'un build multistage ? Essayez de comprendre chaque étape de ce dockerfile

Check

Une application Spring Boot simple avec un point de terminaison HelloWorld qui fonctionne.

Avez-vous remarqué que Maven télécharge toutes les bibliothèques à chaque build de l'image ? Vous pouvez contribuer à économiser de l'énergie et du temps en mettant en cache les bibliothèques lorsque le fichier pom de Maven n'a pas été modifié en exécutant la commande : mvn dependency:go-offline.

API Backend 3-tiers

Nous avons vu comment builder et lancer du code Java. Passons maintenant au build et à l'exécution de l'API qui nous intéresse pour notre application 3-tiers. Vous pouvez obtenir le code source compressé ici : simple-api.

Ajustez la configuration dans le fichier simple-api/src/main/resources/application.yml. Vous devrez indiquer les configuration pour accèder à votre base de données. Normalement pour ce type de configuration qui dépend de l'environement il est fortement recommandé de ne pas hardcoder le setting dans l'image. On preferera utiliser des variables d'environnement que l'on indiquera au lancement du docker (option -e ou bien via un fichier d'env).

  datasource:
    url: "jdbc:postgresql://${DATABASE_URL}:${DATABASE_PORT}/${POSTGRES_DB}

Créer le Dockerfile de votre API comme vous l'avez fait dans le chapitre précédent. Builder votre image API et lancer le docker à partir de cette image.

Tip

Attention votre API doit pouvoir se connecter à la Base de données. Comment faire ? C'est très simpe : placez votre contenaur API dans le même réseau que votre DB, créez un docker network si ce n'est pas déjà fait. Rappelez-vous également que dans un network vous pouvez requêter un conteneur sur son nom.

Tip

Votre API doit-être publiée pour pouvoir y accèder. Vous aurez surement besoin de l'option -p au lancement du conteneur. Faites attention a utiliser un port disponible sur votre Host.

Une fois que tout est correctement lancé, vous devriez pouvoir accéder à votre API, par exemple sur : /departments/IT/students. (dépend du port mapping que vous avez réalisé)

[
  {
    "id": 1,
    "firstname": "Eli",
    "lastname": "Copter",
    "department": {
      "id": 1,
      "name": "IT"
    }
  }
]

Explorez d'autres points d'accès de votre API, jetez un coup d'œil aux contrôleurs dans le code source.

Check

Une API Web fonctionne sur votre base de données.

Serveur HTTP

Choisissez une image de base appropriée.

Créez une page de destination simple : index.html et placez-la dans votre conteneur.

Cela devrait suffire pour l'instant, démarrez votre conteneur et vérifiez que tout fonctionne comme prévu.

Voici quelques commandes que vous pouvez essayer pour vérifier :

  • docker stats
  • docker inspect
  • docker logs

!!! link - Httpd - Mise en route

Récupération de la Configuration

Vous utilisez la configuration Apache par défaut, et cela devrait suffire pour l'instant. Utilisez docker exec pour récupérer cette configuration par défaut depuis votre conteneur en cours d'exécution /usr/local/apache2/conf/httpd.conf.

Note

Vous pouvez également utiliser docker cp.

Check

Avoir un fichier httpd.conf en local prêt à être édité.

Reverse Proxy (RPX)

Nous allons configurer le serveur HTTP en tant que serveur reverse proxy devant notre application. Ce serveur pourrait être utilisé pour fournir une application front, configurer un endpoint SSL ou gérer la répartition de charge.

Donc, cela peut être assez utile même si, dans notre cas, nous garderons les choses simples. En effet, il servira de passerelle pour accéder à votre API (proxyPass).

Voici la documentation : Reverse Proxy.

Ajoutez ce qui suit à la configuration httpd.conf que vous avez récupéré précédemment, modifier votre Dockerfile pour injecter cette configuration :

ServerName localhost


<VirtualHost *:80>
ProxyPreserveHost On
ProxyPass / http://YOUR_BACKEND_LINK:8080/
ProxyPassReverse / http://YOUR_BACKEND_LINK:8080/
</VirtualHost>
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

Tip

COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf devrait être présent dans votre dockerfile YOUR_BACKEND_LINK correspond à l'url de votre API (nom du docker api grâce au DNS interne)

lancez votre front httpd avec les bonnes options (bon network notamment)

Vérifiez que votre reverse proxy sert bien l'API

Créons une stack

Maintenant imaginons que je vous demande de tout détruire et de tout reconstruire. Où bien de me cloner cette stack 3-tiers, vous devrez relancer l'ensemble des commandes, créer les networks, les volumes, les conteneurs. Heureusement, pour créer des stack complête Docker Compose arrive à la rescousse !

Docker-compose

1- Installez docker-compose si la commande docker compose ne fonctionne pas.

Vous avez peut-être remarqué qu'il peut être assez fastidieux d'orchestrer manuellement le démarrage, l'arrêt et la reconstruction de nos conteneurs. Heureusement, un outil utile appelé docker-compose est utile dans ces situations.

2- Créez un fichier docker-compose.yml avec la structure suivante pour définir et piloter nos conteneurs :

version: "3.7"

services:
  backend:
    build:
    #TODO
    networks:
    #TODO
    depends_on:
    #TODO

  database:
    build:
    #TODO
    networks:
    #TODO

  httpd:
    build:
    #TODO
    ports:
    #TODO
    networks:
    #TODO
    depends_on:
    #TODO

networks:
  my-network:

Docker-compose gérera les trois conteneurs et un réseau pour nous.

Une fois que vos conteneurs sont orchestrés par docker-compose, vous devriez disposer d'une application parfaitement fonctionnelle. Assurez-vous de pouvoir accéder à votre API sur localhost.

Note

Les ports de votre backend et de votre base de données ne doivent pas être ouverts sur votre machine hôte.

Question

Pourquoi docker compose est-il si important ?

Check

Une application 3-tiers fonctionne avec docker-compose.

Bonus - Amélioration du Docker compose

  1. Ajoutez un volume à votre base de données

    volumes:
      my_db_volume:
        driver: local
    
  2. Utilisez un fichier .env pour stocker vos variables d'environnement (pour la DB par exemple):

    database:
      env_file:
        - database/.env
    
  3. ségreguez votre stack avec plusieurs networks : un network front (front, API) / un network back (DB, API)

  4. Mettez en place un health check sur votre base de données, et un depends_on sur votre API qui fonctionnera sur la readiness de votre DB

    # coté compose api
        depends_on:
        database:
            condition: service_healthy
    
    # coté compose database
        healthcheck:
        test: ["CMD-SHELL", "pg_isready -U postgres"]
        interval: 10s
        timeout: 5s
        retries: 3
    

Bonus - Publication de vos images

Vos images Docker sont stockées localement, publions-les pour qu'elles puissent être utilisées par d'autres membres de l'équipe ou sur d'autres machines. En effet l'un des éléments fondamental de l'écosystème est la publication des images (le plus leger possible), dans le flow de dévelopement c'est ce qu'on appelle le Delivery. Dans notre cas nous allons publier nos images sur le registry Docker principale : Docker Hub

Vous aurez besoin d'un compte Docker Hub.

1- Connectez-vous à votre compte Docker Hub fraîchement créé avec docker login.

2- Étiquetez votre image. Jusqu'à présent, nous n'avons utilisé que l'étiquette "latest", maintenant que nous voulons la publier, ajoutons des informations de version significatives à nos images.

docker tag my-database USERNAME/my-database:1.0

3- Ensuite, poussez votre image sur Docker Hub :

docker push USERNAME/my-database

Docker Hub n'est pas le seul registre d'images Docker, et vous pouvez également héberger vos propres images (c'est évidemment le choix de la plupart des entreprises).

Une fois que vous avez publié vos images sur Docker Hub, vous les verrez dans votre compte : il serait très utile d'avoir une documentation pour votre image si vous prévoyez de la partager avec d'autres personnes.

Note

Vous pouvez également utiliser le registre Google Container, Amazon ECR ou bien au plus proche de votre code sur Gitlab ou Github.Vous pouvez aussi déployer votre propre registry avec des outils comme Portus ou Harbor.

Check

Des images Docker publiées.

Conclusion

Cela devrait vous donner un bon aperçu de l'utilisation de Docker. Vous avez mis en place une application trois tiers, configuré une base de données, une API Backend et un serveur HTTP. Tout cela est orchestré par Docker Compose. Enfin, vous avez publié vos images sur Docker Hub pour les partager avec d'autres.

© Takima 2023

"