Table des matières

Comment on fabrique une image Docker ?

On va essayer de fabriquer notre première image Docker. Soyons fous, on va faire une image d’un serveur web assez connu, nginx, pour mettre en ligne notre premier site sur les serveurs de Picasoft. ^_^

On va supposer pour cette page que tu es connecté sur la machine de test.

Tu arrives dans ton dossier personnel, et tu vas pouvoir créer un fichier Dockerfile, qui est la recette de cuisine que va suivre Docker pour construire l’image. En d’autres termes, Docker va partir d’une image pré-existante, exécuter toutes tes instructions, et enregistrer le tout dans une nouvelle image.

Un serveur web vierge

Note:

Pour créer et éditer un fichier, tu peux utiliser l’éditeur nano, dans le terminal. Une fois que tu as fini d’éditer ton fichier, appuie sur Ctrl+X, Y et Enter pour quitter. Tu peux ensuite utiliser la commande cat pour lire le contenu du fichier.

Écrire la recette de cuisine

Un Dockerfile commence toujours par une instruction FROM. On indique de quelle image on veut partir. En général, c’est une distribution Linux. On utilise beaucoup Debian à Picasoft, une distribution robuste et stable. La première ligne sera donc :

FROM debian

Note:

On peut préciser une version de l’image en la suffixant avec :. Exemples : debian:strech, debian:buster… C’est même une Bonne Pratique™, mais on va pas partir dans les détails. :-P

Ensuite, on va utiliser l’instruction RUN pour indiquer les commandes qu’on veut lancer. C’est ici qu’il faut se demander :

Question:

Quelles commandes j’aurai lancé pour installer nginx sur ma machine Linux ?

On regarde sur la documentation d'installation, et on voit qu’il y a deux commandes à lancer. Alors c’est parti, on rajoute des RUN dans le Dockerfile pour indiquer qu’il faut lancer ces commandes :

RUN apt-get update
RUN apt-get install -y nginx

Petite subtilité, on a rajouté un -y (« yes » pour ne pas demander confirmation), puisque la commande sera exécutée sans interaction par Docker.

Ensuite, il faut savoir qu’nginx écoute sur le port 80, le port standard pour le web.

Question:

C’est quoi un port ?

Il faut imaginer un port comme une porte de chambre dans un hôtel. Puisque plusieurs applications peuvent tourner sur un même ordinateur, si on veut les contacter de l’extérieur, il faut bien un moyen pour indiquer que c’est à telle application que je veux parler et pas à une autre. C’est l’idée des ports : chaque application choisit un port sur lequel elle écoute, et dès que quelqu’un tape à son port, elle sait que ça lui est destiné.

Conséquence : un port ne peut être écouté que par une seule application, sinon on se retrouve avec le problème de ne pas savoir à qui est destiné le message.

On va donc indiquer à l’image qu’il faudra ouvrir le port 80, le rendre contactable par l’extérieur ! Pour ce faire on utilise l’instruction :

EXPOSE 80

On a presque fini. Il manque une instruction essentielle : celle qui va indiquer à l’image ce qu’il faut faire quand on démarrera un conteneur.

Question:

Il faut faire quoi ?

Il faut lancer le processus nginx, pour que le serveur web tourne ! Et en l’occurrence, il faut le lancer en premier plan.

Note:

Heu, comment ça en premier plan ?

Sur Linux et ailleurs, les programmes peuvent se lancer en premier plan ou en arrière plan, dans un terminal. En premier plan, ça veut dire que tant que le logiciel n’a pas fini de s’exécuter, on ne peut plus rien faire dans le terminal. En arrière plan (on appelle ça un démon, ou daemon), on a la main sur le terminal et on peut continuer à taper des commandes.

Sauf que si nginx est lancé en arrière plan, ce qui est le défaut, Docker va chercher une autre commande à exécuter, et comme il n’y en a pas, il va juste… s’arrêter. Voilà pourquoi il faut toujours que la commande lancée par le conteneur à son démarrage soit en premier plan.

Bref, pour faire ça, il faut passer à nginx l’argument -g 'daemon off;'. La commande à lancer s’indiquer avec l’instruction CMD, suivi d’un tableau ["commande", "argument 1", "argument 2", "..."]. Ce qui nous donne :

CMD ["nginx", "-g", "daemon off;"]

On arrive donc à notre premier Dockerfile :

