335 lines
11 KiB
Markdown
335 lines
11 KiB
Markdown
Ma première image ... par `Dockerfile`
|
||
--------------------------------------
|
||
|
||
Pour construire une image, nous ne sommes pas obligés de passer par une série
|
||
de *commits*. Docker dispose d'un mécanisme permettant d'automatiser la
|
||
construction de nouvelles images. Nous pouvons arriver au même résultat que ce
|
||
que l'on a réussi à faire précédemment en utilisant le `Dockerfile` suivant :
|
||
|
||
<div lang="en-US">
|
||
```dockerfile
|
||
FROM ubuntu:latest
|
||
|
||
RUN apt-get update
|
||
RUN apt-get install -y nano
|
||
```
|
||
</div>
|
||
|
||
La syntaxe d'un `Dockerfile` est simple : le premier mot de chaque ligne est
|
||
l'intitulé d'une instruction (que l'on écrit généralement en majuscule), elle
|
||
est suivie de ses arguments.
|
||
|
||
Dans notre exemple, nous utilisons `FROM`{.dockerfile} qui indique une image de
|
||
départ à utiliser ; `RUN`{.dockerfile} est une commande qui sera exécutée dans
|
||
le conteneur, dans le but de le construire.
|
||
|
||
Pour lancer la construction de la nouvelle image, créons un nouveau dossier ne
|
||
contenant que notre fichier `Dockerfile`, plaçons-nous ensuite dedans, puis
|
||
lançons la commande `build` :
|
||
|
||
<div lang="en-US">
|
||
```bash
|
||
docker image build --tag=my_editor .
|
||
```
|
||
</div>
|
||
|
||
Une fois la construction de l'image terminée, nous pouvons la lancer et
|
||
constater l'existence de notre éditeur favori :
|
||
|
||
<div lang="en-US">
|
||
```bash
|
||
docker container run -it my_editor /bin/bash
|
||
(in_cntr)# nano
|
||
```
|
||
</div>
|
||
|
||
|
||
### `RUN` dans le `Dockerfile`
|
||
|
||
Dans un `Dockerfile`, chaque ligne est exécutée indépendamment des autres et
|
||
correspondra à une nouvelle couche de notre image. Exactement comme on a
|
||
réalisé le script à la fin de la partie précédente.
|
||
|
||
Cela signifie que l'exemple suivant **ne fonctionne pas** :
|
||
|
||
<div lang="en-US">
|
||
```dockerfile
|
||
COPY db.sql /db.sql
|
||
RUN service mysqld start
|
||
RUN mysql -u root -p toor virli < /db.sql
|
||
```
|
||
</div>
|
||
|
||
Cet exemple ne fonctionne pas car le serveur MySQL est bien lancé dans le
|
||
premier `RUN`{.dockerfile}, mais il se trouve brutalement arrêté dès lors que
|
||
la commande `service` se termine. En fait, à chaque instruction, Docker réalise
|
||
automatiquement l'équivalent un `docker run` suivi d'un `commit`. Et vous
|
||
pouvez constater par vous-même que, en créant l'image `tinysql` à partir d'un
|
||
simple `apt install mysql` :
|
||
|
||
<div lang="en-US">
|
||
```bash
|
||
docker container run tinysql service mysqld start
|
||
```
|
||
</div>
|
||
|
||
rend la main directement, sans laisser de `mysqld` dans l'arborescence de
|
||
processus.\
|
||
|
||
Pour avoir le résultat escompté, il faut exécuter les commandes ensemble :
|
||
|
||
<div lang="en-US">
|
||
```dockerfile
|
||
COPY db.sql /db.sql
|
||
RUN service mysqld start && mysql -u root -p toor virli < /db.sql
|
||
```
|
||
</div>
|
||
|
||
Après le `RUN`{.dockerfile}, MySQL sera de nouveau tué.\
|
||
|
||
En aucun cas, une commande exécutée par un `RUN`{.dockerfile} se retrouvera en
|
||
cours d'exécution lorsque l'on invoquera un conteneur par `docker container
|
||
run`. Seul la commande fournie par l'utilisateur ou la commande par défaut de
|
||
l'image sera exécutée au lancement d'un conteneur.
|
||
|
||
|
||
### Exposer des ports
|
||
|
||
Construisons maintenant un conteneur avec un service web :
|
||
|
||
<div lang="en-US">
|
||
```dockerfile
|
||
FROM my_editor
|
||
|
||
RUN apt-get update
|
||
RUN apt-get install -y nginx
|
||
|
||
EXPOSE 80
|
||
```
|
||
</div>
|
||
|
||
L'instruction `EXPOSE`{.dockerfile} sera traitée plus tard par le client Docker
|
||
(équivalent à l'argument `--expose`). Il s'agit d'une métadonnée qui sera
|
||
attachée à l'image (et à toutes ses images filles). Elle ne crée d'ailleurs pas
|
||
de couche supplémentaire dans notre image.\
|
||
|
||
En précisant tous les ports qu'expose une image dans ses métadonnées, ces
|
||
ports seront automatiquement exposés en utilisant l'option `-P` du `run` : cela
|
||
assigne une redirection de port aléatoire sur la machine hôte vers votre
|
||
conteneur :
|
||
|
||
<div lang="en-US">
|
||
```
|
||
42sh$ docker image build --tag=my_webserver .
|
||
42sh$ docker container run -it -P my_webserver /bin/bash
|
||
(cntnr)# service nginx start
|
||
```
|
||
</div>
|
||
|
||
Dans un autre terminal, lançons un `docker container ls`, pour consulter la colonne
|
||
*PORTS* afin de connaître le port choisi par Docker pour effectuer la redirection.
|
||
|
||
Rendez-vous ensuite dans votre navigateur sur <http://localhost:49153/>.
|
||
|
||
:::::: {.exercice}
|
||
|
||
#### À vous de jouer {-}
|
||
|
||
Utilisez l'instruction `COPY`{.dockerfile} pour afficher votre propre
|
||
`index.html` remplaçant celui installé de base par `nginx`. <!--Si vous manquez
|
||
d'inspiration, utilisez [cette page de compte à
|
||
rebours](https://virli.nemunai.re/countdown.html).-->
|
||
|
||
:::::
|
||
|
||
### Les caches
|
||
|
||
Nous avons vu que chaque instruction de notre `Dockerfile` est exécutée dans un
|
||
conteneur, qui génère une image intermédiaire. Cette image intermédiaire sert
|
||
ensuite d'image de base pour le conteneur qui sera lancé avec l'instruction
|
||
suivante.
|
||
|
||
Lorsqu'on lance la reconstruction du même `Dockerfile`, les images
|
||
intermédiaires sont réutilisées, comme un cache d'instructions. Cela permet de
|
||
gagner du temps sur les étapes qui n'ont pas changé. Ainsi, lorsque vous
|
||
modifiez une instruction dans votre `Dockerfile`, les instructions précédentes
|
||
ne sont pas réexécutées mais sont ressorties du cache.
|
||
|
||
Le cache se base principalement sur le contenu de chaque instruction du
|
||
`Dockerfile` (pour les `COPY` et `ADD`, il va aussi regarder la date de
|
||
dernière modification de fichier à copier ou à ajouter). Donc tant qu'une
|
||
instruction n'est pas modifiée dans le `Dockerfile`, le cache sera utilisé.
|
||
|
||
Il est possible de ne pas utiliser le cache et de relancer toutes les étapes du
|
||
`Dockerfile` en ajoutant l'option `--no-cache` au moment du `docker image
|
||
build`.
|
||
|
||
Les couches du cache peuvent être partagées entre plusieurs conteneurs, c'est
|
||
ainsi que vous pouvez partager facilement une plus grosse partie du système de
|
||
fichiers (rappelez-vous le principe d'union FS).
|
||
|
||
Pour profiter du cache, on va placer de préférence les étapes les plus
|
||
génériques (qui seraient les plus susceptibles d'apparaître dans d'autres
|
||
images), en haut du `Dockerfile`.
|
||
|
||
|
||
### Métadonnées pures
|
||
|
||
L'instruction `LABEL`{.dockerfile} permet d'ajouter une métadonnée à une image,
|
||
sous forme de clef/valeur.
|
||
|
||
Une métadonnée courante[^MAINTAINER] est d'indiquer le nom du
|
||
mainteneur de l'image :
|
||
|
||
[^MAINTAINER]: Voir par exemple : <https://github.com/nginxinc/docker-nginx/blob/master/stable/debian/Dockerfile#L8>
|
||
|
||
<div lang="en-US">
|
||
```dockerfile
|
||
LABEL maintainer="Pierre-Olivier Mercier <nemunaire@nemunai.re>"
|
||
```
|
||
</div>
|
||
|
||
Dans notre `Dockerfile`, indiquez juste après l'image de base, vos noms,
|
||
prénoms et mails de contact avec l'instruction `LABEL maintainer`{.dockerfile},
|
||
pour indiquer que c'est vous qui maintenez cette image, si des utilisateurs ont
|
||
besoin de vous avertir pour le mettre à jour ou s'ils rencontrent des
|
||
difficultés par exemple.
|
||
|
||
On le place dès le début, car comme c'est une information qui n'est pas amenée
|
||
à changer, elle sera toujours retrouvée en cache.
|
||
|
||
|
||
### Commande par défaut
|
||
|
||
Vous pouvez placer dans un `Dockerfile` une instruction `CMD`{.dockerfile} qui
|
||
sera exécutée si aucune commande n'est passée lors du `run`, par exemple :
|
||
|
||
<div lang="en-US">
|
||
```dockerfile
|
||
CMD nginx -g "daemon off;"
|
||
```
|
||
</div>
|
||
|
||
<div lang="en-US">
|
||
```bash
|
||
42sh$ docker image build --tag=my_nginx .
|
||
42sh$ docker container run -d -P my_nginx
|
||
```
|
||
</div>
|
||
|
||
L'option `-d` passée au `run` lance le conteneur en tâche de fond. Si vous
|
||
constatez via un `docker container ls` que le conteneur s'arrête directement,
|
||
retirez cette option pour voir ce qui ne va pas, ou utilisez la commande
|
||
`docker container logs`.
|
||
|
||
|
||
### Construire son application au moment de la construction du conteneur ?
|
||
|
||
Comment faire lorsque l'on a besoin de compiler une application avant de
|
||
l'intégrer dans le conteneur ?
|
||
|
||
On peut vouloir lancer la compilation sur notre machine, mais cela ne sera pas
|
||
très reproductible et cela aura nécessité d'installer le compilateur et les
|
||
outils liés au langage que l'on souhaite compiler. Peut-être que plusieurs
|
||
versions de ces outils existent, laquelle choisir ? ... Ok c'est trop
|
||
compliqué.
|
||
|
||
D'un autre côté, si l'on fait cela dans un conteneur, celui-ci contiendra dans
|
||
ses couches des données inutiles à l'exécution : les sources, les produits
|
||
intermédiaires de compilation, le compilateur, n'ont rien à faire dans les
|
||
couches de notre image.
|
||
|
||
Le meilleur des deux mondes se trouve dans les *Multi-stage builds* : au sein
|
||
du même `Dockerfile`, on va réaliser les opérations de préparation dans un ou
|
||
plusieurs conteneurs, avant d'agréger le contenu compilé au sein du conteneur
|
||
final :
|
||
|
||
<div lang="en-US">
|
||
```dockerfile
|
||
FROM gcc:4.9
|
||
COPY . /usr/src/myapp
|
||
WORKDIR /usr/src/myapp
|
||
RUN gcc -static -static-libgcc -o hello hello.c
|
||
|
||
FROM scratch
|
||
COPY --from=0 /usr/src/myapp/hello /hello
|
||
CMD ["/hello"]
|
||
```
|
||
</div>
|
||
|
||
Dans cet exemple, deux conteneurs distincts sont créés : le premier à partir de
|
||
l'image `gcc`, il contient tout le nécessaire pour compiler notre
|
||
`hello.c`. Mais l'image finale (le dernier `FROM`{.dockerfile} de notre
|
||
`Dockerfile`) est l'image vide, dans laquelle nous recopions simplement le
|
||
produit de notre compilation.
|
||
|
||
L'image ainsi générée est minime, car elle ne contient rien d'autre que le
|
||
strict nécessaire pour s'exécuter.
|
||
|
||
#### Étapes nommées
|
||
|
||
Nous avons utilisé `--from=0` pour désigner la première image de notre
|
||
`Dockerfile`. Lorsque l'on réalise des montages plus complexes, on peut vouloir
|
||
donner des noms à chaque image, plutôt que de devoir jongler avec les
|
||
numéros. Dans ce cas, on indiquera :
|
||
|
||
<div lang="en-US">
|
||
```dockerfile
|
||
FROM gcc:4.9 as builder
|
||
COPY . /usr/src/myapp
|
||
WORKDIR /usr/src/myapp
|
||
RUN gcc -static -static-libgcc -o hello hello.c
|
||
|
||
FROM scratch
|
||
COPY --from=builder /usr/src/myapp/hello /hello
|
||
CMD ["/hello"]
|
||
```
|
||
</div>
|
||
|
||
Par défaut la dernière étape du `Dockerfile` est retenue comme étant l'image que
|
||
l'on souhaite `tagger`, mais il est possible de préciser quelle image
|
||
spécifiquement on souhaite construire avec l'option `--target` :
|
||
|
||
<div lang="en-US">
|
||
```
|
||
42sh$ docker build --target builder -t hello-builder .
|
||
```
|
||
</div>
|
||
|
||
Cela peut être particulièrement utile si l'on dispose d'une image de debug,
|
||
incluant tous les symboles, et une image de production, plus propre. On
|
||
sélectionnera ainsi avec l'option `--target` l'un ou l'autre en fonction de
|
||
l'environnement dans lequel on souhaite se déployer.
|
||
|
||
|
||
### D'autres instructions ?
|
||
|
||
Consultez <https://docs.docker.com/engine/reference/builder/> pour la liste
|
||
complète des instructions reconnues.
|
||
|
||
|
||
::::: {.exercice}
|
||
|
||
Pour mettre en application tout ce que nous venons de voir, réalisons le
|
||
`Dockerfile` du service web [`youp0m`](https://you.p0m.fr/) que nous avons
|
||
déjà utilisé précédemment.
|
||
|
||
Pour réaliser ce genre de contribution, on ajoute généralement un `Dockerfile`
|
||
à la racine du dépôt.
|
||
|
||
Vous pouvez cloner le dépôt de sources de `youp0m` à :\
|
||
<https://git.nemunai.re/nemunaire/youp0m.git>
|
||
|
||
Pour compiler le projet, vous pouvez utiliser dans votre `Dockerfile`
|
||
|
||
<div lang="en-US">
|
||
```dockerfile
|
||
FROM golang:1.16
|
||
COPY . /go/src/git.nemunai.re/youp0m
|
||
WORKDIR /go/src/git.nemunai.re/youp0m
|
||
RUN go build -tags dev -v
|
||
```
|
||
</div>
|
||
|
||
:::::
|