Dans cette seconde partie, nous allons mettre en place le notre projet avec le système Docker

Présentation de Docker

Mais qu'est-ce que c'est ce système ? C'est tout simplement un ensemble de composants logiciels qui permet d'exécuter tes applications dans des conteneurs.
Chaque conteneur possède son propre OS avec sa liste de dépendances liées à ton application. Chacun des conteneurs est indépendant, peu importe le système d'exploitation qui héberge docker. Cela permet donc une meilleure isolation de tes conteneurs et de ta machine hôte.
Cela évite de se retrouver avec, par exemple, plusieurs version de python, de npm ou autre sur ta machine. C'est vraiment la galère quand tu dois changer ton path ou variable d'environnement.
Une conteneur est donc basé sur une image, par exemple Ubuntu, python, Debian, ... qui contient tout le nécessaire pour la faire tourner. Ensuite tu enrichis ce conteneur avec une liste de commandes, comme par exemple une suite d'installation de paquets, propre au fonctionnement de ton application finale.

Illustrons cela par une image toute simple :
container-seperated-frm-eachother-300x239

L'avantage de ce système c'est qu'il est possible de reproduire le fonctionnement de ton application d'une machine à un autre en écrivant uniquement la recette du Dockerfile et dudocker-compose.
Le Dockerfile est un fichier qui va indiquer comment construire l'image (= le conteneur) avec la distribution et les paquets qui vont bien (python3, ssl,...).
Le docker-compose, quant à lui, est un fichier qui indique comment va être lancé le conteneur :

  • le type de variable d'environnement à utiliser
  • l'emplacement des volumes à utiliser
  • ...

Je vous invite à lire la documentation officielle (https://docs.docker.com/ et https://docs.docker.com/compose/) pour comprendre plus en détails le fonctionnement.

C'est un outil très puissant pour le quotidien, pour reproduire facilement des environnement de développements mais aussi de production.

Si tu es un développeur, je te conseille de t'intéresser rapidement à cette technologie. Elle sera très pratique pour la suite :)

Application du système docker à notre template

Il faut tout d'abord installer Docker sur votre Machine (locale ou VPS). Je vous renvoie vers le très bon lien suivant :
https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04-fr

Une fois que c'est fait, il faut créer ces fameuses recettes pour chaque partie du projet.

Il sera avant-tout nécessaire d'apporter quelques modification à notre frontend et notre backend pour spécifier les URLS à utiliser.
Ainsi, pour le frontend, il faut indiquer dans le fichier src/config.config.js l'URL du backend (cf exemple).
Idem pour le backend, il faudra les renseigner dans le fichier backend/core/settings.py

CORS_ALLOWED_ORIGINS avec l'adresse IP/Port du frontend

Ces manipulations sont à faire en mode de développement ou en mode production.
Pour la production, c'est le settings_prod.py qu'il faudra modifier. Le fichier est déjà épuré pour avoir un niveau de sécurité plus élevé. Je suis preneur pour toutes les pistes d'amélioration au niveau production du backend :)

Lancement en mode développement

La recette Dockerfile pour le Frontend :

FROM node:lts-alpine
WORKDIR /app
COPY package.json ./
COPY . .
RUN npm install @vue/cli@3.7.0 -g
CMD ["sh", "-c", "npm install && npm run test"]

Quelques explications s'imposent :

  • la première ligne indique l'OS que l'on utilise, dans notre cas, on veut on OS qui fasse tourner directement du nodejs avec tous ses composants
  • Nous indiquons le répertoire de travail
  • Nous copions tous les fichiers qui se trouvent sur la machine hôte (le dossier où le Dockerfile est présent) dans le répertoire de travail
  • Nous installons Vuejs et son cli
  • Et le CMD pour exécuter une commande lors du démarrage du conteneur. Dans notre cas, installation de toutes les dépendances de notre projet et lancement du projet en mode développement ("npm run test")

La recette Dockerfile pour le backend est la suivante :

FROM python:3
ENV PYTHONUNBUFFERED=1
RUN mkdir /work
WORKDIR /work
RUN apt-get update &&\
apt-get install -y binutils libproj-dev gdal-bin python-gdal python3-gdal
COPY requirements.txt /work/
RUN pip install -r requirements.txt
COPY . /work/
COPY ./entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

C'est un peu le même principe que pour le premier Dockerfile, sauf qu'ici, nous partons d'un OS avec une distribution python3.
La commande RUN permet d'exécuter une opération bash (installer un paquet, mettre des droites,...)
A la fin, notre image démarre en exécutant le script bash entrypoint.sh.
Ce coup-ci nous avons remplacer la commande CMD par ENTRYPOINT mais pourquoi ?
La différence entre ces deux commandes c'est que CMD peut-être surchargé et contient en principe peu d'instructions. Entrypoint lui peut-être difficilement surchargé et contient une liste d'instructions plus importantes.

Son contenu est le suivant

#!/bin/sh

echo "Running docker entrypoint with these arguments: $@"
echo "Current user: $(id)"
echo "Current environment:"
set
echo

if [ "$1" = "coreRest" ]; then
    python manage.py makemigrations
    python manage.py migrate
    python manage.py collectstatic --noinput

    if [ "$2" = "init" ]; then
        echo "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'pass')" | python manage.py shell
    fi

    exec python manage.py runserver 0.0.0.0:8000
fi

exec $@

Et enfin le fichier docker-compose-debug.xml

version: "3.8"

services:

   django_rest:
    build:
        context: ./backend
        dockerfile: DockerfileBackend
    command: coreRest init
    volumes:
      - backend-app:/work
    ports:
      - "8000:8000"

   front:
    build:
        context: .
        dockerfile: DockerfileFrontend
    ports:
      - "8080:8080"
    volumes:
      - vue-app:/app

volumes:
  vue-app:
    driver: local
    driver_opts:
      type: none
      device: /absolute/pathofdir
      o: bind

  backend-app:
    driver: local
    driver_opts:
      type: none
      device: /absolute/pathofdir/backend
      o: bind

L'ensemble des sources est partagé dans les docker via le montage des volumes.
Dans ce fichier, nous indiquons aussi les ports exposés .N'oubliez pas d'aller regarder la documentation pour comprendre le fonctionnement des volumes.
Nous avons mis en place une Stack qui contient deux services, la Backend nommé django_rest et le Frontend nommé front..

Pour lancer la Stack, rien de plus simple, il suffit d'exécuter la commande suivante :

docker-compose -f docker-compose-debug.xml up

On peut ajouter l'argument -d pour détacher cette "Stack" de la console. Dans un premier temps, lancer sans cette option pour savoir si la "Stack" s'exécute bien !.
Les différents problèmes que l'on peut rencontrer :

  • les volumes ne sont pas bien paramétrés
  • les ports sont déjà ouverts

Le Backend tourne sur le port 8000 et le Frontend sur le port 8080. Ces deux ports sont exposés vers l'extérieur.

Lancement en mode production

Comme pour le partie développement, il faut s'assurer que les fichiers de configuration du Frontend et Backend soient bien configurés.

La recette pour le Dockerfile du Frontend change légèrement. En effet, ce docker va utiliser NGINX (un serveur web) pour distribuer la page web.
La page Web est quant à elle construite via le build de npm et donne en résultat un liste de fichiers html, css et js.

FROM node:lts-alpine as build_front
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
ENV NPM_CONFIG_UNSAFE_PERM=true
COPY package.json /app/package.json
RUN npm install -i
RUN npm install @vue/cli -g
COPY . /app
RUN npm run build

FROM nginx:1.16.0-alpine
COPY --from=build_front /app/dist /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 8099
CMD ["nginx", "-g", "daemon off;"]

Nous avons fait du Multi-staging, c'est à dire que nous créons 2 images dont l'une repose sur l'autre.
Une image qui nous permet de builder notre application Web et un autre qui nous délivre l'application Web.
C'est couramment utilisé en production.

La recette Dockerfile du Backend change très très peu, nous utilisons un fichier entrypoint_prod.sh en utilisant gunicorn pour servir notre application Django

FROM python:3
ENV PYTHONUNBUFFERED=1
RUN mkdir /work
WORKDIR /work
RUN apt-get update &&\
apt-get install -y binutils libproj-dev gdal-bin python-gdal python3-gdal
COPY requirements.txt /work/
RUN pip install -r requirements.txt
COPY . /work/
COPY ./entrypoint_prod.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

Il n'est pas possible de servir les fichiers static et les media dans cette recette du Backend. Nous devons donc créer nouvelle recette en ajoutant NGINX pour rendre disponible ces pages.

FROM nginx:1.19.0-alpine
LABEL maintainer="TemplateDjangoVueSaas"
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d

Le fichier de configuration associé à NGINX est le suivant :

server_tokens off;
server {

  listen 8099;
  gzip_static on;
  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }

  error_page   500 502 503 504  /50x.html;

  location = /50x.html {
    root   /usr/share/nginx/html;
  }

}

