Avec ton coeur de Docker

Avec ton coeur de Docker

Je m'abonne
Temps de lecture: 17 mins

Durant mon cursus d'apprentissage, je n'avais pas appris Docker. Je connaissais vaguement son principe, mais je ne comprenais pas vraiment son intérêt par rapport à une machine virtuelle. Je l'ai laissé trop longtemps de côté.
Je m'y suis mis réellement il y a maintenant à peu près six mois. J'ai pris des conteneurs sur la tête, ça, c'est certain ! Mais je dirais qu'en fait, je ne peux tout simplement plus m'en passer.

Que ce soit pour passer un projet à un copain, déployer une application ou éviter les erreurs liées à un système en particulier, c’est vraiment un must have dans le dev.

Bien évidemment, cela demande un peu de temps d'apprentissage, mais ça en vaut la peine.

Je ne prétends pas être un "Docker Master" ni que cet article te permettra de sauter la phase d'apprentissage, mais il t'aidera au moins à en saisir tout l’intérêt.

 La Théorie des conteneurs

Ça paraît bête, mais si on n'en voit pas l’intérêt (comme moi avant), on n'aura pas envie de l'apprendre.

Docker, pour ceux qui ne connaissent pas, c'est un système de virtualisation par image, bien plus léger qu'un hyperviseur du genre Proxmox ou VMware.

En fait, on peut installer Docker sur quasiment n'importe quel système, et il devient une interface de lancement d’applications. Cela nous permet de nous affranchir de la couche d'infrastructure de notre système.

Par exemple, si je travaille sur Debian, que je développe hors Docker, puis que j'installe mon projet sur une Fedora, je vais devoir refaire toute la configuration pour le faire fonctionner.
En revanche, si je commence avec Docker sur Debian et que je lance mon application sur le Docker de ma Fedora, je n'aurai aucune différence de configuration, car Docker fait office de couche d’abstraction.

Un autre avantage, c'est sa capacité à mettre à jour les versions des systèmes qu'on utilise en ne changeant que quelques lignes.

Avec Docker, on crée des Dockerfile, qui sont des fichiers d'instructions servant à générer des images pour exécuter nos applications. C'est comme si l'on créait un système 100 % dédié à notre application.