FROM debian
RUN apt-get update
RUN apt-get install -y nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Lancer la cuisson

Question:

Alors c’est bien beau tout ça, mis comment on construit l’image ?

docker build -t nginx:quentin .

On voit toutes les étapes défiler, la plus longue étant le téléchargement et l’installation de nginx. Docker maintient un cache de chacune des étapes de la construction de l’image, c’est-à-dire que si tu relances la construction d’une image, Docker n’aura pas besoin de tout refaire, il a gardé en mémoire les résultats intermédiaires de chaque étape, aussi appelée layer. Si tu relances une deuxième fois…

À table !

Maintenant qu’on a notre image qui contient tout ce dont on a besoin, on va l’instancier, c’est-à-dire créer un conteneur à partir de l’image. C’est la commande docker run qui fait ça, et je te propose de lui passer quelques arguments :

docker run -d --name quentin nginx:quentin

Note:

On peut lancer autant de conteneurs qu’on veut à partir de cette image, du moment qu’ils ont un nom différent.

La commande docker ps permet de voir que le conteneur a bien été lancé.

A Savoir:

La commande curl permet de consulter une page web depuis un terminal. Il suffit de lui donner une URL, et par défaut, curl utilise le port 80.

On devrait donc pouvoir accéder à la page d’accueil par défaut de nginx !

Question:

