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
É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.
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 .
-t
(pour tag) permet de donner un nom (nginx
) et une étiquette (quentin
) à l’image que l’on va construire. Si on est plusieurs à construire l’image, chacun prendra une étiquette différente..
indique le dossier où se trouve le Dockerfile. Dans le monde Linux,.
indique le dossier courant.
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
-d
(detach) pour lancer le conteneur en arrière plan, sinon on reste bloqué tant qu’il n’est pas éteint--name
pour préciser le nom du conteneur, iciquentin
. Sinon il prend un nom au hasard et c’est pas facile de le manipuler…- et enfin, l’image à lancer, celle qu’on vient de construire !
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 ?
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 !
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 !
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 !
- Supprimer le conteneur qui tourne
- Modifier le Dockerfile
- Reconstruire l’image
- Recréer le conteneur
- 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>
-v
pour volumes. On en parle juste après!- Le chemin de la machine doit être absolu, pas relatif à là où tu te trouves. Pour savoir le chemin absolu du dossier courant, utilise la commande
pwd
.
À ce stade, tu devrais aussi pouvoir faire l’opération. Concrètement, l’idée c’est de :
- Supprimer l’ancien conteneur
- Changer le fichier
index.html
de l’hôte, pour bien voir le changement par rapport au fichier qu’on a copié dans l’image - Lancer le conteneur en montant le dossier
website
sur/var/www/html
(le chemin machine sera le résultat de la commandepwd
+/website
) - Lancer un
curl
et observer la magie !
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.
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!