Cette image, on peut ensuite la charger et la lancer dans un conteneur (l'équivalent d’une petite machine virtuelle) qui tournera sur Docker avec la configuration que l'on aura choisis au préalable via des fichiers de configuration.

On peut aussi créer des réseaux internes pour permettre à plusieurs conteneurs de communiquer entre eux.

Finalement, les intérêts sont multiples : le CI/CD, l'abstraction de l'infrastructure, la facilité de mise à jour d'un service… Les avantages sont nombreux !

Direction le port !
Allez, direction le port !

Pour suivre ce tutoriel tu devras bien évidement avoir Docker sur ta machine 😅 Pour l’installation c'est par ici

 Notre première image

On peut soit utiliser une image disponible sur Docker Hub, qui est la référence pour toutes les images officielles (attention à toujours bien vérifier la source), soit créer la nôtre.

L'intérêt ici est justement de créer notre propre image. On verra plus tard comment récupérer une image depuis le Hub, car cela peut parfois nous faire gagner du temps.

On commence donc par créer un fichier Dockerfile (sans extension).

Dockerfile

Première étape : le système

Puisque notre image va servir à créer un environnement isolé (comme une machine virtuelle), la première ligne de notre Dockerfile doit définir quel système on va utiliser comme base.
C'est un peu comme pour un PC : pour qu'une application fonctionne, il faut lui dire sur quel OS elle va tourner.

Pour ce tutoriel, on va utiliser une image ultra légère d'un Linux intégrant PHP et Apache, qui contient déjà tous les packages liés à PHP :

FROM php:8.3-apache

C'est donc la première brique de notre image.

Dans ce FROM, il y a en réalité d'autres instructions, déjà définies par des contributeurs open source, qui nous permettent de l'utiliser aussi simplement.
C'est une sorte de raccourci : sinon, on devrait à chaque fois tout recréer ligne par ligne pour installer et configurer le système de base.

Ensuite, on va lui indiquer dans quel répertoire se trouvera notre application à l’intérieur du conteneur.

FROM php:8.3-apache

WORKDIR /var/www/html

Quand on lancera notre conteneur plus tard, on sera directement positionné dans ce dossier.
Docker utilisera alors ce chemin comme référence relative pour toutes les instructions suivantes.

On va maintenant lui indiquer ce que l'on souhaite exécuter comme projet.
Pour le tutoriel, ce sera simplement un fichier PHP qui affiche un texte avec un echo.

On crée donc un fichier index.php contenant juste un echo, puis on l'ajoute dans notre image.

L'instruction COPY indique à Docker de copier l'ensemble du répertoire de notre machine hôte (celui où se trouve le Dockerfile) dans le répertoire "." du conteneur — ici, sous-entendu /var/www/html/mon_premier_docker.

FROM php:8.3-apache

WORKDIR /var/www/html

COPY . .

Parfait, on a maintenant notre fichier, c’est top !
Il ne reste plus qu'à le transformer en image Docker grâce à une simple commande.

docker build -t ma-premiere-image-docker:latest .

Build de l'image

On voit bien les steps (étapes) qui se construisent lors de la création de notre image.
Chaque ligne d'instruction de notre Dockerfile correspond à un Step, qu'on appelle aussi une couche (Layer).

Une fois la construction terminée, on peut vérifier la présence de notre image dans Docker avec la commande suivante : docker image ls.

docker image ls

Notre image est maintenant prête : c'est une sorte de template.
Il faut à présent la transformer en conteneur à l'aide de la commande suivante :

docker run -d --name mon-premier-container-docker -p 80:80 ma-premiere-image-docker:latest

Concrètement, qu'est-ce qu’on fait ici ?

  • On donne un nom à notre conteneur grâce à l'option --name.

  • On mappe le port 80 de notre machine vers le port 80 du conteneur, afin de pouvoir y accéder depuis le navigateur.

  • On associe l'image que nous avons créée au conteneur.

  • Le -d permet de détacher le processus de la console pour libérer le terminal et continuer à exécuter d'autres commandes ensuite.

À présent, on peut ouvrir son navigateur et vérifier que tout fonctionne en se rendant sur : http://localhost:80

La baleine de docker

On a bien notre index.php qui est servi par notre conteneur !

Finalement, on vient tout simplement de créer un serveur Apache qui sert des fichiers PHP.

La configuration est volontairement simple ici, mais c'était suffisant pour l’exemple.
On pourra, à notre convenance, y ajouter d’autres services comme phpMyAdmin et une base de données.

Et justement… on va voir ça tout de suite !

 Les docker compose

C'est un autre fichier que l'on associe souvent aux Dockerfile, mais qui, en réalité, est totalement indépendant.
On peut tout à fait avoir un projet fonctionnel avec zéro Dockerfile dans le dossier, à condition que les images utilisées soient déjà disponibles sur le web (par exemple sur Docker Hub ou un repository privé).

Les Docker Compose nous permettent d'ajouter facilement des réseaux et des volumes afin de rendre les données persistantes.
Car oui, si on éteint un conteneur, toutes les informations présentes à l'intérieur de la "VM" disparaissent avec lui.
D'où l'intérêt de monter des volumes pour sauvegarder certaines données, comme celles d’une base de données, par exemple.

On va donc se créer un nouveau fichier nommé docker-compose.yml :

services:

Chaque première indentation sera le nom de nos services :

services:
	db:
	
	phpmyadmin:

Ici on aura donc 2 services lors de la création de nos images : db et phpmyadmin

Pour chaque service, on va lui indiquer l'image qu’il doit utiliser :

services:
	db:
	  image: mariadb:10.11
	
	phpmyadmin:
		  image: phpmyadmin

En cas de plantage ou de soucis avec notre container, on a également besoin qu'il se restart automatiquement, on lui ajoute donc une propriété restart=always . Pour ceux qui sont familier avec systemd c'est un peu le même genre.

services:
	db:
	  image: mariadb:10.11
	  restart: always
	
	phpmyadmin:
		  image: phpmyadmin
		  restart: always

Pour notre DB on prend le port par défaut de mariadb qui est 3306 donc on ne le spécifiera pas. Par contre pour phpmyadmin, on va changer son port par défaut et mettre 8080 :

services:
	db:
	  image: mariadb:10.11
	  restart: always
	
	phpmyadmin:
		  image: phpmyadmin
		  restart: always
    ports:
      - 8080:80

On va maintenant indiquer les variables d'environnements pour paramétrer notre DB, notamment le nom de notre DB, le password etc …

services:
	db:
	  image: mariadb:10.11
	  restart: always
    environment:
      MYSQL_ROOT_PASSWORD: monSupermotdepassequitue!
      MYSQL_DATABASE: ma_premiere_db_docker
	
	phpmyadmin:
		  image: phpmyadmin
		  restart: always
    ports:
      - 8080:80

Par défaut un container prendra le nom de son service, mais on peut aussi lui changer :

services:
	db:
	  image: mariadb:10.11
	  restart: always
	  container_name: ma-premiere-db-docker
    environment:
      MYSQL_ROOT_PASSWORD: monSupermotdepassequitue!
      MYSQL_DATABASE: ma_premiere_db_docker
	
	phpmyadmin:
		  image: phpmyadmin
		  restart: always
    ports:
      - 8080:80

Voila ! On a nos 2 services, mais on peut aussi prendre le dockerfile que l'on vient de créer pour en faire un troisième :

services:
	db:
	  image: mariadb:10.11
	  restart: always
	  container_name: ma-premiere-db-docker
    environment:
      MYSQL_ROOT_PASSWORD: monSupermotdepassequitue!
      MYSQL_DATABASE: ma_premiere_db_docker
	
	phpmyadmin:
		  image: phpmyadmin
		  restart: always
    ports:
      - 8080:80
      
  php:
	  build: .
	  restart: always
	  container_name: mon-premier-container-docker
    ports:
      - 80:80

On indique ainsi au docker-compose d'utiliser le build créé par le dockerfile comme base de ce service.

J'ai conscience que ça fait pas mal de commandes, pas mal de fichiers et d'instructions mais à force de les utiliser, comme GIT, ça devient naturel !

On peut ainsi lancer la commande docker compose -d up pour indiquer que l'on veut monter nos container directement après la création des images :

Lancement de la commande docker compose

On voit bien toutes les layers qui se créent et qui se lancent.

A présent, on peut aller sur localhost:8080 et utiliser l'admin root et le password de notre docker-compose :

PHPMyAdmin login

et notre PHPMyAdmin voit bien notre base de données :

Nos DBs

Mais alors comment ça se fait ? c'est censé être des containers séparés logiquement PHPMyAdmin ne devrait même pas voir ma_premiere_db_docker.

Et bien en fait tout ce qui se trouve dans le docker-compose (sauf indication contraire) se trouvera dans le même réseau créé par défaut.

On peut voir la liste des réseaux avec cette commande : docker network list:

docker network inspect

Il prend le nom du dossier dans lequel se trouve le docker-compose.yml. On pourra le changer par la suite. Pour voir quels containers se trouvent dans notre réseau on fait

docker network inspect docker_intro_default.
Les containers dans le réseau

ça nous permettra plus tard de récupérer les adresses IP pour communiquer avec nos containers si besoin et surtout de savoir en cas de debug si mon container est bien dans le bon réseau 😅.

Ici on voit bien que PHPMyAdmin est dans le réseau de la DB, ils peuvent donc communiquer ensemble.

On va insérer quelques data à présent via PMA, on va donc dans notre db ma_premiere_db_docker puis on insère :

Création de la table
Création de la table
Création des propriétés
Création des propriétés
Voila, à présent on a bien notre base de données avec une table.
Notre table créée

Voyons voir ce que ça donne si on simule un crash de serveur en éteignant notre container.

On exécute la commande suivante docker compose down .

docker compose down

On relance pour retourner sur notre PMA docker compose up -d .

Et là … c’est le drame !

Plus rien dans notre DB
Noooooon rien de riiiiien ! 🎙️

Aucune table, alors qu’on vient de les créer … Pourquoi ???

Tout simplement parce que notre container ne crée rien de persistant si on ne lui indique pas. Tout est volatile avec les containers, sauf si on lui dit le contraire.

Pour ça on utilise la propriété "volumes" dans notre docker-compose :

	db:
	  image: mariadb:10.11
	  restart: always
	  container_name: ma-premiere-db-docker
    environment:
      MYSQL_ROOT_PASSWORD: monSupermotdepassequitue!
      MYSQL_DATABASE: ma_premiere_db_docker
    volumes:
      - ./db:/var/lib/mysql

On dit à notre container d'utiliser le dossier db comme volume persistant à notre base de données mariadb.

Si le dossier db n’existe pas côté local, il sera créé si le user docker à les droits sinon il y aura une erreur (passer en sudo si besoin).

On recrée les containers directement via docker compose up -d --build pour reforcer le build des images des services.

On retourne sur notre PMA et on récrée notre première table comme plus haut.

On stop notre container : docker compose down puis directement docker compose up et là … Tadaaaaaam ! La base de données a persistée ! On peut donc y sauver de la data sans crainte de perdre les data.

On peut aussi faire de même pour notre petit container PHP , on crée un sous dossier ./app et on y mets notre index.php.puis on vient mappé notre volume :

  php:
    build: .
    restart: always
    container_name: mon-premier-container-docker
    volumes:
        - ./app:/var/www/html/

On modifie le Dockerfile pour faire correspondre ce changement de dossier :

COPY . .COPY ./app .

Mais pourquoi on fait ça ? Etant donné que l'on fige les informations de l'image vers le container, sans volume partagé on devrait à chaque modification de notre code, rebuild notre image. Hors ici, avec le volume partager, on peut directement modifier notre code PHP sans rebuild !

Si tu veux voir les logs de ton app, tu peux aussi créer un volume avec en source les logs de apache :

    volumes:
        - ./app:/var/www/html/
        - ./logs:/var/log/apache2/

 rebuild et lance le container directement avec docker compose up -d --build et dans ton répertoire :

Répertoire
Notre répertoire

 Tu peux aussi aller directement dans ton container pour regarder des fichiers au besoin à l'aide la comande :

docker exec -ti mon-premier-container-docker /bin/bash
grâce à ça tu peux intéragir dans ton container, besoin d'un tail ? d’un cat, tu peux le faire. Et si besoin tu peux y ajouter ponctuellement des paquets comme nano par exemple.On est dans notre container

Pourquoi ponctuellement ? Souviens toi, à chaque fois que tu éteins ton container, quand tu le rédémarera, il démarrera comme au tout début, juste au niveau du build. Donc si tu veux un paquet en plus, tu peux l’ajouter dans ton Dockerfile :

FROM php:8.3-apache

RUN apt-get update && apt-get install -y nano

WORKDIR /var/www/html

COPY ./app . 

Tu sais maintenant démarrer docker avec des images créés toi même, mettre tes containers dans des réseaux et garder des données persistantes.

Par contre, tu vas vite voir qu’avec docker, c'est très facile d’avoir des tas d'images et des tas de containers inutilisées 😅. Pour le bien de ton stockage et de tes ressources, tu devras éteindre et faire le tri dans les containers.

Pour lister tes images ou tes containers : docker image ls et docker container lsdocker container ls

Tu peux aussi voir les containers qui sont actifs avec docker ps (un peu comme ps aux  pour les unix). C’est utile pour voir aussi si tes containers sont bien allumés.

docker ps

Chaque ID correspond à une référence unique du conteneur et tu peux donc en arrêter un manuellement avec docker stop f1f (Seuls les trois premiers caractères de l'ID suffisent en général).

Pense aussi à supprimer tes images avec docker image rmi <iddetonimage>.

Et si tu veux faire un nettoyage global, tu peux exécuter docker image prune. Cette commande supprimera toutes les images qui ne sont associées à aucun conteneur, ce qui te permettra d'économiser de l’espace disque.

J'aurais pu aussi te faire une liste du style "Les 20 commandes Docker les plus utilisées", mais ce n’est pas vraiment utile pour le moment. Inutile de t'assommer avec des commandes que tu oublierais tant que tu ne maîtrises pas déjà celles que l’on vient de voir.

Je vais par contre te les récapituler ici :

  • docker build -t <nomdetonimage>:<tag/version> <dossierDeTonDockerfile> ça te permet de créer l'image via ton Dockerfile.
  • docker run -d --name <nomdetoncontainer> -p <port:port> <nomdetonimage>:<tag/version> (Utile aussi pour lancer une image depuis le dockerHub)
  • docker compose -d up (lancer depuis le dossier où est le docker-compose.yml) Le -d permet uniquement de se détacher de la tâche dans la console (pour ne pas à ouvrir un second terminal et le lancer en tâche de fond) Si on rajoute --build on reconstruit via les Dockerfiles ou les images mises à jour. docker compose permet de build les images (ou les récupérer via le cloud) et de lancer les containers associés avec up.
  • docker compose down (éteint l'ensemble des containers du docker-compose.yml)
  • docker network list (Affiche la liste de tous tes networks)
  • docker network inspect <nondetonréseau> (Affiche plein d'infos sur le réseau et les containers associés)
  • docker exec -ti <nomducontainer/idducontainer> /bin/bash (-ti permet d’accéder à la commande executée avec /bin/bash on affiche donc un terminal dans le container).

Avec ça, tu peux faire tes armes Docker. Tu pourras aussi étoffer tes docker-compose.yml au fil du temps ! Et surtout, ça te permettra de te détacher de ton système de développement initial et du "ça marche sur mon poste" !

ça marche sur mon poste
(Deux fois que j’utilise cette image sur mon blog,
elle fonctionne dans beaucoup trop de situation 😅)

Je tiens aussi à remercier Cédric qui se reconnaîtra pour m'avoir beaucoup aidé dans mon apprentissage Docker. Avant de découvrir cette manière de virtualiser j'étais un fervent défenseur des VMs que l’on créé via Proxmox. Je ne retournerai pas en arrière.

Et petite dédicace à mon frère qui bosse au port du Havre entouré de conteneurs toute la journée, chaque fois que je fais une commande docker, j’ai une petite pensée pour lui ! ❤️

A présent je me rends compte que finalement on n'est pas obligé de sortir l'artillerie lourde pour abattre un moustique ! Et l'avantage avec Docker, c'est que ça va te faire progresser en réseau et en commandes Linux ! Alors tu attends quoi pour lancer ton premier conteneur ?