Le fichier docker-compose qui orchestre ces conteneurs est le suivant :


version: "3.8"

services:

   django_rest:
    build:
        context: ./backend
        dockerfile: DockerfileBackend
    command: coreRest init
    ports:
      - 8000
    volumes:
      - /root/TempplateVueJsDjangoSaas/backend/core/settings_prod.py:/work/core/settings.py
      - /root/TempplateVueJsDjangoSaas/backend/app/migrations:/work/app/migrations
      - /root/TempplateVueJsDjangoSaas/backend/static:/work/static
      - /root/TempplateVueJsDjangoSaas/backend/media:/work/media
      - /root/TempplateVueJsDjangoSaas/backend/db.sqlite3:/work/db.sqlite3

   nginxfile:
    build: 
        context: ./backend
        dockerfile: DockerfileNginx
    volumes:
        - /root/TempplateVueJsDjangoSaas/backend/media:/media
        - /root/TempplateVueJsDjangoSaas/backend/static:/static
    ports:
          - "8000:80"
    command: /bin/sh -c "nginx -g 'daemon off;'"

   front:
    build: 
        context: .
        dockerfile: Dockerfile-Prod
    ports:
      - "8080:8099"

On constate contrairement à notre docker de développement, il n'y a pas de volume pour le Frontend. C'est normal car tous les sources minifiées sont dans le conteneur. C'est toujours le port 8080 qui est exposé vers l'extérieur et c'est le port 8099 qui est utilisé dans notre image.
Pour la partie Backend, ce n'est plus Django qui est exposé à l'extérieur, c'est notre NGINX qui fait office de reverse proxy et transmet les demandes !
Par la même occasion, les volumes sont moindres dans le Backend, nous partageons uniquement les fichiers de paramètres et des fichiers à sauvegarder. Le source de l'application est dans le conteneur.

Pour lancer la Stack, rien de plus simple, il suffit d'exécuter la commande suivante :

docker-compose up

Voilà j'espère que cette partie n'a pas été trop dense et complexe. Il est bon de manipuler pour comprendre le lien entre les différents éléments.
Baignant depuis quelques années dans Docker, il se peut que je sois passé un peu vite sur des notions donc n'hésitez pas à me solliciter ou me laisser vos remarques.
Un combo sympa est d'utiliser le conteneur Traefik pour déployer facilement ce Frontend et Backend avec votre nom de domaine.

Happy Coding !!

Article précédent Article suivant