Comment ça, connexion refusée ? m(

En fait, on a oublié un détail. C’est que le conteneur a ses propres ports. On a bien indiqué au conteneur d’ouvrir son port 80, mais quand on tape pica01-test.picasoft.net, on parle à la machine, pas au conteneur… La situation est la suivante :

Il faudrait donc indiquer à Docker que quand on toque au port 80 de la machine hôte depuis Internet (ce que fait curl), il faut en réalité aller toquer au port 80 du conteneur quentin !

Pour ce faire, on utilise l’argument -p <port machine>:<port conteneur>.

Important:

À chaque fois que tu crées un nouveau conteneur, il faudra supprimer l’ancien avant puisque deux conteneurs ne peuvent pas avoir le même nom. Pour ce faire :

docker rm -f quentin

où tu remplaces quentin par le nom de ton conteneur.

Aussi, je n’aurais pas dû utiliser le port 80 de l’hôte car il est souvent utilisé par un autre service, appelé Traefik. Tu peux l’éteindre en faisant :

docker stop traefik

Ça nous donne :

docker run -d --name quentin -p 80:80 nginx:quentin

On se retrouve donc dans la situation suivante :

Alors, ça donne quoi ?

On voit bien le code de la page d’accueil de nginx ! Nickel ! :-D

Question:

Ouais enfin bon, un site web c’est quand même bien avec un peu de contenu, non ?

Pimper le serveur web

Un serveur web sans contenu, c’est comme une fondue sans fromage : un peu sec.

Alors, comment on change la page d’accueil de nginx, par exemple ?

Note:

nginx sert les fichiers qui se trouvent dans le dossier /var/www/html. Si on ne lui précise rien, il sert le fichier index.html. Notre but, c’est donc de remplacer le fichier /var/www/html/index.html dans le conteneur nginx ! :-D

Tu vas créer un dossier website dans lequel se trouvera un fichier index.html, avec le contenu que tu veux.

Exemple :

Modifier la recette

Une des premières idées, c’est de modifier l’image pour qu’elle embarque avec elle le fichier index.html.

Dans un Dockerfile, la syntaxe pour copier un fichier est la suivante :

COPY <source> <destination>

Bon, normalement, à ce stade, tu sais faire !

  1. Supprimer le conteneur qui tourne
  2. Modifier le Dockerfile
  3. Reconstruire l’image
  4. Recréer le conteneur
  5. Faire un curl et croiser les doigts!

Je te laisse retrouver les instructions et essayer, et je te montre en dessous ce que ça donne chez moi!

Ça fonctionne bien.

Question:

Est-ce-que tu vois un problème avec cette façon de faire ?

Question:

Par exemple, projette toi : tu crées ton blog, tu publies des articles tous les jours. Est-ce-que tu te vois reconstruire l’image tous les jours ? Est-ce-que ça a du sens ?

En fait, le « sens » de l’image, c’est d’avoir un serveur web qui est prêt à servir tous les fichiers qu’on lui donne. Rajouter un fichier particulier à l’image la rend très spécialisée, personne ne peut la ré-utiliser à part toi… Or, l’idée des images, c’est quand même d’être réutilisables !

Donc, ce qu’on voudrait, c’est lancer l’image et lui dire en même temps « hey, tu peux servir ces fichiers là au lieu de ta page d’accueil par défaut pourrie ? »

Monter des fichiers dans un conteneur

Note:

On parle de montage quand on insère un clé USB et que ça donne un dossier… là c’est un peu pareil, on veut prendre un dossier de la machine et en faire un dossier du conteneur, sans modifier l’image.

Il y a une syntaxe pour ça, quand on lance un conteneur :

-v <chemin machine>:<chemin conteneur>

À ce stade, tu devrais aussi pouvoir faire l’opération. Concrètement, l’idée c’est de :

Je te montre ce que ça donne chez moi :

Note:

Si tu modifies ton fichier sur la machine pendant que le conteneur tourne et que tu refais un curl, tu verras que la modification se voit immédiatement, parce que le dossier est monté dans le conteneur! Plus besoin de recréer le conteneur, donc.

Il y a une dernière méthode, c’est d’utiliser un volume Docker.

Passer aux choses sérieuses

Question:

C’est quoi le problème ? C’est pas bien les montages ?

Si, c’est chouette, mais ça pose quelques « inconvénients ». Je vais pas rentrer dans les détails, mais imagine que quelqu’un supprime ton dossier par erreur, ben c’est un peu dommage… Le chemin du dossier dépend de l’hôte, c’est pas très « portable ». Aussi, quand tu montes un dossier vide sur un dossier non-vide, ça l’écrase, parfois ce n’est pas le comportement attendu… Alors, c’est souvent plus pratique d’utiliser les volumes Docker !

Important:

Un volume Docker n’est pas supprimé quand le conteneur qui le monte est supprimé. Les volumes Docker sont souvent utilisés pour persister des données qui sinon seraient perdues, par exemple les données d’une base de données.

Un volume Docker, c’est un dossier caché quelque par dans /var/lib/docker/volumes sur la machine et qui est géré par Docker. Il a un nom, on peut lister les volumes avec des commandes Docker, et quand on monte un volume vide sur un dossier non vide, le contenu du dossier est copié dans le volume. Ça peut être utile, mais passons les détails.

Pour regarder les volumes déjà créés, tu peux taper :

docker volume ls

Pour créer un nouveau volume :

docker volume create quentin

avec le nom que tu veux.

Ensuite, pour monter le volume dans le conteneur, c’est la même syntaxe, sauf qu’au lieu du chemin sur la machine, on utilise le nom du volume ! On pense à supprimer le conteneur, et ça nous donne ceci :

docker rm -f quentin
docker run -d --name quentin -p 80:80 -v quentin:/var/www/html nginx:quentin

Question:

À ton avis, que contient le volume Docker après le lancement ?

Si on revient un peu en arrière, on se rappelle qu’on a copié index.html directement dans l’image qu’on utilise. Comme on a monté un volume vide (quentin) sur un dossier non-vide, le contenu du dossier a été copié. Le volume quentin contient donc l’index.html de l’image, pour le moment.

Si tu fais un curl, tu devrais donc voir ton premier message apparaître.

Question:

Comment remplacer le fichier dans le volume, alors ?

On va tout simplement copier notre fichier dans /var/www/html, qui sera directement copié dans le volume! Et comme c’est un volume, il ne sera pas supprimé quand on supprimera le conteneur, ce qui veut dire qu’on pourra le monter une prochaine fois avec le bon fichier dedans. Pour ce faire, une commande :

docker cp <chemin machine> <nom du conteneur>:<chemin conteneur>

Je te laisse essayer, et je te montre ce que ça donne chez moi :

C'est un peu lourd, non ?

On a vu comment créer une image Docker, comment relier ses ports à ceux de la machine, et comment relier ses dossiers à ceux de la machine.

Mais oui, c’est un peu lourd… parce que ça implique qu’à chaque fois qu’on recrée un conteneur, par exemple pour faire une mise à jour, il faut écrire une ligne de commande longue comme le bras, ne pas se tromper, bien connaître l’ensemble des ports et des volumes… c’est un coup à devenir fou. :-P

Heureusement, chez Picasoft (et partout ailleurs, en fait), on fait autrement. Et c’est grâce à Docker Compose, qui permet de décrire la configuration des conteneurs une fois pour toutes dans un fichier et de les lancer avec une commande qui tient dans la main!